kld-sdd 2.5.1 → 2.6.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.
Files changed (35) hide show
  1. package/README.md +118 -8
  2. package/kld-sdd-guide.html +1 -1
  3. package/lib/init.js +24 -5
  4. package/lib/tool-profiles.js +1 -1
  5. package/package.json +4 -2
  6. package/skywalk-sdd/context-client.cjs +160 -0
  7. package/skywalk-sdd/index.cjs +445 -36
  8. package/skywalk-sdd/ontology/archive-package.cjs +489 -0
  9. package/skywalk-sdd/ontology/artifact-observer.cjs +91 -0
  10. package/skywalk-sdd/ontology/artifact-parser.cjs +621 -0
  11. package/skywalk-sdd/ontology/change-lock.cjs +126 -0
  12. package/skywalk-sdd/ontology/cli.cjs +146 -0
  13. package/skywalk-sdd/ontology/effective-graph.cjs +158 -0
  14. package/skywalk-sdd/ontology/id.cjs +126 -0
  15. package/skywalk-sdd/ontology/identity-index.cjs +287 -0
  16. package/skywalk-sdd/ontology/normalizer.cjs +107 -0
  17. package/skywalk-sdd/ontology/runtime.cjs +466 -0
  18. package/skywalk-sdd/ontology/schema.cjs +139 -0
  19. package/skywalk-sdd/ontology/structural-identity.cjs +77 -0
  20. package/skywalk-sdd/ontology/traceability-validator.cjs +610 -0
  21. package/skywalk-sdd/ontology/working-artifacts.cjs +243 -0
  22. package/templates/openspec/design.md +18 -0
  23. package/templates/openspec/proposal.md +19 -6
  24. package/templates/openspec/spec.md +62 -8
  25. package/templates/openspec/tasks.md +28 -6
  26. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +19 -1
  27. package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -1
  28. package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -0
  29. package/templates/skills/kld-sdd/opsx-check/checklist.md +2 -0
  30. package/templates/skills/kld-sdd/opsx-design/SKILL.md +11 -0
  31. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +12 -0
  32. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  33. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +34 -0
  34. package/templates/skills/kld-sdd/opsx-spec/checklist.md +5 -0
  35. package/templates/skills/kld-sdd/opsx-task/SKILL.md +11 -0
@@ -520,31 +520,119 @@ function syncArchivedSpecs(projectRoot, archiveDir, changeName) {
520
520
  return copiedSpecs;
521
521
  }
522
522
 
523
+ function prepareArchivedSpecSync(projectRoot, archiveDir, changeName, token) {
524
+ const operations = discoverFullSpecFiles(archiveDir, changeName).map((spec) => {
525
+ const target = path.join(projectRoot, 'openspec', 'specs', spec.capability, 'spec.md');
526
+ ensureDir(path.dirname(target));
527
+ const temp = `${target}.${token}.archive-tmp`;
528
+ const backup = `${target}.${token}.archive-bak`;
529
+ fs.copyFileSync(spec.source, temp);
530
+ if (fs.existsSync(target)) fs.copyFileSync(target, backup);
531
+ return {
532
+ capability: spec.capability,
533
+ source: spec.source,
534
+ target,
535
+ temp,
536
+ backup,
537
+ had_original: fs.existsSync(target),
538
+ committed: false,
539
+ };
540
+ });
541
+ return {
542
+ operations,
543
+ copiedSpecs: operations.map((operation) => ({
544
+ capability: operation.capability,
545
+ source: toProjectRelative(projectRoot, operation.source),
546
+ target: toProjectRelative(projectRoot, operation.target),
547
+ })),
548
+ };
549
+ }
550
+
551
+ function commitArchivedSpecSync(transaction, failAt = '') {
552
+ for (const operation of transaction.operations) {
553
+ if (fs.existsSync(operation.target)) fs.rmSync(operation.target, { force: true });
554
+ fs.renameSync(operation.temp, operation.target);
555
+ operation.committed = true;
556
+ if (failAt === 'during-spec-sync') throw new Error('during-spec-sync');
557
+ }
558
+ }
559
+
560
+ function rollbackArchivedSpecSync(transaction) {
561
+ if (!transaction || !transaction.operations) return;
562
+ for (const operation of [...transaction.operations].reverse()) {
563
+ if (fs.existsSync(operation.backup)) {
564
+ fs.rmSync(operation.target, { force: true });
565
+ fs.renameSync(operation.backup, operation.target);
566
+ } else if (operation.committed) {
567
+ fs.rmSync(operation.target, { force: true });
568
+ }
569
+ fs.rmSync(operation.temp, { force: true });
570
+ fs.rmSync(operation.backup, { force: true });
571
+ }
572
+ }
573
+
574
+ function cleanupArchivedSpecSync(transaction) {
575
+ if (!transaction || !transaction.operations) return;
576
+ for (const operation of transaction.operations) {
577
+ fs.rmSync(operation.temp, { force: true });
578
+ fs.rmSync(operation.backup, { force: true });
579
+ }
580
+ }
581
+
582
+ function assertConfirmedArchive(projectRoot, changeName, archiveDir, options = {}) {
583
+ const { parseChangeArtifacts } = require('./ontology/artifact-parser.cjs');
584
+ const { normalizeFacts } = require('./ontology/normalizer.cjs');
585
+ const { validateArchiveCandidate } = require('./ontology/identity-index.cjs');
586
+ const facts = normalizeFacts(parseChangeArtifacts(projectRoot, changeName, {
587
+ changeDir: archiveDir,
588
+ persistStructuralIdentities: false,
589
+ }));
590
+ const validation = validateArchiveCandidate({
591
+ change: path.basename(archiveDir),
592
+ changeDir: archiveDir,
593
+ expectedArchiveDir: options.expectedArchiveDir,
594
+ }, facts);
595
+ if (!validation.eligible) {
596
+ const codes = validation.diagnostics.map((item) => item.code).join(', ');
597
+ throw new Error(`Archive 未通过 confirmed 事实校验: ${codes}`);
598
+ }
599
+ return { facts, snapshot: validation.snapshot, manifest: validation.manifest };
600
+ }
601
+
523
602
  function ensureArchiveManifest(projectRoot, changeName, archiveDir, options = {}) {
524
- const manifestPath = path.join(archiveDir, 'archive-manifest.json');
525
- const copiedSpecs = syncArchivedSpecs(projectRoot, archiveDir, changeName);
603
+ const copiedSpecs = Array.isArray(options.copiedSpecs)
604
+ ? options.copiedSpecs
605
+ : syncArchivedSpecs(projectRoot, archiveDir, changeName);
606
+ const committedArchiveDir = options.archivePath || archiveDir;
526
607
  // U4: 归档时拷 events jsonl 进 archive/evidence/events/,报告可从归档目录重建(源数据与产物不再分离)
527
608
  const eventsSrcDir = path.join(getDataDir(projectRoot), 'events', safeChangeName(changeName));
528
609
  let evidenceEventsPath = null;
529
610
  if (fs.existsSync(eventsSrcDir)) {
530
611
  const eventsDstDir = path.join(archiveDir, 'evidence', 'events');
531
612
  copyDirSync(eventsSrcDir, eventsDstDir);
532
- evidenceEventsPath = toProjectRelative(projectRoot, eventsDstDir);
613
+ evidenceEventsPath = toProjectRelative(
614
+ projectRoot,
615
+ path.join(committedArchiveDir, 'evidence', 'events'),
616
+ );
533
617
  }
534
- const manifest = {
535
- change: changeName,
536
- archived_at: options.archivedAt || nowISO(),
618
+ const { materializeArchivePackage } = require('./ontology/archive-package.cjs');
619
+ const produced = materializeArchivePackage({
620
+ projectRoot,
621
+ changeName,
622
+ archiveDir,
623
+ archivePath: committedArchiveDir,
624
+ packagePath: Object.prototype.hasOwnProperty.call(options, 'packagePath')
625
+ ? options.packagePath
626
+ : `${committedArchiveDir}.zip`,
627
+ archivedAt: options.archivedAt,
537
628
  reason: options.reason || '变更已完成实施',
538
629
  method: options.method || 'skywalk-full-spec-archive',
539
- source_path: `openspec/changes/${changeName}`,
540
- archive_path: toProjectRelative(projectRoot, archiveDir),
541
- copied_specs: copiedSpecs,
542
- evidence_events_path: evidenceEventsPath,
543
- };
544
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
630
+ sourcePath: `openspec/changes/${changeName}`,
631
+ copiedSpecs,
632
+ evidenceEventsPath,
633
+ });
545
634
  return {
546
- manifest,
547
- manifest_path: manifestPath,
635
+ ...produced,
548
636
  copied_specs: copiedSpecs,
549
637
  };
550
638
  }
@@ -574,6 +662,8 @@ function ensureArchiveSuccessArtifacts(projectRoot, changeName, details = {}, op
574
662
  };
575
663
  }
576
664
 
665
+ assertConfirmedArchive(normalizedRoot, changeName, archiveDir);
666
+
577
667
  // 显式 reportOutput 优先;否则默认落归档后 archive 目录的 reports/ 子目录
578
668
  const reportPath = options.reportOutput
579
669
  ? (path.isAbsolute(options.reportOutput) ? options.reportOutput : path.resolve(normalizedRoot, options.reportOutput))
@@ -587,18 +677,27 @@ function ensureArchiveSuccessArtifacts(projectRoot, changeName, details = {}, op
587
677
  const manifestInfo = ensureArchiveManifest(normalizedRoot, changeName, archiveDir, {
588
678
  reason: archiveReason,
589
679
  method: archiveMethod || 'skywalk-full-spec-archive',
590
- archivedAt: details.archive_result?.archived_at || nowISO(),
680
+ archivedAt: details.archive_result?.created_at || details.archive_result?.archived_at,
591
681
  });
592
682
 
593
683
  const archiveResult = {
594
684
  reason: archiveReason,
595
685
  method: manifestInfo.manifest.method,
686
+ schema_version: manifestInfo.manifest.schema_version,
687
+ producer: manifestInfo.manifest.producer,
688
+ project_id: manifestInfo.manifest.project_id,
689
+ archive_id: manifestInfo.manifest.archive_id,
690
+ change_id: manifestInfo.manifest.change_id,
691
+ created_at: manifestInfo.manifest.created_at,
596
692
  archive_path: manifestInfo.manifest.archive_path,
597
693
  report_path: toProjectRelative(normalizedRoot, reportPath),
598
694
  // report_html_path 为预期路径:由 cmdEnd 落盘阶段写入(try/catch 容错,失败时不阻塞 md 主产物)。
599
695
  // 此处无条件派生,html 实际落盘失败时该路径可能不存在;语义为"预期路径",消费者不应假定文件已存在。
600
696
  report_html_path: toProjectRelative(normalizedRoot, reportPath.replace(/\.md$/i, '.html')),
601
697
  manifest_path: toProjectRelative(normalizedRoot, manifestInfo.manifest_path),
698
+ canonical_facts_path: toProjectRelative(normalizedRoot, manifestInfo.canonical_facts_path),
699
+ conversion_report_path: toProjectRelative(normalizedRoot, manifestInfo.conversion_report_path),
700
+ package_path: toProjectRelative(normalizedRoot, manifestInfo.package_path),
602
701
  task_completion: taskCompletion,
603
702
  copied_specs: manifestInfo.copied_specs,
604
703
  };
@@ -632,30 +731,124 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
632
731
  ensureDir(archiveRoot);
633
732
  const archiveDate = options.date || today();
634
733
  const archiveDir = nextAvailableDir(archiveRoot, `${archiveDate}-${changeName}`);
734
+ const { acquireChangeLock, releaseChangeLock } = require('./ontology/change-lock.cjs');
735
+ const ontologyRuntime = require('./ontology/runtime.cjs');
736
+ const lock = acquireChangeLock(normalizedRoot, changeName, options.lockOptions);
737
+ const token = lock.token.replace(/-/g, '');
738
+ const stagingDir = path.join(archiveRoot, `.archive-staging-${token}`);
739
+ const stagingPackagePath = `${stagingDir}.zip`;
740
+ const finalPackagePath = `${archiveDir}.zip`;
741
+ const sourceBackup = path.join(path.dirname(sourceDir), `.archive-source-${token}`);
742
+ let specTransaction = null;
743
+ let sourceMoved = false;
744
+ let archiveCommitted = false;
745
+ let packageCommitted = false;
746
+ try {
747
+ const semanticResult = ontologyRuntime.reconcileChange(normalizedRoot, changeName, {
748
+ profile: options.profile || 'auto',
749
+ markPending: true,
750
+ lock,
751
+ });
752
+ if (!semanticResult.valid) {
753
+ const codes = [...new Set(semanticResult.diagnostics
754
+ .filter(item => item.severity === 'error')
755
+ .map(item => item.code))];
756
+ throw new Error(`本体语义校验失败: ${codes.join(', ')}`);
757
+ }
635
758
 
636
- copyDirSync(sourceDir, archiveDir);
637
- // P2-2: 根据 task_completion 调整归档原因,has_incomplete 时不写"已完成实施"
638
- const tcForReason = scanTaskCompletionForArchiveDir(normalizedRoot, changeName, archiveDir);
639
- let archiveReason = options.reason || '变更已完成实施';
640
- if (tcForReason?.has_incomplete) {
641
- archiveReason = `部分完成(${tcForReason.incomplete} 项验收未勾选)`;
642
- }
643
- const manifestInfo = ensureArchiveManifest(normalizedRoot, changeName, archiveDir, {
644
- reason: archiveReason,
645
- method: 'skywalk-full-spec-archive',
646
- });
647
- const manifest = manifestInfo.manifest;
759
+ fs.rmSync(stagingDir, { recursive: true, force: true });
760
+ fs.rmSync(stagingPackagePath, { force: true });
761
+ copyDirSync(sourceDir, stagingDir);
762
+ if (options.failAt === 'after-copy') throw new Error('after-copy');
763
+
764
+ ontologyRuntime.createArchiveSnapshot(
765
+ normalizedRoot,
766
+ changeName,
767
+ stagingDir,
768
+ semanticResult.state,
769
+ );
770
+ if (options.failAt === 'after-snapshot') throw new Error('after-snapshot');
771
+
772
+ const tcForReason = scanTaskCompletionForArchiveDir(normalizedRoot, changeName, stagingDir);
773
+ let archiveReason = options.reason || '变更已完成实施';
774
+ if (tcForReason?.has_incomplete) {
775
+ archiveReason = `部分完成(${tcForReason.incomplete} 项验收未勾选)`;
776
+ }
777
+ specTransaction = prepareArchivedSpecSync(
778
+ normalizedRoot,
779
+ stagingDir,
780
+ changeName,
781
+ token,
782
+ );
783
+ const manifestInfo = ensureArchiveManifest(normalizedRoot, changeName, stagingDir, {
784
+ reason: archiveReason,
785
+ method: 'skywalk-full-spec-archive',
786
+ archivePath: archiveDir,
787
+ packagePath: stagingPackagePath,
788
+ copiedSpecs: specTransaction.copiedSpecs,
789
+ });
790
+ const confirmed = assertConfirmedArchive(normalizedRoot, changeName, stagingDir, {
791
+ expectedArchiveDir: archiveDir,
792
+ });
793
+ if (confirmed.snapshot.source_revision !== semanticResult.revision) {
794
+ throw new Error('confirmed snapshot 的 source_revision 与工作态 revision 不一致');
795
+ }
796
+ if (options.failAt === 'after-manifest') throw new Error('after-manifest');
797
+ if (typeof options._testBeforeFinalize === 'function') {
798
+ options._testBeforeFinalize({ sourceDir, stagingDir, archiveDir });
799
+ }
648
800
 
649
- if (!options.keepActive) {
650
- fs.rmSync(sourceDir, { recursive: true, force: true });
651
- }
801
+ const activeFacts = require('./ontology/normalizer.cjs').normalizeFacts(
802
+ require('./ontology/artifact-parser.cjs').parseChangeArtifacts(
803
+ normalizedRoot,
804
+ changeName,
805
+ { changeDir: sourceDir, persistStructuralIdentities: false },
806
+ ),
807
+ );
808
+ if (activeFacts.facts_hash !== semanticResult.state.facts_hash) {
809
+ throw new Error('活动 Change 在归档事务期间发生变化');
810
+ }
652
811
 
653
- return {
654
- ...manifest,
655
- project_root: normalizedRoot,
656
- archive_path: archiveDir,
657
- active_change_exists: fs.existsSync(sourceDir),
658
- };
812
+ if (!options.keepActive) {
813
+ fs.renameSync(sourceDir, sourceBackup);
814
+ sourceMoved = true;
815
+ }
816
+ if (options.failAt === 'after-source-move') throw new Error('after-source-move');
817
+ fs.renameSync(stagingDir, archiveDir);
818
+ archiveCommitted = true;
819
+ commitArchivedSpecSync(specTransaction, options.failAt);
820
+ fs.rmSync(finalPackagePath, { force: true });
821
+ fs.renameSync(stagingPackagePath, finalPackagePath);
822
+ packageCommitted = true;
823
+ if (sourceMoved) fs.rmSync(sourceBackup, { recursive: true, force: true });
824
+ cleanupArchivedSpecSync(specTransaction);
825
+
826
+ return {
827
+ ...manifestInfo.manifest,
828
+ project_root: normalizedRoot,
829
+ archive_path: archiveDir,
830
+ manifest_path: path.join(archiveDir, 'archive-manifest.json'),
831
+ canonical_facts_path: path.join(archiveDir, 'canonical-facts.json'),
832
+ conversion_report_path: path.join(archiveDir, 'conversion-report.json'),
833
+ package_path: finalPackagePath,
834
+ active_change_exists: fs.existsSync(sourceDir),
835
+ };
836
+ } catch (error) {
837
+ rollbackArchivedSpecSync(specTransaction);
838
+ if (archiveCommitted) fs.rmSync(archiveDir, { recursive: true, force: true });
839
+ if (packageCommitted) fs.rmSync(finalPackagePath, { force: true });
840
+ fs.rmSync(stagingDir, { recursive: true, force: true });
841
+ fs.rmSync(stagingPackagePath, { force: true });
842
+ if (sourceMoved && fs.existsSync(sourceBackup) && !fs.existsSync(sourceDir)) {
843
+ fs.renameSync(sourceBackup, sourceDir);
844
+ }
845
+ const wrapped = new Error(`归档事务失败,活动 Change 保持不变: ${error.message}`);
846
+ wrapped.code = error.code || 'SEM_ARCHIVE_TRANSACTION_FAILED';
847
+ wrapped.cause = error;
848
+ throw wrapped;
849
+ } finally {
850
+ releaseChangeLock(lock);
851
+ }
659
852
  }
660
853
 
661
854
  /** 追加一行 JSONL 到事件文件(写入失败时抛出异常) */
@@ -3340,6 +3533,37 @@ function cmdStart(args) {
3340
3533
  console.log(JSON.stringify(output, null, 2));
3341
3534
  }
3342
3535
 
3536
+ const ONTOLOGY_AUTHORING_STAGES = new Set(['propose', 'spec', 'design', 'task', 'check']);
3537
+
3538
+ function syncWorkingOntologyForStage(projectRoot, changeName, command, result) {
3539
+ if (!ONTOLOGY_AUTHORING_STAGES.has(command) || result === 'failure') return null;
3540
+ if (!changeName || changeName === 'general') return null;
3541
+ const changeDir = getChangeDir(projectRoot, changeName);
3542
+ if (!fs.existsSync(changeDir)) return null;
3543
+ const reconciled = require('./ontology/runtime.cjs').reconcileChange(
3544
+ projectRoot,
3545
+ changeName,
3546
+ {
3547
+ profile: 'auto',
3548
+ markPending: command === 'check',
3549
+ },
3550
+ );
3551
+ return {
3552
+ schema_version: reconciled.state.artifact_schema_version,
3553
+ index_schema_version: reconciled.state.artifact_index_schema_version,
3554
+ revision: reconciled.revision,
3555
+ review_status: reconciled.state.review_status,
3556
+ valid: reconciled.valid,
3557
+ changed: Boolean(reconciled.changed),
3558
+ working_ontology_path: toProjectRelative(projectRoot, reconciled.paths.working),
3559
+ artifact_index_path: toProjectRelative(projectRoot, reconciled.paths.artifactIndex),
3560
+ artifact_json_paths: reconciled.paths.artifactFacts.map((filePath) => (
3561
+ toProjectRelative(projectRoot, filePath)
3562
+ )),
3563
+ diagnostics: reconciled.diagnostics,
3564
+ };
3565
+ }
3566
+
3343
3567
  /**
3344
3568
  * log end: 记录阶段结束
3345
3569
  */
@@ -3422,6 +3646,30 @@ function cmdEnd(args, options = {}) {
3422
3646
  fail(`details JSON 解析失败: ${err.message}`);
3423
3647
  }
3424
3648
 
3649
+ let semanticState = null;
3650
+ try {
3651
+ semanticState = syncWorkingOntologyForStage(projectRoot, change, command, result);
3652
+ if (semanticState) {
3653
+ details = {
3654
+ ...details,
3655
+ semantic_state: semanticState,
3656
+ };
3657
+ }
3658
+ } catch (error) {
3659
+ if (ONTOLOGY_AUTHORING_STAGES.has(command) && result !== 'failure') {
3660
+ result = 'partial';
3661
+ summary = `${summary}(工作态本体 JSON 同步失败:${error.message})`.trim();
3662
+ semanticState = {
3663
+ status: 'failed',
3664
+ error: error.message,
3665
+ };
3666
+ details = {
3667
+ ...details,
3668
+ semantic_state: semanticState,
3669
+ };
3670
+ }
3671
+ }
3672
+
3425
3673
  const durationMs = startEvent
3426
3674
  ? new Date(timestamp).getTime() - new Date(startEvent.timestamp).getTime()
3427
3675
  : null;
@@ -3565,6 +3813,7 @@ function cmdEnd(args, options = {}) {
3565
3813
  duration_ms: durationMs,
3566
3814
  recorded_at: timestamp,
3567
3815
  report_output: resolvedReportOutput || undefined,
3816
+ semantic_state: semanticState || undefined,
3568
3817
  message: `SDD ${event.command} 阶段结束(${result},耗时 ${durationMs ? (durationMs / 1000).toFixed(1) + 's' : '未知'})`,
3569
3818
  };
3570
3819
  if (!options.silent) {
@@ -4133,6 +4382,106 @@ function cmdTasksStatus(args) {
4133
4382
  }
4134
4383
  }
4135
4384
 
4385
+ function semanticCommandContext(args) {
4386
+ const projectRoot = normalizeProjectRoot(args.project || args['project-root'] || process.cwd());
4387
+ const changeName = args.change || args['change-name'];
4388
+ if (!changeName) {
4389
+ throw new Error('缺少 --change 参数');
4390
+ }
4391
+ return {
4392
+ projectRoot,
4393
+ changeName,
4394
+ profile: args.profile || 'auto',
4395
+ };
4396
+ }
4397
+
4398
+ function cmdSemanticIdentity(args) {
4399
+ const identity = require('./ontology/id.cjs').allocateIdentity(args);
4400
+ console.log(JSON.stringify(identity, null, 2));
4401
+ return identity;
4402
+ }
4403
+
4404
+ function semanticResultPayload(result) {
4405
+ return {
4406
+ change: result.state.change,
4407
+ profile: result.profile,
4408
+ valid: result.valid,
4409
+ review_status: result.state.review_status,
4410
+ revision: result.revision,
4411
+ changed: Boolean(result.changed),
4412
+ counts: result.counts,
4413
+ paths: result.paths,
4414
+ diagnostics: result.diagnostics,
4415
+ };
4416
+ }
4417
+
4418
+ function cmdSemanticScan(args) {
4419
+ const context = semanticCommandContext(args);
4420
+ const result = require('./ontology/runtime.cjs').scanChange(
4421
+ context.projectRoot,
4422
+ context.changeName,
4423
+ { profile: context.profile },
4424
+ );
4425
+ console.log(JSON.stringify(semanticResultPayload({ ...result, changed: false, paths: undefined }), null, 2));
4426
+ if (!result.valid) process.exitCode = 1;
4427
+ return result;
4428
+ }
4429
+
4430
+ function cmdSemanticReconcile(args, options = {}) {
4431
+ const context = semanticCommandContext(args);
4432
+ const result = require('./ontology/runtime.cjs').reconcileChange(
4433
+ context.projectRoot,
4434
+ context.changeName,
4435
+ { profile: context.profile, markPending: Boolean(options.markPending) },
4436
+ );
4437
+ console.log(JSON.stringify(semanticResultPayload(result), null, 2));
4438
+ if (!result.valid) process.exitCode = 1;
4439
+ return result;
4440
+ }
4441
+
4442
+ function cmdSemanticCheck(args) {
4443
+ return cmdSemanticReconcile(args, { markPending: true });
4444
+ }
4445
+
4446
+ function cmdSemanticStatus(args) {
4447
+ const context = semanticCommandContext(args);
4448
+ const state = require('./ontology/runtime.cjs').readWorkingState(context.projectRoot, context.changeName);
4449
+ if (!state) throw new Error(`不存在工作态本体实例: ${context.changeName}`);
4450
+ console.log(JSON.stringify(state, null, 2));
4451
+ return state;
4452
+ }
4453
+
4454
+ function cmdSemanticObserve(args) {
4455
+ const context = semanticCommandContext(args);
4456
+ const observer = require('./ontology/artifact-observer.cjs').observeChangeArtifacts(
4457
+ context.projectRoot,
4458
+ context.changeName,
4459
+ {
4460
+ profile: context.profile,
4461
+ pollIntervalMs: Number(args.interval || 1500),
4462
+ onReconciled(result) {
4463
+ console.log(JSON.stringify(semanticResultPayload(result)));
4464
+ },
4465
+ onError(error) {
4466
+ console.error(`[ontology-observer] ${error.message}`);
4467
+ },
4468
+ },
4469
+ );
4470
+ console.log(JSON.stringify({
4471
+ change: context.changeName,
4472
+ mode: observer.mode,
4473
+ change_dir: observer.changeDir,
4474
+ boundary: 'observe-and-reconcile-only',
4475
+ }));
4476
+ const close = () => {
4477
+ observer.close();
4478
+ process.exit(0);
4479
+ };
4480
+ process.once('SIGINT', close);
4481
+ process.once('SIGTERM', close);
4482
+ return observer;
4483
+ }
4484
+
4136
4485
  /**
4137
4486
  * log archive-docs: 真实归档 Simple/Full spec,结束 archive 阶段并生成最终报告
4138
4487
  */
@@ -4149,6 +4498,7 @@ function cmdArchiveDocs(args) {
4149
4498
  reason: args.reason || '',
4150
4499
  date: args.date,
4151
4500
  keepActive: Boolean(args['keep-active']),
4501
+ profile: args.profile || 'auto',
4152
4502
  });
4153
4503
  } else {
4154
4504
  const repaired = ensureArchiveSuccessArtifacts(projectRoot, changeName, {
@@ -4195,6 +4545,28 @@ function cmdArchiveDocs(args) {
4195
4545
  }, { silent: true });
4196
4546
  }
4197
4547
 
4548
+ const archiveDir = path.resolve(result.archive_path);
4549
+ const finalPackage = ensureArchiveManifest(projectRoot, changeName, archiveDir, {
4550
+ reason: result.reason || args.reason || '',
4551
+ method: result.method || 'skywalk-full-spec-archive',
4552
+ archivePath: archiveDir,
4553
+ archivedAt: result.created_at || result.archived_at,
4554
+ copiedSpecs: result.copied_specs,
4555
+ });
4556
+ result = {
4557
+ ...result,
4558
+ schema_version: finalPackage.manifest.schema_version,
4559
+ producer: finalPackage.manifest.producer,
4560
+ project_id: finalPackage.manifest.project_id,
4561
+ archive_id: finalPackage.manifest.archive_id,
4562
+ change_id: finalPackage.manifest.change_id,
4563
+ created_at: finalPackage.manifest.created_at,
4564
+ manifest_path: finalPackage.manifest_path,
4565
+ canonical_facts_path: finalPackage.canonical_facts_path,
4566
+ conversion_report_path: finalPackage.conversion_report_path,
4567
+ package_path: finalPackage.package_path,
4568
+ };
4569
+
4198
4570
  console.log(JSON.stringify({
4199
4571
  ...result,
4200
4572
  report_output: stageEnd ? stageEnd.report_output : reportOutput,
@@ -4257,6 +4629,24 @@ function main() {
4257
4629
  case 'archive-docs':
4258
4630
  cmdArchiveDocs(flags);
4259
4631
  break;
4632
+ case 'semantic-scan':
4633
+ cmdSemanticScan(flags);
4634
+ break;
4635
+ case 'semantic-identity':
4636
+ cmdSemanticIdentity(flags);
4637
+ break;
4638
+ case 'semantic-reconcile':
4639
+ cmdSemanticReconcile(flags);
4640
+ break;
4641
+ case 'semantic-check':
4642
+ cmdSemanticCheck(flags);
4643
+ break;
4644
+ case 'semantic-status':
4645
+ cmdSemanticStatus(flags);
4646
+ break;
4647
+ case 'semantic-observe':
4648
+ cmdSemanticObserve(flags);
4649
+ break;
4260
4650
  case 'check-task':
4261
4651
  cmdCheckTask(flags);
4262
4652
  break;
@@ -4278,6 +4668,13 @@ SDD Telemetry CLI - 流程度量采集工具
4278
4668
  node skywalk-sdd/log.cjs tasks-status --project=<path> --change=<name> [--require-complete]
4279
4669
  node skywalk-sdd/log.cjs check-task --project=<path> --change=<name> --task-id=<id>
4280
4670
  node skywalk-sdd/log.cjs archive-docs --project=<path> --change=<name> [--reason=<text>] [--event-id=<id>] [--report-output=<file>]
4671
+ node skywalk-sdd/log.cjs semantic-identity --delta-state=added
4672
+ node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
4673
+ node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
4674
+ node skywalk-sdd/log.cjs semantic-reconcile --project=<path> --change=<name> [--profile=auto|simple|full|strict]
4675
+ node skywalk-sdd/log.cjs semantic-check --project=<path> --change=<name> [--profile=...]
4676
+ node skywalk-sdd/log.cjs semantic-status --project=<path> --change=<name>
4677
+ node skywalk-sdd/log.cjs semantic-observe --project=<path> --change=<name> [--interval=1500]
4281
4678
 
4282
4679
  子命令:
4283
4680
  start 记录 SDD 阶段开始,返回 event_id
@@ -4289,6 +4686,12 @@ SDD Telemetry CLI - 流程度量采集工具
4289
4686
  tasks-status 扫描 Full/Simple 模式 tasks.md 勾选状态
4290
4687
  check-task 扫描变更目录 tasks.md 并勾选指定 task_id
4291
4688
  archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
4689
+ semantic-identity 生成或复用实体/版本 UUID;无需网络和中央 ID 服务
4690
+ semantic-scan 只读解析并校验当前 Change
4691
+ semantic-reconcile 全量对账文件与本地工作态本体实例
4692
+ semantic-check 全量对账并在通过时标记 pending
4693
+ semantic-status 读取当前工作态本体实例
4694
+ semantic-observe 观察文件变化并同步;不提供 Hook 式控制,Check/Archive 仍会全量对账
4292
4695
 
4293
4696
  示例:
4294
4697
  node skywalk-sdd/log.cjs start --command=propose --project=/my/project --change=user-auth --agent=cursor
@@ -4327,6 +4730,12 @@ module.exports = {
4327
4730
  cmdDoctor,
4328
4731
  cmdTasksStatus,
4329
4732
  cmdArchiveDocs,
4733
+ cmdSemanticIdentity,
4734
+ cmdSemanticScan,
4735
+ cmdSemanticReconcile,
4736
+ cmdSemanticCheck,
4737
+ cmdSemanticStatus,
4738
+ cmdSemanticObserve,
4330
4739
  computeChangeMetrics,
4331
4740
  computeOverviewMetrics,
4332
4741
  computeCapabilityMetrics,