kld-sdd 2.7.8-5 → 2.7.8-7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kld-sdd-init.js +86 -1
- package/lib/init.js +13 -1
- package/lib/maintenance.js +126 -1
- package/lib/skills-bundle.js +2 -0
- package/package.json +2 -2
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/index.cjs +30 -3
- package/skywalk-sdd/kb-upload.cjs +160 -33
- 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/schema.cjs +6 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +507 -0
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -10
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +11 -8
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +2 -1
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +3 -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-spec/SKILL.md +5 -3
|
@@ -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
|
|
@@ -50,15 +55,18 @@ function cacheKey(changeName, context) {
|
|
|
50
55
|
return JSON.stringify([context.api.replace(/\/+$/, ''), context.spaceId, context.kbId, changeName]);
|
|
51
56
|
}
|
|
52
57
|
|
|
53
|
-
function assertIngestionResult(result) {
|
|
58
|
+
function assertIngestionResult(result, options = {}) {
|
|
54
59
|
if (result?.code != null && result.code !== 0) throw new Error(result.message || 'KB API 拒绝入库');
|
|
55
60
|
const job = result?.data || result;
|
|
56
61
|
if (!job || !['projecting', 'succeeded'].includes(job.status) || job.relationStatus !== 'committed') {
|
|
57
62
|
throw new Error(`INGEST_NOT_COMMITTED: ${job?.errorCode || job?.status || 'unknown'} ${job?.errorMessage || ''} job=${job?.jobId || 'unknown'}`);
|
|
58
63
|
}
|
|
59
|
-
if (Number(job.report?.version_conflicts || 0) > 0
|
|
64
|
+
if (Number(job.report?.version_conflicts || 0) > 0) {
|
|
60
65
|
throw new Error(`VERSION_CONFLICT: 事实包已保存为候选,当前事实未更新;请合并基线后重新发布。job=${job.jobId}`);
|
|
61
66
|
}
|
|
67
|
+
if (job.report?.publication_status === 'needs_review' && !options.allowNeedsReview) {
|
|
68
|
+
throw new Error(`REVIEW_REQUIRED: 事实包已保存为候选,当前事实未更新;请审核本体后重新发布。job=${job.jobId}`);
|
|
69
|
+
}
|
|
62
70
|
return job;
|
|
63
71
|
}
|
|
64
72
|
|
|
@@ -240,9 +248,65 @@ function preCheckProjectId(projectId, state, selectedTarget) {
|
|
|
240
248
|
* 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
|
|
241
249
|
* 6. 返回 zip 路径
|
|
242
250
|
*/
|
|
243
|
-
|
|
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 = {}) {
|
|
244
307
|
const runtime = require('./ontology/runtime.cjs');
|
|
245
308
|
const archivePackage = require('./ontology/archive-package.cjs');
|
|
309
|
+
const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
|
|
246
310
|
|
|
247
311
|
// 1. 语义管线
|
|
248
312
|
console.log('→ 运行语义管线 (semantic-reconcile)...');
|
|
@@ -250,13 +314,20 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
250
314
|
profile: 'auto',
|
|
251
315
|
markPending: true,
|
|
252
316
|
});
|
|
317
|
+
const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
|
|
318
|
+
const legacyIdentityOnly = legacy.legacyCompatible;
|
|
253
319
|
if (!semanticResult.valid) {
|
|
254
|
-
|
|
255
|
-
.
|
|
256
|
-
|
|
257
|
-
|
|
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})`);
|
|
258
330
|
}
|
|
259
|
-
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
260
331
|
|
|
261
332
|
// 2. 创建临时 staging 目录
|
|
262
333
|
const stagingDir = path.join(
|
|
@@ -269,28 +340,63 @@ function buildPackageFromFolder(specRoot, changeName, options) {
|
|
|
269
340
|
const changeDir = path.join(specRoot, 'openspec', 'changes', changeName);
|
|
270
341
|
copyDirRecursive(changeDir, stagingDir);
|
|
271
342
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
}
|
|
287
386
|
|
|
288
387
|
console.log(`✓ Archive Package 已生成`);
|
|
289
388
|
console.log(` archive_id: ${result.manifest.archive_id}`);
|
|
290
389
|
console.log(` project_id: ${result.manifest.project_id}`);
|
|
291
390
|
console.log(` files: ${result.manifest.files.length}`);
|
|
292
391
|
|
|
293
|
-
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
|
+
};
|
|
294
400
|
} catch (err) {
|
|
295
401
|
// 出错时清理 staging
|
|
296
402
|
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
@@ -313,7 +419,7 @@ async function uploadZip(zipPath, state, options) {
|
|
|
313
419
|
|
|
314
420
|
try {
|
|
315
421
|
const result = await uploadFile(uploadUrl, zipPath, state.apiKey);
|
|
316
|
-
assertIngestionResult(result);
|
|
422
|
+
assertIngestionResult(result, options);
|
|
317
423
|
return result;
|
|
318
424
|
} catch (err) {
|
|
319
425
|
throw new Error(`上传失败: ${err.message}`);
|
|
@@ -330,6 +436,7 @@ async function main() {
|
|
|
330
436
|
console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
|
|
331
437
|
console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
|
|
332
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 # 历史产物只上传候选,不自动确认本体');
|
|
333
440
|
process.exit(1);
|
|
334
441
|
}
|
|
335
442
|
|
|
@@ -413,12 +520,17 @@ async function main() {
|
|
|
413
520
|
markPending: true,
|
|
414
521
|
});
|
|
415
522
|
if (!semanticResult.valid) {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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})`);
|
|
420
533
|
}
|
|
421
|
-
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
422
534
|
console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
|
|
423
535
|
return;
|
|
424
536
|
}
|
|
@@ -438,8 +550,9 @@ async function main() {
|
|
|
438
550
|
|
|
439
551
|
// 5. 构建 Archive Package
|
|
440
552
|
console.log('');
|
|
441
|
-
const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
|
|
553
|
+
const { zipPath, manifest, stagingDir, requiresReview, legacyStatus } = buildPackageFromFolder(specRoot, changeName, {
|
|
442
554
|
reason: args.reason,
|
|
555
|
+
requireReview: Boolean(args['require-review']),
|
|
443
556
|
});
|
|
444
557
|
|
|
445
558
|
try {
|
|
@@ -492,6 +605,7 @@ async function main() {
|
|
|
492
605
|
const result = await uploadZip(zipPath, state, {
|
|
493
606
|
spaceId: target.spaceId,
|
|
494
607
|
kbId: target.kbId,
|
|
608
|
+
allowNeedsReview: requiresReview,
|
|
495
609
|
});
|
|
496
610
|
|
|
497
611
|
console.log('\n── 上传结果 ──');
|
|
@@ -504,8 +618,21 @@ async function main() {
|
|
|
504
618
|
console.log(` change_id: ${manifest.change_id}`);
|
|
505
619
|
console.log(` files: ${manifest.files.length}`);
|
|
506
620
|
|
|
507
|
-
|
|
508
|
-
|
|
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
|
+
}
|
|
509
636
|
|
|
510
637
|
// === 投影状态提示 ===
|
|
511
638
|
// 上传成功后,KB 后端异步执行 vector embedding + AGE graph 投影。
|
|
@@ -4,6 +4,13 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { generateUuidV7 } = require('./id.cjs');
|
|
7
|
+
const {
|
|
8
|
+
SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
9
|
+
SPEC_ONTOLOGY_PATH,
|
|
10
|
+
buildSpecOntology,
|
|
11
|
+
validateSpecOntology,
|
|
12
|
+
specOntologyContentSignature,
|
|
13
|
+
} = require('./spec-ontology.cjs');
|
|
7
14
|
const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
|
|
8
15
|
const {
|
|
9
16
|
readJson,
|
|
@@ -581,6 +588,18 @@ function materializeArchivePackage(options = {}) {
|
|
|
581
588
|
);
|
|
582
589
|
const canonicalPath = path.join(archiveDir, CANONICAL_FACTS_PATH);
|
|
583
590
|
const canonicalBytes = writeJsonAtomic(canonicalPath, canonicalFacts);
|
|
591
|
+
const specOntology = buildSpecOntology(snapshot, {
|
|
592
|
+
projectId: projectIdentity.project_id,
|
|
593
|
+
archiveId,
|
|
594
|
+
compatibilityMode: 'ADAPTED_V2',
|
|
595
|
+
reviewStatus: 'confirmed',
|
|
596
|
+
});
|
|
597
|
+
const specOntologyValidation = validateSpecOntology(specOntology);
|
|
598
|
+
if (!specOntologyValidation.valid) {
|
|
599
|
+
throw new Error(`Spec 本体事实包校验失败: ${specOntologyValidation.errors.join('; ')}`);
|
|
600
|
+
}
|
|
601
|
+
const specOntologyPath = path.join(archiveDir, SPEC_ONTOLOGY_PATH);
|
|
602
|
+
writeJsonAtomic(specOntologyPath, specOntology);
|
|
584
603
|
|
|
585
604
|
const conversionReport = {
|
|
586
605
|
schema_version: CONVERSION_REPORT_SCHEMA_VERSION,
|
|
@@ -589,6 +608,9 @@ function materializeArchivePackage(options = {}) {
|
|
|
589
608
|
source_schema: String(snapshot.schema_version || 'unknown'),
|
|
590
609
|
target_file: CANONICAL_FACTS_PATH,
|
|
591
610
|
target_schema: CANONICAL_FACTS_SCHEMA_VERSION,
|
|
611
|
+
spec_ontology_file: SPEC_ONTOLOGY_PATH,
|
|
612
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
613
|
+
compatibility_mode: specOntology.compatibility_mode,
|
|
592
614
|
status: 'converted',
|
|
593
615
|
entity_count: canonicalFacts.entities.length,
|
|
594
616
|
relation_count: canonicalFacts.relations.length,
|
|
@@ -618,6 +640,9 @@ function materializeArchivePackage(options = {}) {
|
|
|
618
640
|
change_id: changeId,
|
|
619
641
|
content_hash: `sha256:${sha256(canonicalBytes)}`,
|
|
620
642
|
canonical_facts_path: CANONICAL_FACTS_PATH,
|
|
643
|
+
spec_ontology_path: SPEC_ONTOLOGY_PATH,
|
|
644
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
645
|
+
compatibility_mode: specOntology.compatibility_mode,
|
|
621
646
|
conversion_report_path: CONVERSION_REPORT_PATH,
|
|
622
647
|
created_at: createdAt,
|
|
623
648
|
files,
|
|
@@ -647,6 +672,121 @@ function materializeArchivePackage(options = {}) {
|
|
|
647
672
|
conversion_report: conversionReport,
|
|
648
673
|
manifest_path: existingManifestPath,
|
|
649
674
|
canonical_facts_path: canonicalPath,
|
|
675
|
+
spec_ontology_path: specOntologyPath,
|
|
676
|
+
conversion_report_path: path.join(archiveDir, CONVERSION_REPORT_PATH),
|
|
677
|
+
package_path: packagePath,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function materializeCompatibilityPackage(options = {}) {
|
|
682
|
+
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
683
|
+
const archiveDir = path.resolve(requiredText(options.archiveDir, 'archiveDir', 'Compatibility Package'));
|
|
684
|
+
const facts = options.facts;
|
|
685
|
+
if (!facts || !Array.isArray(facts.entities)) {
|
|
686
|
+
throw new Error('Compatibility Package 缺少已解析 facts');
|
|
687
|
+
}
|
|
688
|
+
const projectIdentity = ensureProjectIdentity(projectRoot);
|
|
689
|
+
const changeName = requiredText(options.changeName || facts.change, 'changeName', 'Compatibility Package');
|
|
690
|
+
const changeId = String(facts.change_id || `CHG-LEGACY-${sha256(changeName).slice(0, 12)}`).toUpperCase();
|
|
691
|
+
const publicationVariant = options.specOntology?.review_status === 'confirmed' ? 'reviewed' : 'candidate';
|
|
692
|
+
const archiveId = `urn:kld:sdd:archive:${sha256(
|
|
693
|
+
`${projectIdentity.project_id}\n${changeId}\n${facts.facts_hash || sha256(JSON.stringify(facts))}\n${publicationVariant}`,
|
|
694
|
+
)}`;
|
|
695
|
+
let specOntology;
|
|
696
|
+
if (options.specOntology) {
|
|
697
|
+
specOntology = JSON.parse(JSON.stringify(options.specOntology));
|
|
698
|
+
if (specOntology.project_id !== projectIdentity.project_id) {
|
|
699
|
+
throw new Error('已审核 Spec 本体的 project_id 与当前项目不一致,请重新物化并审核');
|
|
700
|
+
}
|
|
701
|
+
if (specOntology.compatibility_mode !== 'LEGACY_MATERIALIZED') {
|
|
702
|
+
throw new Error('兼容包只接受 LEGACY_MATERIALIZED 本体');
|
|
703
|
+
}
|
|
704
|
+
if (specOntology.review_status !== 'confirmed' || specOntology.publication_eligible !== true) {
|
|
705
|
+
throw new Error('历史 Spec 本体尚未审核,不能发布为 current 事实');
|
|
706
|
+
}
|
|
707
|
+
const currentOntology = buildSpecOntology({ ...facts, change_id: changeId }, {
|
|
708
|
+
projectId: projectIdentity.project_id,
|
|
709
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
710
|
+
reviewStatus: 'pending',
|
|
711
|
+
});
|
|
712
|
+
if (specOntologyContentSignature(specOntology) !== specOntologyContentSignature(currentOntology)) {
|
|
713
|
+
throw new Error('已审核 Spec 本体与当前 Markdown 内容不一致,请重新物化并审核');
|
|
714
|
+
}
|
|
715
|
+
specOntology.archive_id = archiveId;
|
|
716
|
+
specOntology.change = changeName;
|
|
717
|
+
specOntology.change_id = changeId;
|
|
718
|
+
delete specOntology.ontology_hash;
|
|
719
|
+
specOntology.ontology_hash = `sha256:${sha256(JSON.stringify(specOntology))}`;
|
|
720
|
+
} else {
|
|
721
|
+
specOntology = buildSpecOntology({ ...facts, change_id: changeId }, {
|
|
722
|
+
projectId: projectIdentity.project_id,
|
|
723
|
+
archiveId,
|
|
724
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
725
|
+
reviewStatus: 'pending',
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
const validation = validateSpecOntology(specOntology);
|
|
729
|
+
if (!validation.valid) {
|
|
730
|
+
throw new Error(`历史 Spec 本体物化失败: ${validation.errors.join('; ')}`);
|
|
731
|
+
}
|
|
732
|
+
const toleratedDiagnostics = (options.toleratedDiagnostics || [])
|
|
733
|
+
.map((item) => `LEGACY_DIAGNOSTIC_TOLERATED: ${item.code}: ${item.message}`);
|
|
734
|
+
const specOntologyPath = path.join(archiveDir, SPEC_ONTOLOGY_PATH);
|
|
735
|
+
const specOntologyBytes = writeJsonAtomic(specOntologyPath, specOntology);
|
|
736
|
+
const conversionReport = {
|
|
737
|
+
schema_version: CONVERSION_REPORT_SCHEMA_VERSION,
|
|
738
|
+
producer: PRODUCER,
|
|
739
|
+
source_file: 'legacy-markdown',
|
|
740
|
+
source_schema: String(facts.schema_version || 'legacy-markdown'),
|
|
741
|
+
target_file: SPEC_ONTOLOGY_PATH,
|
|
742
|
+
target_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
743
|
+
compatibility_mode: 'LEGACY_MATERIALIZED',
|
|
744
|
+
status: specOntology.publication_eligible ? 'materialized_reviewed' : 'materialized_pending_review',
|
|
745
|
+
entity_count: specOntology.entities.length,
|
|
746
|
+
relation_count: specOntology.relations.length,
|
|
747
|
+
warnings: [...new Set([...(specOntology.warnings || []), ...toleratedDiagnostics])],
|
|
748
|
+
};
|
|
749
|
+
writeJsonAtomic(path.join(archiveDir, CONVERSION_REPORT_PATH), conversionReport);
|
|
750
|
+
|
|
751
|
+
const createdAt = options.archivedAt || new Date().toISOString();
|
|
752
|
+
const filePaths = collectPackageFilePaths(archiveDir);
|
|
753
|
+
const files = filePaths.map((relativePath) => ({
|
|
754
|
+
path: relativePath,
|
|
755
|
+
content_hash: `sha256:${sha256(fs.readFileSync(path.join(archiveDir, relativePath)))}`,
|
|
756
|
+
}));
|
|
757
|
+
const manifest = {
|
|
758
|
+
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
759
|
+
producer: PRODUCER,
|
|
760
|
+
project_id: projectIdentity.project_id,
|
|
761
|
+
archive_id: archiveId,
|
|
762
|
+
change_id: changeId,
|
|
763
|
+
content_hash: `sha256:${sha256(specOntologyBytes)}`,
|
|
764
|
+
canonical_facts_path: null,
|
|
765
|
+
spec_ontology_path: SPEC_ONTOLOGY_PATH,
|
|
766
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
767
|
+
compatibility_mode: 'LEGACY_MATERIALIZED',
|
|
768
|
+
conversion_report_path: CONVERSION_REPORT_PATH,
|
|
769
|
+
created_at: createdAt,
|
|
770
|
+
files,
|
|
771
|
+
change: changeName,
|
|
772
|
+
archived_at: createdAt,
|
|
773
|
+
reason: options.reason || '历史 Spec 本体兼容入库',
|
|
774
|
+
method: options.method || 'legacy-spec-ontology-materialization',
|
|
775
|
+
source_path: options.sourcePath || '',
|
|
776
|
+
archive_path: toPosix(path.relative(projectRoot, archiveDir)),
|
|
777
|
+
copied_specs: [],
|
|
778
|
+
evidence_events_path: null,
|
|
779
|
+
};
|
|
780
|
+
const manifestPath = path.join(archiveDir, MANIFEST_PATH);
|
|
781
|
+
writeJsonAtomic(manifestPath, manifest);
|
|
782
|
+
const packagePath = path.resolve(options.packagePath || `${archiveDir}.zip`);
|
|
783
|
+
writeArchiveZip(archiveDir, packagePath, manifest);
|
|
784
|
+
return {
|
|
785
|
+
manifest,
|
|
786
|
+
conversion_report: conversionReport,
|
|
787
|
+
validation,
|
|
788
|
+
manifest_path: manifestPath,
|
|
789
|
+
spec_ontology_path: specOntologyPath,
|
|
650
790
|
conversion_report_path: path.join(archiveDir, CONVERSION_REPORT_PATH),
|
|
651
791
|
package_path: packagePath,
|
|
652
792
|
};
|
|
@@ -707,5 +847,6 @@ module.exports = {
|
|
|
707
847
|
readStoredZipEntries,
|
|
708
848
|
verifyArchivePackage,
|
|
709
849
|
materializeArchivePackage,
|
|
850
|
+
materializeCompatibilityPackage,
|
|
710
851
|
finalizeArchivePackageAfterReports,
|
|
711
852
|
};
|
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { DIAGNOSTIC_CODES, entityTypeForId } = require('./schema.cjs');
|
|
7
7
|
const { ensureStructuralIdentities } = require('./structural-identity.cjs');
|
|
8
8
|
|
|
9
|
-
const ENTITY_ID_PATTERN = /\b(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
|
|
9
|
+
const ENTITY_ID_PATTERN = /\b(?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
|
|
10
10
|
const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state|external-ref)\*\*\s*[::]\s*(.*?)\s*$/i;
|
|
11
11
|
|
|
12
12
|
function toPosix(value) {
|
|
@@ -444,7 +444,7 @@ function parseSpec(target, file, lines, frontmatter) {
|
|
|
444
444
|
});
|
|
445
445
|
continue;
|
|
446
446
|
}
|
|
447
|
-
const inheritedRelationMatch = line.match(/^\s*-\s+\*\*relation\*\*\s*[::]\s*`?((?:CHG|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)\s+(contains|acceptedBy|constrainedBy|realizes|implements|covers|dependsOn)\s+((?:CHG|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)`?\s*$/i);
|
|
447
|
+
const inheritedRelationMatch = line.match(/^\s*-\s+\*\*relation\*\*\s*[::]\s*`?((?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)\s+(contains|usesObject|acceptedBy|constrainedBy|realizes|implements|covers|dependsOn)\s+((?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)`?\s*$/i);
|
|
448
448
|
if (inheritedRelationMatch) {
|
|
449
449
|
let source = '';
|
|
450
450
|
for (let offset = index + 1; offset < Math.min(lines.length, index + 6); offset += 1) {
|
|
@@ -461,7 +461,28 @@ function parseSpec(target, file, lines, frontmatter) {
|
|
|
461
461
|
});
|
|
462
462
|
continue;
|
|
463
463
|
}
|
|
464
|
-
let match = line.match(/^\s*####\s
|
|
464
|
+
let match = line.match(/^\s*####\s+业务对象[::]\s*[【\[]?(OBJ-[A-Z0-9-]+)[】\]]?\s*(.*?)\s*$/i);
|
|
465
|
+
if (match) {
|
|
466
|
+
const objectId = match[1].toUpperCase();
|
|
467
|
+
const endIndex = sectionEnd(lines, index, 4);
|
|
468
|
+
addEntity(
|
|
469
|
+
target,
|
|
470
|
+
'BusinessObject',
|
|
471
|
+
objectId,
|
|
472
|
+
match[2],
|
|
473
|
+
file,
|
|
474
|
+
index + 1,
|
|
475
|
+
{ capability_id: capabilityId || undefined },
|
|
476
|
+
parseIdentityBlock(lines, index + 1, endIndex),
|
|
477
|
+
lines.slice(index, endIndex).join('\n'),
|
|
478
|
+
);
|
|
479
|
+
if (capabilityId) {
|
|
480
|
+
addRelation(target, 'usesObject', capabilityId, objectId, file, index + 1, 'asserted');
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
match = line.match(/^\s*####\s+需求项[::]\s*[【\[]?(STMT-[A-Z0-9-]+)[】\]]?\s*(.*?)\s*$/i);
|
|
465
486
|
if (match) {
|
|
466
487
|
currentStatement = match[1].toUpperCase();
|
|
467
488
|
currentConstraint = '';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const {
|
|
6
7
|
scanChange,
|
|
@@ -18,6 +19,11 @@ const listChanges = require('./list-changes.cjs');
|
|
|
18
19
|
const sddConfig = require('./sdd-config.cjs');
|
|
19
20
|
const activeChanges = require('./active-changes.cjs');
|
|
20
21
|
const { resolveProjectArg } = require('./resolve-spec-root.cjs');
|
|
22
|
+
const {
|
|
23
|
+
materializeChangeSpecOntology,
|
|
24
|
+
validateSpecOntology,
|
|
25
|
+
approveSpecOntologyFile,
|
|
26
|
+
} = require('./spec-ontology.cjs');
|
|
21
27
|
|
|
22
28
|
function parseArgs(argv) {
|
|
23
29
|
const result = { _: [] };
|
|
@@ -77,6 +83,9 @@ function showHelp() {
|
|
|
77
83
|
node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
|
|
78
84
|
node skywalk-sdd/ontology/cli.cjs observe --project=. --change=<name> [--interval=1500]
|
|
79
85
|
node skywalk-sdd/ontology/cli.cjs snapshot --project=. --change=<name> --archive=<path>
|
|
86
|
+
node skywalk-sdd/ontology/cli.cjs materialize-ontology --project=. --change=<name> [--output=<path>]
|
|
87
|
+
node skywalk-sdd/ontology/cli.cjs validate-spec-ontology --file=<spec-ontology.json>
|
|
88
|
+
node skywalk-sdd/ontology/cli.cjs approve-spec-ontology --file=<spec-ontology.json>
|
|
80
89
|
|
|
81
90
|
observe 只负责采集和同步;check/archive 前仍必须执行 reconcile。`);
|
|
82
91
|
}
|
|
@@ -254,11 +263,55 @@ function main(argv = process.argv.slice(2)) {
|
|
|
254
263
|
if (!result.ok) process.exitCode = 1;
|
|
255
264
|
return;
|
|
256
265
|
}
|
|
266
|
+
if (command === 'validate-spec-ontology') {
|
|
267
|
+
const filePath = path.resolve(args.file || args._[1] || 'spec-ontology.json');
|
|
268
|
+
const payload = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
269
|
+
const result = validateSpecOntology(payload);
|
|
270
|
+
console.log(JSON.stringify({ file: filePath, ...result }, null, 2));
|
|
271
|
+
if (!result.valid) process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (command === 'approve-spec-ontology') {
|
|
275
|
+
const filePath = path.resolve(args.file || args._[1] || 'spec-ontology.json');
|
|
276
|
+
const result = approveSpecOntologyFile(filePath, {
|
|
277
|
+
allowCollisions: args['allow-collisions'] === true,
|
|
278
|
+
});
|
|
279
|
+
console.log(JSON.stringify({
|
|
280
|
+
file: result.file,
|
|
281
|
+
compatibility_mode: result.payload.compatibility_mode,
|
|
282
|
+
review_status: result.payload.review_status,
|
|
283
|
+
publication_eligible: result.payload.publication_eligible,
|
|
284
|
+
validation: result.validation,
|
|
285
|
+
warnings: result.payload.warnings,
|
|
286
|
+
}, null, 2));
|
|
287
|
+
if (!result.validation.valid) process.exitCode = 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
257
290
|
const projectRoot = resolveCliProject(args);
|
|
258
291
|
const changeName = args.change;
|
|
259
292
|
if (!changeName) throw new Error('缺少 --change 参数');
|
|
260
293
|
const profile = args.profile || 'auto';
|
|
261
294
|
|
|
295
|
+
if (command === 'materialize-ontology' || command === 'materialize') {
|
|
296
|
+
const result = materializeChangeSpecOntology(projectRoot, changeName, {
|
|
297
|
+
outputPath: args.output,
|
|
298
|
+
mappingPath: args.mapping,
|
|
299
|
+
reviewStatus: args['review-status'],
|
|
300
|
+
});
|
|
301
|
+
console.log(JSON.stringify({
|
|
302
|
+
change: changeName,
|
|
303
|
+
compatibility_mode: result.payload.compatibility_mode,
|
|
304
|
+
review_status: result.payload.review_status,
|
|
305
|
+
publication_eligible: result.payload.publication_eligible,
|
|
306
|
+
output_path: result.output_path,
|
|
307
|
+
mapping_path: result.mapping_path,
|
|
308
|
+
validation: result.validation,
|
|
309
|
+
warnings: result.payload.warnings,
|
|
310
|
+
}, null, 2));
|
|
311
|
+
if (!result.validation.valid) process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
262
315
|
if (command === 'scan') {
|
|
263
316
|
const result = scanChange(projectRoot, changeName, { profile });
|
|
264
317
|
console.log(JSON.stringify(summary({ ...result, changed: false, paths: undefined }), null, 2));
|
|
@@ -42,7 +42,7 @@ function nextEntityId(prefix, scope, existingIds = []) {
|
|
|
42
42
|
|
|
43
43
|
function isValidEntityId(entityId, expectedType = '') {
|
|
44
44
|
const value = String(entityId || '').trim().toUpperCase();
|
|
45
|
-
if (!/^(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value)) {
|
|
45
|
+
if (!/^(?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value)) {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
if (expectedType && entityTypeForId(value) !== expectedType) {
|
|
@@ -4,6 +4,7 @@ const SCHEMA_VERSION = 'kld-sdd-ontology/v2';
|
|
|
4
4
|
|
|
5
5
|
const ENTITY_TYPES = Object.freeze([
|
|
6
6
|
'Change',
|
|
7
|
+
'BusinessObject',
|
|
7
8
|
'Capability',
|
|
8
9
|
'SpecificationStatement',
|
|
9
10
|
'AcceptanceCriterion',
|
|
@@ -17,6 +18,7 @@ const ENTITY_TYPES = Object.freeze([
|
|
|
17
18
|
|
|
18
19
|
const ID_PREFIX_TYPES = Object.freeze({
|
|
19
20
|
CHG: 'Change',
|
|
21
|
+
OBJ: 'BusinessObject',
|
|
20
22
|
CAP: 'Capability',
|
|
21
23
|
STMT: 'SpecificationStatement',
|
|
22
24
|
AC: 'AcceptanceCriterion',
|
|
@@ -37,6 +39,10 @@ const RELATION_TYPES = Object.freeze({
|
|
|
37
39
|
Object.freeze(['Capability', 'SpecificationStatement']),
|
|
38
40
|
]),
|
|
39
41
|
}),
|
|
42
|
+
usesObject: Object.freeze({
|
|
43
|
+
domain: Object.freeze(['Capability']),
|
|
44
|
+
range: Object.freeze(['BusinessObject']),
|
|
45
|
+
}),
|
|
40
46
|
acceptedBy: Object.freeze({
|
|
41
47
|
domain: Object.freeze(['SpecificationStatement']),
|
|
42
48
|
range: Object.freeze(['AcceptanceCriterion']),
|