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.
@@ -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 || job.report?.publication_status === 'needs_review') {
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
- function buildPackageFromFolder(specRoot, changeName, options) {
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
- const errors = semanticResult.diagnostics
255
- .filter(d => d.severity === 'error')
256
- .map(d => `${d.code}: ${d.message}`);
257
- throw new Error(`语义校验失败:\n${errors.join('\n')}`);
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
- // 4. 创建 archive snapshot(生成 archive-ontology.json)
273
- runtime.createArchiveSnapshot(specRoot, changeName, stagingDir, semanticResult.state);
274
- console.log('✓ 已生成 archive-ontology.json');
275
-
276
- // 5. 生成 manifest + canonical-facts + conversion-report + zip
277
- const projectIdentity = archivePackage.ensureProjectIdentity(specRoot);
278
- const result = archivePackage.materializeArchivePackage({
279
- projectRoot: specRoot,
280
- archiveDir: stagingDir,
281
- archivePath: stagingDir,
282
- changeName: changeName,
283
- packagePath: zipPath,
284
- reason: options.reason || '随时入库(文件夹上传)',
285
- method: 'kb-upload-folder',
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 { zipPath, manifest: result.manifest, stagingDir };
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 errors = semanticResult.diagnostics
417
- .filter(d => d.severity === 'error')
418
- .map(d => `${d.code}: ${d.message}`);
419
- throw new Error(`语义校验失败:\n${errors.join('\n')}`);
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
- // Canonical commit has been verified; a receipt does not imply projection completion.
508
- updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
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+需求项[::]\s*[【\[]?(STMT-[A-Z0-9-]+)[】\]]?\s*(.*?)\s*$/i);
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']),