kld-sdd 2.5.0 → 2.5.2
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 +95 -8
- package/kld-sdd-guide.html +1109 -0
- package/lib/command-bridge.js +156 -0
- package/lib/deploy-codebuddy-hooks.js +99 -0
- package/lib/hook-gate-core.js +333 -0
- package/lib/init.js +137 -82
- package/lib/settings-merge.js +85 -0
- package/lib/skills-bundle.js +142 -5
- package/lib/tool-profiles.js +270 -0
- package/package.json +4 -2
- package/skywalk-sdd/index.cjs +329 -24
- package/skywalk-sdd/ontology/artifact-observer.cjs +91 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +621 -0
- package/skywalk-sdd/ontology/change-lock.cjs +126 -0
- package/skywalk-sdd/ontology/cli.cjs +146 -0
- package/skywalk-sdd/ontology/effective-graph.cjs +158 -0
- package/skywalk-sdd/ontology/id.cjs +126 -0
- package/skywalk-sdd/ontology/identity-index.cjs +262 -0
- package/skywalk-sdd/ontology/normalizer.cjs +107 -0
- package/skywalk-sdd/ontology/runtime.cjs +341 -0
- package/skywalk-sdd/ontology/schema.cjs +139 -0
- package/skywalk-sdd/ontology/structural-identity.cjs +77 -0
- package/skywalk-sdd/ontology/traceability-validator.cjs +610 -0
- package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
- package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
- package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
- package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
- package/templates/hooks/codebuddy/settings.json +72 -0
- package/templates/openspec/design.md +18 -0
- package/templates/openspec/proposal.md +19 -6
- package/templates/openspec/spec.md +62 -8
- package/templates/openspec/tasks.md +28 -6
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +5 -5
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +17 -1
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -1
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +11 -1
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
- package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
- package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +12 -1
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -1
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
package/skywalk-sdd/index.cjs
CHANGED
|
@@ -520,16 +520,101 @@ 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
603
|
const manifestPath = path.join(archiveDir, 'archive-manifest.json');
|
|
525
|
-
const copiedSpecs =
|
|
604
|
+
const copiedSpecs = Array.isArray(options.copiedSpecs)
|
|
605
|
+
? options.copiedSpecs
|
|
606
|
+
: syncArchivedSpecs(projectRoot, archiveDir, changeName);
|
|
607
|
+
const committedArchiveDir = options.archivePath || archiveDir;
|
|
526
608
|
// U4: 归档时拷 events jsonl 进 archive/evidence/events/,报告可从归档目录重建(源数据与产物不再分离)
|
|
527
609
|
const eventsSrcDir = path.join(getDataDir(projectRoot), 'events', safeChangeName(changeName));
|
|
528
610
|
let evidenceEventsPath = null;
|
|
529
611
|
if (fs.existsSync(eventsSrcDir)) {
|
|
530
612
|
const eventsDstDir = path.join(archiveDir, 'evidence', 'events');
|
|
531
613
|
copyDirSync(eventsSrcDir, eventsDstDir);
|
|
532
|
-
evidenceEventsPath = toProjectRelative(
|
|
614
|
+
evidenceEventsPath = toProjectRelative(
|
|
615
|
+
projectRoot,
|
|
616
|
+
path.join(committedArchiveDir, 'evidence', 'events'),
|
|
617
|
+
);
|
|
533
618
|
}
|
|
534
619
|
const manifest = {
|
|
535
620
|
change: changeName,
|
|
@@ -537,7 +622,7 @@ function ensureArchiveManifest(projectRoot, changeName, archiveDir, options = {}
|
|
|
537
622
|
reason: options.reason || '变更已完成实施',
|
|
538
623
|
method: options.method || 'skywalk-full-spec-archive',
|
|
539
624
|
source_path: `openspec/changes/${changeName}`,
|
|
540
|
-
archive_path: toProjectRelative(projectRoot,
|
|
625
|
+
archive_path: toProjectRelative(projectRoot, committedArchiveDir),
|
|
541
626
|
copied_specs: copiedSpecs,
|
|
542
627
|
evidence_events_path: evidenceEventsPath,
|
|
543
628
|
};
|
|
@@ -574,6 +659,8 @@ function ensureArchiveSuccessArtifacts(projectRoot, changeName, details = {}, op
|
|
|
574
659
|
};
|
|
575
660
|
}
|
|
576
661
|
|
|
662
|
+
assertConfirmedArchive(normalizedRoot, changeName, archiveDir);
|
|
663
|
+
|
|
577
664
|
// 显式 reportOutput 优先;否则默认落归档后 archive 目录的 reports/ 子目录
|
|
578
665
|
const reportPath = options.reportOutput
|
|
579
666
|
? (path.isAbsolute(options.reportOutput) ? options.reportOutput : path.resolve(normalizedRoot, options.reportOutput))
|
|
@@ -632,30 +719,110 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
|
|
|
632
719
|
ensureDir(archiveRoot);
|
|
633
720
|
const archiveDate = options.date || today();
|
|
634
721
|
const archiveDir = nextAvailableDir(archiveRoot, `${archiveDate}-${changeName}`);
|
|
722
|
+
const { acquireChangeLock, releaseChangeLock } = require('./ontology/change-lock.cjs');
|
|
723
|
+
const ontologyRuntime = require('./ontology/runtime.cjs');
|
|
724
|
+
const lock = acquireChangeLock(normalizedRoot, changeName, options.lockOptions);
|
|
725
|
+
const token = lock.token.replace(/-/g, '');
|
|
726
|
+
const stagingDir = path.join(archiveRoot, `.archive-staging-${token}`);
|
|
727
|
+
const sourceBackup = path.join(path.dirname(sourceDir), `.archive-source-${token}`);
|
|
728
|
+
let specTransaction = null;
|
|
729
|
+
let sourceMoved = false;
|
|
730
|
+
let archiveCommitted = false;
|
|
731
|
+
try {
|
|
732
|
+
const semanticResult = ontologyRuntime.reconcileChange(normalizedRoot, changeName, {
|
|
733
|
+
profile: options.profile || 'auto',
|
|
734
|
+
markPending: true,
|
|
735
|
+
lock,
|
|
736
|
+
});
|
|
737
|
+
if (!semanticResult.valid) {
|
|
738
|
+
const codes = [...new Set(semanticResult.diagnostics
|
|
739
|
+
.filter(item => item.severity === 'error')
|
|
740
|
+
.map(item => item.code))];
|
|
741
|
+
throw new Error(`本体语义校验失败: ${codes.join(', ')}`);
|
|
742
|
+
}
|
|
635
743
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
744
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
745
|
+
copyDirSync(sourceDir, stagingDir);
|
|
746
|
+
if (options.failAt === 'after-copy') throw new Error('after-copy');
|
|
747
|
+
|
|
748
|
+
ontologyRuntime.createArchiveSnapshot(
|
|
749
|
+
normalizedRoot,
|
|
750
|
+
changeName,
|
|
751
|
+
stagingDir,
|
|
752
|
+
semanticResult.state,
|
|
753
|
+
);
|
|
754
|
+
if (options.failAt === 'after-snapshot') throw new Error('after-snapshot');
|
|
755
|
+
|
|
756
|
+
const tcForReason = scanTaskCompletionForArchiveDir(normalizedRoot, changeName, stagingDir);
|
|
757
|
+
let archiveReason = options.reason || '变更已完成实施';
|
|
758
|
+
if (tcForReason?.has_incomplete) {
|
|
759
|
+
archiveReason = `部分完成(${tcForReason.incomplete} 项验收未勾选)`;
|
|
760
|
+
}
|
|
761
|
+
specTransaction = prepareArchivedSpecSync(
|
|
762
|
+
normalizedRoot,
|
|
763
|
+
stagingDir,
|
|
764
|
+
changeName,
|
|
765
|
+
token,
|
|
766
|
+
);
|
|
767
|
+
const manifestInfo = ensureArchiveManifest(normalizedRoot, changeName, stagingDir, {
|
|
768
|
+
reason: archiveReason,
|
|
769
|
+
method: 'skywalk-full-spec-archive',
|
|
770
|
+
archivePath: archiveDir,
|
|
771
|
+
copiedSpecs: specTransaction.copiedSpecs,
|
|
772
|
+
});
|
|
773
|
+
const confirmed = assertConfirmedArchive(normalizedRoot, changeName, stagingDir, {
|
|
774
|
+
expectedArchiveDir: archiveDir,
|
|
775
|
+
});
|
|
776
|
+
if (confirmed.snapshot.source_revision !== semanticResult.revision) {
|
|
777
|
+
throw new Error('confirmed snapshot 的 source_revision 与工作态 revision 不一致');
|
|
778
|
+
}
|
|
779
|
+
if (options.failAt === 'after-manifest') throw new Error('after-manifest');
|
|
780
|
+
if (typeof options._testBeforeFinalize === 'function') {
|
|
781
|
+
options._testBeforeFinalize({ sourceDir, stagingDir, archiveDir });
|
|
782
|
+
}
|
|
648
783
|
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
784
|
+
const activeFacts = require('./ontology/normalizer.cjs').normalizeFacts(
|
|
785
|
+
require('./ontology/artifact-parser.cjs').parseChangeArtifacts(
|
|
786
|
+
normalizedRoot,
|
|
787
|
+
changeName,
|
|
788
|
+
{ changeDir: sourceDir, persistStructuralIdentities: false },
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
if (activeFacts.facts_hash !== semanticResult.state.facts_hash) {
|
|
792
|
+
throw new Error('活动 Change 在归档事务期间发生变化');
|
|
793
|
+
}
|
|
652
794
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
795
|
+
if (!options.keepActive) {
|
|
796
|
+
fs.renameSync(sourceDir, sourceBackup);
|
|
797
|
+
sourceMoved = true;
|
|
798
|
+
}
|
|
799
|
+
if (options.failAt === 'after-source-move') throw new Error('after-source-move');
|
|
800
|
+
fs.renameSync(stagingDir, archiveDir);
|
|
801
|
+
archiveCommitted = true;
|
|
802
|
+
commitArchivedSpecSync(specTransaction, options.failAt);
|
|
803
|
+
if (sourceMoved) fs.rmSync(sourceBackup, { recursive: true, force: true });
|
|
804
|
+
cleanupArchivedSpecSync(specTransaction);
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
...manifestInfo.manifest,
|
|
808
|
+
project_root: normalizedRoot,
|
|
809
|
+
archive_path: archiveDir,
|
|
810
|
+
active_change_exists: fs.existsSync(sourceDir),
|
|
811
|
+
};
|
|
812
|
+
} catch (error) {
|
|
813
|
+
rollbackArchivedSpecSync(specTransaction);
|
|
814
|
+
if (archiveCommitted) fs.rmSync(archiveDir, { recursive: true, force: true });
|
|
815
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
816
|
+
if (sourceMoved && fs.existsSync(sourceBackup) && !fs.existsSync(sourceDir)) {
|
|
817
|
+
fs.renameSync(sourceBackup, sourceDir);
|
|
818
|
+
}
|
|
819
|
+
const wrapped = new Error(`归档事务失败,活动 Change 保持不变: ${error.message}`);
|
|
820
|
+
wrapped.code = error.code || 'SEM_ARCHIVE_TRANSACTION_FAILED';
|
|
821
|
+
wrapped.cause = error;
|
|
822
|
+
throw wrapped;
|
|
823
|
+
} finally {
|
|
824
|
+
releaseChangeLock(lock);
|
|
825
|
+
}
|
|
659
826
|
}
|
|
660
827
|
|
|
661
828
|
/** 追加一行 JSONL 到事件文件(写入失败时抛出异常) */
|
|
@@ -4133,6 +4300,106 @@ function cmdTasksStatus(args) {
|
|
|
4133
4300
|
}
|
|
4134
4301
|
}
|
|
4135
4302
|
|
|
4303
|
+
function semanticCommandContext(args) {
|
|
4304
|
+
const projectRoot = normalizeProjectRoot(args.project || args['project-root'] || process.cwd());
|
|
4305
|
+
const changeName = args.change || args['change-name'];
|
|
4306
|
+
if (!changeName) {
|
|
4307
|
+
throw new Error('缺少 --change 参数');
|
|
4308
|
+
}
|
|
4309
|
+
return {
|
|
4310
|
+
projectRoot,
|
|
4311
|
+
changeName,
|
|
4312
|
+
profile: args.profile || 'auto',
|
|
4313
|
+
};
|
|
4314
|
+
}
|
|
4315
|
+
|
|
4316
|
+
function cmdSemanticIdentity(args) {
|
|
4317
|
+
const identity = require('./ontology/id.cjs').allocateIdentity(args);
|
|
4318
|
+
console.log(JSON.stringify(identity, null, 2));
|
|
4319
|
+
return identity;
|
|
4320
|
+
}
|
|
4321
|
+
|
|
4322
|
+
function semanticResultPayload(result) {
|
|
4323
|
+
return {
|
|
4324
|
+
change: result.state.change,
|
|
4325
|
+
profile: result.profile,
|
|
4326
|
+
valid: result.valid,
|
|
4327
|
+
review_status: result.state.review_status,
|
|
4328
|
+
revision: result.revision,
|
|
4329
|
+
changed: Boolean(result.changed),
|
|
4330
|
+
counts: result.counts,
|
|
4331
|
+
paths: result.paths,
|
|
4332
|
+
diagnostics: result.diagnostics,
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
function cmdSemanticScan(args) {
|
|
4337
|
+
const context = semanticCommandContext(args);
|
|
4338
|
+
const result = require('./ontology/runtime.cjs').scanChange(
|
|
4339
|
+
context.projectRoot,
|
|
4340
|
+
context.changeName,
|
|
4341
|
+
{ profile: context.profile },
|
|
4342
|
+
);
|
|
4343
|
+
console.log(JSON.stringify(semanticResultPayload({ ...result, changed: false, paths: undefined }), null, 2));
|
|
4344
|
+
if (!result.valid) process.exitCode = 1;
|
|
4345
|
+
return result;
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
function cmdSemanticReconcile(args, options = {}) {
|
|
4349
|
+
const context = semanticCommandContext(args);
|
|
4350
|
+
const result = require('./ontology/runtime.cjs').reconcileChange(
|
|
4351
|
+
context.projectRoot,
|
|
4352
|
+
context.changeName,
|
|
4353
|
+
{ profile: context.profile, markPending: Boolean(options.markPending) },
|
|
4354
|
+
);
|
|
4355
|
+
console.log(JSON.stringify(semanticResultPayload(result), null, 2));
|
|
4356
|
+
if (!result.valid) process.exitCode = 1;
|
|
4357
|
+
return result;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
function cmdSemanticCheck(args) {
|
|
4361
|
+
return cmdSemanticReconcile(args, { markPending: true });
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
function cmdSemanticStatus(args) {
|
|
4365
|
+
const context = semanticCommandContext(args);
|
|
4366
|
+
const state = require('./ontology/runtime.cjs').readWorkingState(context.projectRoot, context.changeName);
|
|
4367
|
+
if (!state) throw new Error(`不存在工作态本体实例: ${context.changeName}`);
|
|
4368
|
+
console.log(JSON.stringify(state, null, 2));
|
|
4369
|
+
return state;
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
function cmdSemanticObserve(args) {
|
|
4373
|
+
const context = semanticCommandContext(args);
|
|
4374
|
+
const observer = require('./ontology/artifact-observer.cjs').observeChangeArtifacts(
|
|
4375
|
+
context.projectRoot,
|
|
4376
|
+
context.changeName,
|
|
4377
|
+
{
|
|
4378
|
+
profile: context.profile,
|
|
4379
|
+
pollIntervalMs: Number(args.interval || 1500),
|
|
4380
|
+
onReconciled(result) {
|
|
4381
|
+
console.log(JSON.stringify(semanticResultPayload(result)));
|
|
4382
|
+
},
|
|
4383
|
+
onError(error) {
|
|
4384
|
+
console.error(`[ontology-observer] ${error.message}`);
|
|
4385
|
+
},
|
|
4386
|
+
},
|
|
4387
|
+
);
|
|
4388
|
+
console.log(JSON.stringify({
|
|
4389
|
+
change: context.changeName,
|
|
4390
|
+
mode: observer.mode,
|
|
4391
|
+
change_dir: observer.changeDir,
|
|
4392
|
+
boundary: 'observe-and-reconcile-only',
|
|
4393
|
+
}));
|
|
4394
|
+
const close = () => {
|
|
4395
|
+
observer.close();
|
|
4396
|
+
process.exit(0);
|
|
4397
|
+
};
|
|
4398
|
+
process.once('SIGINT', close);
|
|
4399
|
+
process.once('SIGTERM', close);
|
|
4400
|
+
return observer;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4136
4403
|
/**
|
|
4137
4404
|
* log archive-docs: 真实归档 Simple/Full spec,结束 archive 阶段并生成最终报告
|
|
4138
4405
|
*/
|
|
@@ -4149,6 +4416,7 @@ function cmdArchiveDocs(args) {
|
|
|
4149
4416
|
reason: args.reason || '',
|
|
4150
4417
|
date: args.date,
|
|
4151
4418
|
keepActive: Boolean(args['keep-active']),
|
|
4419
|
+
profile: args.profile || 'auto',
|
|
4152
4420
|
});
|
|
4153
4421
|
} else {
|
|
4154
4422
|
const repaired = ensureArchiveSuccessArtifacts(projectRoot, changeName, {
|
|
@@ -4257,6 +4525,24 @@ function main() {
|
|
|
4257
4525
|
case 'archive-docs':
|
|
4258
4526
|
cmdArchiveDocs(flags);
|
|
4259
4527
|
break;
|
|
4528
|
+
case 'semantic-scan':
|
|
4529
|
+
cmdSemanticScan(flags);
|
|
4530
|
+
break;
|
|
4531
|
+
case 'semantic-identity':
|
|
4532
|
+
cmdSemanticIdentity(flags);
|
|
4533
|
+
break;
|
|
4534
|
+
case 'semantic-reconcile':
|
|
4535
|
+
cmdSemanticReconcile(flags);
|
|
4536
|
+
break;
|
|
4537
|
+
case 'semantic-check':
|
|
4538
|
+
cmdSemanticCheck(flags);
|
|
4539
|
+
break;
|
|
4540
|
+
case 'semantic-status':
|
|
4541
|
+
cmdSemanticStatus(flags);
|
|
4542
|
+
break;
|
|
4543
|
+
case 'semantic-observe':
|
|
4544
|
+
cmdSemanticObserve(flags);
|
|
4545
|
+
break;
|
|
4260
4546
|
case 'check-task':
|
|
4261
4547
|
cmdCheckTask(flags);
|
|
4262
4548
|
break;
|
|
@@ -4278,6 +4564,13 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
4278
4564
|
node skywalk-sdd/log.cjs tasks-status --project=<path> --change=<name> [--require-complete]
|
|
4279
4565
|
node skywalk-sdd/log.cjs check-task --project=<path> --change=<name> --task-id=<id>
|
|
4280
4566
|
node skywalk-sdd/log.cjs archive-docs --project=<path> --change=<name> [--reason=<text>] [--event-id=<id>] [--report-output=<file>]
|
|
4567
|
+
node skywalk-sdd/log.cjs semantic-identity --delta-state=added
|
|
4568
|
+
node skywalk-sdd/log.cjs semantic-identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
|
|
4569
|
+
node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
|
|
4570
|
+
node skywalk-sdd/log.cjs semantic-reconcile --project=<path> --change=<name> [--profile=auto|simple|full|strict]
|
|
4571
|
+
node skywalk-sdd/log.cjs semantic-check --project=<path> --change=<name> [--profile=...]
|
|
4572
|
+
node skywalk-sdd/log.cjs semantic-status --project=<path> --change=<name>
|
|
4573
|
+
node skywalk-sdd/log.cjs semantic-observe --project=<path> --change=<name> [--interval=1500]
|
|
4281
4574
|
|
|
4282
4575
|
子命令:
|
|
4283
4576
|
start 记录 SDD 阶段开始,返回 event_id
|
|
@@ -4289,6 +4582,12 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
4289
4582
|
tasks-status 扫描 Full/Simple 模式 tasks.md 勾选状态
|
|
4290
4583
|
check-task 扫描变更目录 tasks.md 并勾选指定 task_id
|
|
4291
4584
|
archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
|
|
4585
|
+
semantic-identity 生成或复用实体/版本 UUID;无需网络和中央 ID 服务
|
|
4586
|
+
semantic-scan 只读解析并校验当前 Change
|
|
4587
|
+
semantic-reconcile 全量对账文件与本地工作态本体实例
|
|
4588
|
+
semantic-check 全量对账并在通过时标记 pending
|
|
4589
|
+
semantic-status 读取当前工作态本体实例
|
|
4590
|
+
semantic-observe 观察文件变化并同步;不提供 Hook 式控制,Check/Archive 仍会全量对账
|
|
4292
4591
|
|
|
4293
4592
|
示例:
|
|
4294
4593
|
node skywalk-sdd/log.cjs start --command=propose --project=/my/project --change=user-auth --agent=cursor
|
|
@@ -4327,6 +4626,12 @@ module.exports = {
|
|
|
4327
4626
|
cmdDoctor,
|
|
4328
4627
|
cmdTasksStatus,
|
|
4329
4628
|
cmdArchiveDocs,
|
|
4629
|
+
cmdSemanticIdentity,
|
|
4630
|
+
cmdSemanticScan,
|
|
4631
|
+
cmdSemanticReconcile,
|
|
4632
|
+
cmdSemanticCheck,
|
|
4633
|
+
cmdSemanticStatus,
|
|
4634
|
+
cmdSemanticObserve,
|
|
4330
4635
|
computeChangeMetrics,
|
|
4331
4636
|
computeOverviewMetrics,
|
|
4332
4637
|
computeCapabilityMetrics,
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { safeChangeName, scanArtifactFiles } = require('./artifact-parser.cjs');
|
|
7
|
+
const { reconcileChange } = require('./runtime.cjs');
|
|
8
|
+
|
|
9
|
+
function fingerprint(changeDir) {
|
|
10
|
+
const files = scanArtifactFiles(changeDir).map((file) => ({
|
|
11
|
+
path: file.relativePath,
|
|
12
|
+
content_hash: file.contentHash,
|
|
13
|
+
}));
|
|
14
|
+
return crypto.createHash('sha256').update(JSON.stringify(files)).digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function observeChangeArtifacts(projectRoot, changeName, options = {}) {
|
|
18
|
+
const root = path.resolve(projectRoot || process.cwd());
|
|
19
|
+
const safeName = safeChangeName(changeName);
|
|
20
|
+
const changeDir = path.join(root, 'openspec', 'changes', safeName);
|
|
21
|
+
if (!fs.existsSync(changeDir)) {
|
|
22
|
+
throw new Error(`变更目录不存在: ${changeDir}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const pollIntervalMs = Math.max(25, Number(options.pollIntervalMs || 1500));
|
|
26
|
+
const debounceMs = Math.max(0, Number(options.debounceMs || 120));
|
|
27
|
+
// 启动/重启后先把当前文件集合视为待对账,避免停机期间的变化被当作已处理。
|
|
28
|
+
let lastFingerprint = '';
|
|
29
|
+
let debounceTimer = null;
|
|
30
|
+
let closed = false;
|
|
31
|
+
let watcher = null;
|
|
32
|
+
|
|
33
|
+
function reconcileIfChanged(force = false) {
|
|
34
|
+
if (closed) return null;
|
|
35
|
+
const nextFingerprint = fingerprint(changeDir);
|
|
36
|
+
if (!force && nextFingerprint === lastFingerprint) return null;
|
|
37
|
+
const result = reconcileChange(root, safeName, { profile: options.profile || 'auto' });
|
|
38
|
+
lastFingerprint = nextFingerprint;
|
|
39
|
+
if (typeof options.onReconciled === 'function') options.onReconciled(result);
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function schedule() {
|
|
44
|
+
if (closed) return;
|
|
45
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
46
|
+
debounceTimer = setTimeout(() => {
|
|
47
|
+
debounceTimer = null;
|
|
48
|
+
try {
|
|
49
|
+
reconcileIfChanged();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
52
|
+
}
|
|
53
|
+
}, debounceMs);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
watcher = fs.watch(changeDir, { recursive: true }, schedule);
|
|
58
|
+
watcher.on('error', (error) => {
|
|
59
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
watcher = null;
|
|
63
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const poller = setInterval(() => {
|
|
67
|
+
try {
|
|
68
|
+
reconcileIfChanged();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (typeof options.onError === 'function') options.onError(error);
|
|
71
|
+
}
|
|
72
|
+
}, pollIntervalMs);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
mode: watcher ? 'fs-watch+poll-reconcile' : 'poll-reconcile',
|
|
76
|
+
changeDir,
|
|
77
|
+
reconcile: () => reconcileIfChanged(true),
|
|
78
|
+
close() {
|
|
79
|
+
if (closed) return;
|
|
80
|
+
closed = true;
|
|
81
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
82
|
+
clearInterval(poller);
|
|
83
|
+
if (watcher) watcher.close();
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
fingerprint,
|
|
90
|
+
observeChangeArtifacts,
|
|
91
|
+
};
|