kld-sdd 2.5.2 → 2.6.1

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 (29) hide show
  1. package/README.md +33 -10
  2. package/lib/init.js +11 -1
  3. package/package.json +3 -2
  4. package/skywalk-sdd/context-client.cjs +160 -0
  5. package/skywalk-sdd/index.cjs +118 -14
  6. package/skywalk-sdd/ontology/archive-package.cjs +489 -0
  7. package/skywalk-sdd/ontology/id.cjs +16 -19
  8. package/skywalk-sdd/ontology/identity-index.cjs +25 -0
  9. package/skywalk-sdd/ontology/runtime.cjs +179 -54
  10. package/skywalk-sdd/ontology/working-artifacts.cjs +243 -0
  11. package/templates/openspec/proposal.md +1 -1
  12. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +27 -4
  13. package/templates/skills/kld-sdd/opsx-apply/checklist.md +29 -1
  14. package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +54 -3
  15. package/templates/skills/kld-sdd/opsx-apply/reference.md +26 -0
  16. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -1
  17. package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -1
  18. package/templates/skills/kld-sdd/opsx-check/SKILL.md +21 -4
  19. package/templates/skills/kld-sdd/opsx-check/checklist.md +19 -1
  20. package/templates/skills/kld-sdd/opsx-design/SKILL.md +2 -0
  21. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +2 -0
  22. package/templates/skills/kld-sdd/opsx-propose/checklist.md +2 -0
  23. package/templates/skills/kld-sdd/opsx-propose/reference.md +6 -4
  24. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +23 -0
  25. package/templates/skills/kld-sdd/opsx-spec/checklist.md +5 -0
  26. package/templates/skills/kld-sdd/opsx-task/SKILL.md +43 -8
  27. package/templates/skills/kld-sdd/opsx-task/checklist.md +15 -0
  28. package/templates/skills/kld-sdd/opsx-task/reference.md +73 -2
  29. package/templates/skills/kld-sdd/opsx-test/SKILL.md +17 -0
package/README.md CHANGED
@@ -32,8 +32,8 @@ Entity sourcedFrom SEC
32
32
  每个本体实体使用双层身份:
33
33
 
34
34
  - `anchor`:`STMT-ORDER-005` 等人工可读编号,跨迭代保持稳定且删除后不复用。
35
- - `entity-id`:全局唯一 UUIDv7,同一逻辑实体跨迭代复用。
36
- - `version-id`:全局唯一 UUIDv7,每次 added/modified/removed 产生新版本;unchanged 复用历史版本。
35
+ - `entity-id`:全局唯一 32 位 ID(8 位十六进制),同一逻辑实体跨迭代复用。
36
+ - `version-id`:全局唯一 32 位 ID,每次 added/modified/removed 产生新版本;unchanged 复用历史版本。
37
37
  - `predecessor-version`:modified/removed 指向同一实体的直接前序版本。
38
38
 
39
39
  UUID 由本地命令生成,不依赖网络、机器号或中央 ID 服务:
@@ -53,7 +53,7 @@ node skywalk-sdd/log.cjs semantic-identity --delta-state=unchanged --entity-id=<
53
53
  本地命令:
54
54
 
55
55
  ```bash
56
- # 解析与诊断;首次运行会为 Artifact/DocumentSection 分配并持久化结构 UUIDv7 sidecar
56
+ # 解析与诊断;首次运行会为 Artifact/DocumentSection 分配并持久化 32 位结构 ID sidecar
57
57
  node skywalk-sdd/log.cjs semantic-scan --project=. --change=<name>
58
58
 
59
59
  # 文件与工作态本体实例全量对账
@@ -69,6 +69,10 @@ node skywalk-sdd/log.cjs semantic-status --project=. --change=<name>
69
69
  node skywalk-sdd/log.cjs semantic-observe --project=. --change=<name>
70
70
  ```
71
71
 
72
+ `propose/spec/design/task` 在文档写入完成时就必须同步工作态 JSON,不等待 Check 或 Archive。各阶段的 `stage_end` 会自动执行一次 reconcile 作为无 Hook 环境的兜底;Skill 中的显式 `semantic-reconcile` 与该兜底幂等。每份 Markdown 的结构化结果写入 `openspec/changes/<name>/artifacts/<原相对路径>.ontology.json`,`openspec/changes/<name>/artifact-index.json` 与 `working-ontology.json` 提供统一索引与全量工作态视图;这些文件随 change 目录一起提交和归档。`skywalk-sdd/state/ontology/` 仅保留 revision 指针与跨进程锁,默认不提交。
73
+
74
+ 工作态 JSON 使用 `kld-sdd-working-artifact-facts/v1`,明确标记 `canonical=false` 和 `review_status=draft|pending`。它用于当前 Change 的渐进式展开、定向查询和跨文档校验,不能直接入知识库。Check 重新解析原文、校验并在全部通过时生成 pending revision;Archive 再次对账后才确认、转换为 `canonical-facts.json` 并打包。
75
+
72
76
  `semantic-observe` 不是 Hook 替代品:它不能阻止 Cursor、Qoder 或人工编辑,也不能在落盘前控制内容。Observer 启动/重启后会重新对账,暂态失败和锁冲突不会推进文件指纹;Check 和 Archive 始终重新执行 `semantic-reconcile`,以处理重复、乱序或丢失的文件事件。
73
77
 
74
78
  校验 profile:
@@ -79,9 +83,17 @@ node skywalk-sdd/log.cjs semantic-observe --project=. --change=<name>
79
83
  | `full` | 强制 STMT→AC→Design→Task 全链和 Task DAG |
80
84
  | `strict` | 在 full 基础上要求完整语义来源 |
81
85
 
82
- 同一 Change observe/reconcile/check/archive 共用跨进程事务锁。工作态按 revision 目录一次性提交,再原子切换 `current.json`,读取时会拒绝 working/diagnostics/file-index revision 撕裂。
86
+ 同一 Change 的作者阶段同步、observereconcilecheckarchive 共用跨进程事务锁。`working-ontology.json`、diagnostics、file index、artifact index 及所有分文件 JSON 在 change 目录内原子提交,随后切换 `skywalk-sdd/state/ontology/<change>/current.json` 指针;读取时会拒绝任一文件缺失或 revision 撕裂。
87
+
88
+ Archive 使用两阶段事务:先复制到 staging,从 staging 重新解析正文并生成本体快照、canonical facts、转换报告和 v2 manifest,所有步骤成功后才切换为正式 archive。归档完成后同时生成相邻的 `<日期>-<name>.zip`,其中 `archive-manifest.json` 的文件清单和 SHA-256 必须精确覆盖包内文件,可直接交给知识库消费。语义校验、快照、协议转换、打包或 Spec 同步失败时,活动 Change 会保留或恢复,不留下伪 confirmed 半成品。
89
+
90
+ 生产端归档协议产物:
83
91
 
84
- Archive 使用两阶段事务:先复制到 staging,从 staging 重新解析正文并生成 manifest、Spec 投影和带 UUIDv7 `OntologySnapshot` 实体的 `archive-ontology.json`,所有步骤成功后才切换为正式 archive。语义校验、快照、manifest 或 Spec 同步失败时,活动 Change 会保留或恢复,不留下伪 confirmed 半成品。
92
+ - `archive-ontology.json`:KLD-SDD 内部 confirmed 本体快照。
93
+ - `canonical-facts.json`:面向知识库的稳定语义事实,实体与关系均带原文来源及内容哈希。
94
+ - `conversion-report.json`:本体快照到 canonical facts 的转换结果、数量和归一化告警。
95
+ - `archive-manifest.json`:`kld-sdd-archive-manifest/v2` 身份、内容哈希及完整文件清单。
96
+ - `openspec/changes/archive/<日期>-<name>.zip`:知识库可直接读取的 Archive Package。
85
97
 
86
98
  ## 快速开始
87
99
 
@@ -251,6 +263,21 @@ kld-sdd-init --tool codex # 仅配置 Codex
251
263
 
252
264
  ```
253
265
  your-project/
266
+ ├── openspec/
267
+ │ └── changes/
268
+ │ └── <name>/
269
+ │ ├── proposal.md
270
+ │ ├── working-ontology.json
271
+ │ ├── artifact-index.json
272
+ │ ├── diagnostics.json
273
+ │ ├── file-index.json
274
+ │ ├── ontology-identities.json
275
+ │ └── artifacts/
276
+ │ ├── proposal.ontology.json
277
+ │ └── specs/<capability>/
278
+ │ ├── spec.ontology.json
279
+ │ ├── design.ontology.json
280
+ │ └── tasks.ontology.json
254
281
  ├── openspec-templates/ # openSpec 四文档参考模版
255
282
  │ ├── proposal.md
256
283
  │ ├── spec.md
@@ -262,11 +289,7 @@ your-project/
262
289
  │ └── state/ontology/
263
290
  │ ├── .locks/<change>.lock # observe/reconcile/check/archive 跨进程锁
264
291
  │ └── <change>/
265
- ├── current.json # 当前 revision 原子指针
266
- │ └── revisions/<revision>/
267
- │ ├── working-ontology.json
268
- │ ├── diagnostics.json
269
- │ └── file-index.json
292
+ └── current.json # 当前 revision 指针(本体 JSON 在 change 目录)
270
293
  ├── .cursor/
271
294
  │ └── skills/opsx-*/ # SDD skills(扁平一层)
272
295
  ├── .claude/
package/lib/init.js CHANGED
@@ -830,6 +830,15 @@ function deployTelemetryDataDir() {
830
830
  console.log(` ⚠️ Telemetry CLI 源文件缺失: ${telemetrySrc}`);
831
831
  }
832
832
 
833
+ const contextClientSrc = path.join(pkgPath, 'skywalk-sdd', 'context-client.cjs');
834
+ const contextClientDst = path.join(dataDir, 'context-client.cjs');
835
+ if (fs.existsSync(contextClientSrc)) {
836
+ fs.copyFileSync(contextClientSrc, contextClientDst);
837
+ console.log(' ✓ 部署 skywalk-sdd/context-client.cjs(工程 Spec 上下文客户端)');
838
+ } else {
839
+ console.log(` ⚠️ 工程 Spec 上下文客户端缺失: ${contextClientSrc}`);
840
+ }
841
+
833
842
  const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
834
843
  const ontologyDst = path.join(dataDir, 'ontology');
835
844
  if (fs.existsSync(ontologySrc)) {
@@ -847,6 +856,7 @@ function deployTelemetryDataDir() {
847
856
  }
848
857
 
849
858
  console.log(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics|semantic-identity|semantic-reconcile|semantic-check');
859
+ console.log(' ✓ Spec Context: node skywalk-sdd/context-client.cjs --query="<自然语言需求>" --target-stage=spec');
850
860
  console.log(' ✓ Apply worktree: record-base 在 §1.5;收尾 apply-worktree-finish.cjs --change=<name>');
851
861
  console.log('✅ Telemetry 已就绪(数据存储在 skywalk-sdd/,无需配置 MCP)');
852
862
  return true;
@@ -971,7 +981,7 @@ function updateGitignore() {
971
981
  .workbuddy/commands/personal-*
972
982
  .agents/commands/personal-*
973
983
 
974
- # SDD Telemetry 数据(本地度量,可选提交;最终报告落 openspec/changes/<change>/reports/ 随 change 提交)
984
+ # SDD Telemetry 数据(本地度量,可选提交;本体 JSON 与报告随 openspec/changes/<change>/ 提交)
975
985
  skywalk-sdd/events/
976
986
  skywalk-sdd/state/
977
987
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.5.2",
3
+ "version": "2.6.1",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "kld-sdd-init": "bin/kld-sdd-init.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
11
+ "test": "node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
12
12
  },
13
13
  "keywords": [
14
14
  "kld",
@@ -32,6 +32,7 @@
32
32
  "templates/",
33
33
  "kld-sdd-guide.html",
34
34
  "skywalk-sdd/index.cjs",
35
+ "skywalk-sdd/context-client.cjs",
35
36
  "skywalk-sdd/ontology/",
36
37
  "skywalk-sdd/apply-worktree-finish.cjs",
37
38
  "README.md"
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const http = require('http');
6
+ const https = require('https');
7
+ const { URL } = require('url');
8
+
9
+ function parseArgs(argv) {
10
+ const args = {};
11
+ for (const item of argv.slice(2)) {
12
+ if (!item.startsWith('--')) continue;
13
+ const equal = item.indexOf('=');
14
+ if (equal < 0) args[item.slice(2)] = true;
15
+ else args[item.slice(2, equal)] = item.slice(equal + 1);
16
+ }
17
+ return args;
18
+ }
19
+
20
+ function queryFromArgs(args) {
21
+ if (typeof args.query === 'string' && args.query.trim()) {
22
+ return args.query.trim();
23
+ }
24
+ if (typeof args['query-file'] === 'string' && args['query-file'].trim()) {
25
+ return fs.readFileSync(args['query-file'], 'utf8').trim();
26
+ }
27
+ throw new Error('missing --query or --query-file');
28
+ }
29
+
30
+ function buildEndpoint(apiBase, spaceId) {
31
+ const base = String(apiBase || 'http://localhost:8090/api').replace(/\/$/, '');
32
+ return `${base}/v1/spaces/${encodeURIComponent(spaceId)}/context/match-requirement`;
33
+ }
34
+
35
+ function requestJson(url, payload, token, timeoutMs) {
36
+ return new Promise((resolve, reject) => {
37
+ const target = new URL(url);
38
+ const transport = target.protocol === 'https:' ? https : http;
39
+ const body = JSON.stringify(payload);
40
+ const headers = {
41
+ Accept: 'application/json',
42
+ 'Content-Type': 'application/json',
43
+ 'Content-Length': Buffer.byteLength(body),
44
+ };
45
+ if (token) headers.Authorization = `Bearer ${token}`;
46
+
47
+ const request = transport.request(
48
+ {
49
+ hostname: target.hostname,
50
+ port: target.port || (target.protocol === 'https:' ? 443 : 80),
51
+ path: target.pathname + target.search,
52
+ method: 'POST',
53
+ headers,
54
+ timeout: timeoutMs,
55
+ },
56
+ (response) => {
57
+ let responseBody = '';
58
+ response.on('data', (chunk) => {
59
+ responseBody += chunk;
60
+ });
61
+ response.on('end', () => {
62
+ let parsed;
63
+ try {
64
+ parsed = responseBody ? JSON.parse(responseBody) : null;
65
+ } catch {
66
+ reject(new Error(`knowledge base returned invalid JSON (HTTP ${response.statusCode})`));
67
+ return;
68
+ }
69
+ if (response.statusCode >= 400 || parsed?.code !== 0) {
70
+ reject(new Error(parsed?.message || `knowledge base HTTP ${response.statusCode}`));
71
+ return;
72
+ }
73
+ resolve(parsed.data);
74
+ });
75
+ },
76
+ );
77
+ request.on('timeout', () => request.destroy(new Error(`knowledge base timeout after ${timeoutMs}ms`)));
78
+ request.on('error', reject);
79
+ request.write(body);
80
+ request.end();
81
+ });
82
+ }
83
+
84
+ function compactContext(data, includeCandidates) {
85
+ if (includeCandidates) return data;
86
+ return {
87
+ matchType: data.matchType,
88
+ inheritanceAllowed: data.inheritanceAllowed,
89
+ reviewRequired: data.reviewRequired,
90
+ advisory: data.advisory,
91
+ degraded: data.degraded,
92
+ degradationReasons: data.degradationReasons,
93
+ targetStage: data.targetStage,
94
+ reuseBundles: data.reuseBundles,
95
+ answeredQuestions: data.answeredQuestions,
96
+ warnings: data.warnings,
97
+ clarificationQuestions: data.clarificationQuestions,
98
+ specGenerationContext: data.specGenerationContext,
99
+ };
100
+ }
101
+
102
+ async function retrieveContext(args = parseArgs(process.argv), env = process.env) {
103
+ const query = queryFromArgs(args);
104
+ const spaceId = args['space-id'] || env.ENGINEERING_KB_SPACE_ID || '';
105
+ if (!spaceId) {
106
+ return {
107
+ available: false,
108
+ advisory: true,
109
+ degraded: true,
110
+ reason: 'engineering_kb_not_configured',
111
+ hint: 'set ENGINEERING_KB_SPACE_ID to enable reusable Spec context',
112
+ };
113
+ }
114
+
115
+ const apiBase = args['api-base'] || env.ENGINEERING_KB_API || 'http://localhost:8090/api';
116
+ const token = args.token || env.ENGINEERING_KB_TOKEN || '';
117
+ const timeoutMs = Number(args.timeout || env.ENGINEERING_KB_TIMEOUT_MS || 30000);
118
+ const data = await requestJson(
119
+ buildEndpoint(apiBase, spaceId),
120
+ {
121
+ query,
122
+ targetStage: args['target-stage'] || 'spec',
123
+ },
124
+ token,
125
+ timeoutMs,
126
+ );
127
+ return {
128
+ available: true,
129
+ ...compactContext(data, Boolean(args['include-candidates'])),
130
+ };
131
+ }
132
+
133
+ async function main() {
134
+ const args = parseArgs(process.argv);
135
+ try {
136
+ console.log(JSON.stringify(await retrieveContext(args), null, 2));
137
+ } catch (error) {
138
+ console.log(JSON.stringify({
139
+ available: false,
140
+ advisory: true,
141
+ degraded: true,
142
+ reason: 'engineering_kb_unavailable',
143
+ message: error.message,
144
+ }, null, 2));
145
+ if (args.strict) process.exitCode = 1;
146
+ }
147
+ }
148
+
149
+ if (require.main === module) {
150
+ main();
151
+ }
152
+
153
+ module.exports = {
154
+ parseArgs,
155
+ queryFromArgs,
156
+ buildEndpoint,
157
+ requestJson,
158
+ compactContext,
159
+ retrieveContext,
160
+ };
@@ -600,7 +600,6 @@ function assertConfirmedArchive(projectRoot, changeName, archiveDir, options = {
600
600
  }
601
601
 
602
602
  function ensureArchiveManifest(projectRoot, changeName, archiveDir, options = {}) {
603
- const manifestPath = path.join(archiveDir, 'archive-manifest.json');
604
603
  const copiedSpecs = Array.isArray(options.copiedSpecs)
605
604
  ? options.copiedSpecs
606
605
  : syncArchivedSpecs(projectRoot, archiveDir, changeName);
@@ -616,20 +615,24 @@ function ensureArchiveManifest(projectRoot, changeName, archiveDir, options = {}
616
615
  path.join(committedArchiveDir, 'evidence', 'events'),
617
616
  );
618
617
  }
619
- const manifest = {
620
- change: changeName,
621
- 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,
622
628
  reason: options.reason || '变更已完成实施',
623
629
  method: options.method || 'skywalk-full-spec-archive',
624
- source_path: `openspec/changes/${changeName}`,
625
- archive_path: toProjectRelative(projectRoot, committedArchiveDir),
626
- copied_specs: copiedSpecs,
627
- evidence_events_path: evidenceEventsPath,
628
- };
629
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
630
+ sourcePath: `openspec/changes/${changeName}`,
631
+ copiedSpecs,
632
+ evidenceEventsPath,
633
+ });
630
634
  return {
631
- manifest,
632
- manifest_path: manifestPath,
635
+ ...produced,
633
636
  copied_specs: copiedSpecs,
634
637
  };
635
638
  }
@@ -674,18 +677,27 @@ function ensureArchiveSuccessArtifacts(projectRoot, changeName, details = {}, op
674
677
  const manifestInfo = ensureArchiveManifest(normalizedRoot, changeName, archiveDir, {
675
678
  reason: archiveReason,
676
679
  method: archiveMethod || 'skywalk-full-spec-archive',
677
- archivedAt: details.archive_result?.archived_at || nowISO(),
680
+ archivedAt: details.archive_result?.created_at || details.archive_result?.archived_at,
678
681
  });
679
682
 
680
683
  const archiveResult = {
681
684
  reason: archiveReason,
682
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,
683
692
  archive_path: manifestInfo.manifest.archive_path,
684
693
  report_path: toProjectRelative(normalizedRoot, reportPath),
685
694
  // report_html_path 为预期路径:由 cmdEnd 落盘阶段写入(try/catch 容错,失败时不阻塞 md 主产物)。
686
695
  // 此处无条件派生,html 实际落盘失败时该路径可能不存在;语义为"预期路径",消费者不应假定文件已存在。
687
696
  report_html_path: toProjectRelative(normalizedRoot, reportPath.replace(/\.md$/i, '.html')),
688
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),
689
701
  task_completion: taskCompletion,
690
702
  copied_specs: manifestInfo.copied_specs,
691
703
  };
@@ -724,10 +736,13 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
724
736
  const lock = acquireChangeLock(normalizedRoot, changeName, options.lockOptions);
725
737
  const token = lock.token.replace(/-/g, '');
726
738
  const stagingDir = path.join(archiveRoot, `.archive-staging-${token}`);
739
+ const stagingPackagePath = `${stagingDir}.zip`;
740
+ const finalPackagePath = `${archiveDir}.zip`;
727
741
  const sourceBackup = path.join(path.dirname(sourceDir), `.archive-source-${token}`);
728
742
  let specTransaction = null;
729
743
  let sourceMoved = false;
730
744
  let archiveCommitted = false;
745
+ let packageCommitted = false;
731
746
  try {
732
747
  const semanticResult = ontologyRuntime.reconcileChange(normalizedRoot, changeName, {
733
748
  profile: options.profile || 'auto',
@@ -742,6 +757,7 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
742
757
  }
743
758
 
744
759
  fs.rmSync(stagingDir, { recursive: true, force: true });
760
+ fs.rmSync(stagingPackagePath, { force: true });
745
761
  copyDirSync(sourceDir, stagingDir);
746
762
  if (options.failAt === 'after-copy') throw new Error('after-copy');
747
763
 
@@ -768,6 +784,7 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
768
784
  reason: archiveReason,
769
785
  method: 'skywalk-full-spec-archive',
770
786
  archivePath: archiveDir,
787
+ packagePath: stagingPackagePath,
771
788
  copiedSpecs: specTransaction.copiedSpecs,
772
789
  });
773
790
  const confirmed = assertConfirmedArchive(normalizedRoot, changeName, stagingDir, {
@@ -800,6 +817,9 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
800
817
  fs.renameSync(stagingDir, archiveDir);
801
818
  archiveCommitted = true;
802
819
  commitArchivedSpecSync(specTransaction, options.failAt);
820
+ fs.rmSync(finalPackagePath, { force: true });
821
+ fs.renameSync(stagingPackagePath, finalPackagePath);
822
+ packageCommitted = true;
803
823
  if (sourceMoved) fs.rmSync(sourceBackup, { recursive: true, force: true });
804
824
  cleanupArchivedSpecSync(specTransaction);
805
825
 
@@ -807,12 +827,18 @@ function archiveChangeDocs(projectRoot, changeName, options = {}) {
807
827
  ...manifestInfo.manifest,
808
828
  project_root: normalizedRoot,
809
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,
810
834
  active_change_exists: fs.existsSync(sourceDir),
811
835
  };
812
836
  } catch (error) {
813
837
  rollbackArchivedSpecSync(specTransaction);
814
838
  if (archiveCommitted) fs.rmSync(archiveDir, { recursive: true, force: true });
839
+ if (packageCommitted) fs.rmSync(finalPackagePath, { force: true });
815
840
  fs.rmSync(stagingDir, { recursive: true, force: true });
841
+ fs.rmSync(stagingPackagePath, { force: true });
816
842
  if (sourceMoved && fs.existsSync(sourceBackup) && !fs.existsSync(sourceDir)) {
817
843
  fs.renameSync(sourceBackup, sourceDir);
818
844
  }
@@ -3507,6 +3533,37 @@ function cmdStart(args) {
3507
3533
  console.log(JSON.stringify(output, null, 2));
3508
3534
  }
3509
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
+
3510
3567
  /**
3511
3568
  * log end: 记录阶段结束
3512
3569
  */
@@ -3589,6 +3646,30 @@ function cmdEnd(args, options = {}) {
3589
3646
  fail(`details JSON 解析失败: ${err.message}`);
3590
3647
  }
3591
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
+
3592
3673
  const durationMs = startEvent
3593
3674
  ? new Date(timestamp).getTime() - new Date(startEvent.timestamp).getTime()
3594
3675
  : null;
@@ -3732,6 +3813,7 @@ function cmdEnd(args, options = {}) {
3732
3813
  duration_ms: durationMs,
3733
3814
  recorded_at: timestamp,
3734
3815
  report_output: resolvedReportOutput || undefined,
3816
+ semantic_state: semanticState || undefined,
3735
3817
  message: `SDD ${event.command} 阶段结束(${result},耗时 ${durationMs ? (durationMs / 1000).toFixed(1) + 's' : '未知'})`,
3736
3818
  };
3737
3819
  if (!options.silent) {
@@ -4463,6 +4545,28 @@ function cmdArchiveDocs(args) {
4463
4545
  }, { silent: true });
4464
4546
  }
4465
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
+
4466
4570
  console.log(JSON.stringify({
4467
4571
  ...result,
4468
4572
  report_output: stageEnd ? stageEnd.report_output : reportOutput,
@@ -4582,7 +4686,7 @@ SDD Telemetry CLI - 流程度量采集工具
4582
4686
  tasks-status 扫描 Full/Simple 模式 tasks.md 勾选状态
4583
4687
  check-task 扫描变更目录 tasks.md 并勾选指定 task_id
4584
4688
  archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
4585
- semantic-identity 生成或复用实体/版本 UUID;无需网络和中央 ID 服务
4689
+ semantic-identity 生成或复用实体/版本 ID(8 位十六进制);无需网络和中央 ID 服务
4586
4690
  semantic-scan 只读解析并校验当前 Change
4587
4691
  semantic-reconcile 全量对账文件与本地工作态本体实例
4588
4692
  semantic-check 全量对账并在通过时标记 pending