kld-sdd 2.6.3 → 2.6.5

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.
@@ -340,6 +340,161 @@ function validateInheritedReferences(facts, diagnostics, identityHistory) {
340
340
  }
341
341
  }
342
342
 
343
+ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
344
+ const allowed = {
345
+ requirement: new Set(['Capability']),
346
+ scenario: new Set(['SpecificationStatement', 'AcceptanceCriterion']),
347
+ feature: new Set(['Capability']),
348
+ };
349
+ const continuity = facts.continuity || {};
350
+ const kind = String(continuity.kind || '').toLowerCase();
351
+ const resolutionPath = options.continuityResolutionPath
352
+ || (facts.change
353
+ ? require('path').join(
354
+ options.projectRoot || process.cwd(),
355
+ 'openspec',
356
+ 'changes',
357
+ facts.change,
358
+ 'continuity-resolution.json',
359
+ )
360
+ : '');
361
+ let resolution = null;
362
+ if (resolutionPath) {
363
+ try {
364
+ const fs = require('fs');
365
+ if (fs.existsSync(resolutionPath)) {
366
+ resolution = JSON.parse(fs.readFileSync(resolutionPath, 'utf8'));
367
+ }
368
+ } catch {
369
+ resolution = null;
370
+ }
371
+ }
372
+
373
+ if (kind === 'iteration') {
374
+ const caps = (facts.entities || []).filter((entity) => entity.type === 'Capability');
375
+ for (const capability of caps) {
376
+ if (!capability.entity_id || !capability.version_id) {
377
+ diagnostics.push(diagnostic(
378
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
379
+ 'error',
380
+ `Continuity=iteration 时 Capability 缺少 KB 回传身份: ${capability.id}`,
381
+ {
382
+ ...sourceContext(capability),
383
+ entity_id: capability.id,
384
+ suggestion: '在 propose 用 entities/resolve 回填 entity-id / version-id',
385
+ },
386
+ ));
387
+ }
388
+ }
389
+ if (!resolution || resolution.status === 'pending') {
390
+ diagnostics.push(diagnostic(
391
+ DIAGNOSTIC_CODES.CONTINUITY_DECISION_REQUIRED,
392
+ 'error',
393
+ 'Continuity=iteration 但 continuity-resolution.json 缺失或仍为 pending',
394
+ {
395
+ file: resolutionPath || 'continuity-resolution.json',
396
+ suggestion: '在 propose/spec 完成 A/B/C 决议后再 check',
397
+ },
398
+ ));
399
+ }
400
+ }
401
+
402
+ if (resolution && resolution.status && resolution.status !== 'pending') {
403
+ const decisions = [
404
+ ...(Array.isArray(resolution.capabilities) ? resolution.capabilities : []),
405
+ ...(Array.isArray(resolution.scenarios) ? resolution.scenarios : []),
406
+ ];
407
+ const entitiesByAnchor = new Map(
408
+ (facts.entities || []).map((entity) => [String(entity.anchor_id || entity.id || '').toUpperCase(), entity]),
409
+ );
410
+ for (const decision of decisions) {
411
+ const anchor = String(decision.anchor || '').toUpperCase();
412
+ if (!anchor) continue;
413
+ const entity = entitiesByAnchor.get(anchor);
414
+ if (!entity) continue;
415
+ const decisionCode = String(decision.decision || '').toLowerCase();
416
+ const decidedEntityId = String(decision.entityId || decision.entity_id || '').toLowerCase();
417
+ const actualEntityId = String(entity.entity_id || '').toLowerCase();
418
+ if (
419
+ (decisionCode === 'reuse-existing-identity' || decisionCode === 'reuse_existing' || decisionCode === 'a')
420
+ && decidedEntityId
421
+ && actualEntityId
422
+ && decidedEntityId !== actualEntityId
423
+ ) {
424
+ diagnostics.push(diagnostic(
425
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
426
+ 'error',
427
+ `决议复用历史身份但产物仍使用新 entity_id: ${anchor}`,
428
+ {
429
+ ...sourceContext(entity),
430
+ entity_id: entity.id,
431
+ suggestion: '按 continuity-resolution 回填历史 entity-id / version-id',
432
+ },
433
+ ));
434
+ }
435
+ if (
436
+ (decisionCode === 'assign-new-anchor' || decisionCode === 'assign_new' || decisionCode === 'b')
437
+ && decidedEntityId
438
+ && actualEntityId
439
+ && decidedEntityId === actualEntityId
440
+ && String(decision.notes || '').toLowerCase().includes('new-anchor-required')
441
+ ) {
442
+ diagnostics.push(diagnostic(
443
+ DIAGNOSTIC_CODES.CONTINUITY_IDENTITY_MISMATCH,
444
+ 'error',
445
+ `决议为新对象但仍共用旧锚点/身份: ${anchor}`,
446
+ {
447
+ ...sourceContext(entity),
448
+ entity_id: entity.id,
449
+ suggestion: '更换新锚点并分配新 entity_id 后重跑 check',
450
+ },
451
+ ));
452
+ }
453
+ }
454
+ }
455
+
456
+ const bindingKeys = new Map();
457
+ for (const entity of facts.entities || []) {
458
+ for (const ref of entity.external_refs || []) {
459
+ const objectType = String(ref.object_type || '').toLowerCase();
460
+ const allowedTypes = allowed[objectType];
461
+ if (!allowedTypes) {
462
+ diagnostics.push(diagnostic(
463
+ DIAGNOSTIC_CODES.EXTERNAL_REF_INVALID,
464
+ 'error',
465
+ `未知 external object_type: ${objectType} (${entity.id})`,
466
+ { ...sourceContext(entity), entity_id: entity.id },
467
+ ));
468
+ continue;
469
+ }
470
+ if (!allowedTypes.has(entity.type)) {
471
+ diagnostics.push(diagnostic(
472
+ DIAGNOSTIC_CODES.EXTERNAL_REF_TYPE_MISMATCH,
473
+ 'error',
474
+ `external object_type=${objectType} 与实体类型 ${entity.type} 不匹配: ${entity.id}`,
475
+ { ...sourceContext(entity), entity_id: entity.id },
476
+ ));
477
+ }
478
+ const key = `${ref.system}|${objectType}|${ref.external_id}|${entity.anchor_id || entity.id}`;
479
+ const prior = bindingKeys.get(key);
480
+ if (prior && prior !== entity.entity_id) {
481
+ diagnostics.push(diagnostic(
482
+ DIAGNOSTIC_CODES.EXTERNAL_REF_CONFLICT,
483
+ 'error',
484
+ `同外部键+同锚点绑定了不同 entity_id: ${entity.id}`,
485
+ {
486
+ ...sourceContext(entity),
487
+ entity_id: entity.id,
488
+ suggestion: '复用历史身份或更换新锚点后重开 entity_id',
489
+ },
490
+ ));
491
+ } else if (entity.entity_id) {
492
+ bindingKeys.set(key, entity.entity_id);
493
+ }
494
+ }
495
+ }
496
+ }
497
+
343
498
  function validateTraceability(facts, options = {}) {
344
499
  const profile = normalizeProfile(options.profile || 'auto', facts.profile || 'simple');
345
500
  const graphEntities = options.effectiveGraph && options.effectiveGraph.effective_entities
@@ -356,6 +511,7 @@ function validateTraceability(facts, options = {}) {
356
511
  const entitiesById = new Map();
357
512
 
358
513
  validateArtifactPresence(facts, profile, diagnostics);
514
+ validateContinuityAndExternalRefs(facts, diagnostics, options);
359
515
 
360
516
  if (profile === 'strict') {
361
517
  for (const artifact of facts.artifacts || []) {
@@ -0,0 +1,32 @@
1
+ {
2
+ "status": "resolved",
3
+ "updatedAt": "2026-07-23T00:00:00.000Z",
4
+ "capabilities": [
5
+ {
6
+ "anchor": "CAP-ORDER-CANCEL",
7
+ "externalKey": {
8
+ "system": "requirement-mgmt",
9
+ "objectType": "requirement",
10
+ "externalId": "REQ-FI-2024-001"
11
+ },
12
+ "decision": "reuse-existing-identity",
13
+ "entityId": "00000000-0000-4000-8000-000000000001",
14
+ "currentVersionId": "00000000-0000-4000-8000-000000000011",
15
+ "notes": "propose 阶段 A/B/C:复用历史能力"
16
+ }
17
+ ],
18
+ "scenarios": [
19
+ {
20
+ "anchor": "AC-ORDER-CANCEL-001",
21
+ "externalKey": {
22
+ "system": "requirement-mgmt",
23
+ "objectType": "scenario",
24
+ "externalId": "REQ-FI-2024-001:SCN-req-date-empty-002"
25
+ },
26
+ "decision": "reuse-existing-identity",
27
+ "entityId": "00000000-0000-4000-8000-000000000021",
28
+ "currentVersionId": "00000000-0000-4000-8000-000000000031",
29
+ "notes": "spec 阶段场景冲突决议"
30
+ }
31
+ ]
32
+ }
@@ -7,6 +7,24 @@ delta-state: "added"
7
7
  predecessor-version: "" # added 留空;modified/removed 指向直接前序版本
8
8
  mode: "" # full=分 Capability 产物,simple=根目录精简产物
9
9
  test-strategy: "" # tdd=测试先行, impl-first=实现优先, none=无测试
10
+ # 外部需求键(首轮冷启动也建议写入,便于 ingest 种桥)
11
+ requirement-refs:
12
+ - system: requirement-mgmt
13
+ object-type: requirement
14
+ external-id: REQ-<DOMAIN>-<NNN>
15
+ # Continuity:字段一律来自知识库 resolve,禁止本地 archive 文件夹名
16
+ continuity:
17
+ kind: new # iteration | similar-reference | new
18
+ kb-space-id: "" # KB space UUID(iteration 时必填)
19
+ kb-id: "" # KB UUID(iteration 时必填)
20
+ base-change-id: "" # 可选,来自 KB source/change_id
21
+ base-archive-id: "" # 可选溯源(KB archive_id),不是本地目录名
22
+ base-capabilities: []
23
+ # iteration 示例:
24
+ # base-capabilities:
25
+ # - anchor: CAP-<CAPABILITY>
26
+ # entity-id: <uuid>
27
+ # current-version-id: <uuid>
10
28
  ---
11
29
 
12
30
  # proposal.md - 业务意图与上下文总览
@@ -34,6 +34,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
34
34
  - **version-id**: <UUID>
35
35
  - **delta-state**: added
36
36
  - **predecessor-version**: 无
37
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
37
38
  - **当** <!-- 触发条件 -->
38
39
  - **预期** <!-- 预期结果 -->
39
40
 
@@ -42,6 +43,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
42
43
  - **version-id**: <UUID>
43
44
  - **delta-state**: added
44
45
  - **predecessor-version**: 无
46
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
45
47
  - **当** <!-- 触发条件 -->
46
48
  - **预期** <!-- 预期结果 -->
47
49
 
@@ -61,6 +63,7 @@ capability-id: "CAP-<CAPABILITY>" # 必须与 proposal.md 中的 Capability ID
61
63
  - **version-id**: <新 UUID>
62
64
  - **delta-state**: <modified|added>
63
65
  - **predecessor-version**: <modified 时填写;added 为无>
66
+ - **external-ref**: requirement-mgmt:scenario:REQ-<DOMAIN>-<NNN>:SCN-<slug>
64
67
  - **当** <!-- 触发条件 -->
65
68
  - **预期** <!-- 预期结果 -->
66
69
 
@@ -3,10 +3,11 @@ name: opsx-archive
3
3
  description: "归档变更技能 - 将已结束的 SDD 变更真实移入 archive,并生成最终中文度量报告"
4
4
  argument-hint: "[change-name]"
5
5
  license: MIT
6
- compatibility: Requires skywalk-sdd/log.cjs.
6
+ compatibility: Requires skywalk-sdd/log.cjs; depends on opsx-kb-ingest for KB upload.
7
7
  metadata:
8
8
  author: sdd-team
9
9
  version: "3.2"
10
+ depends-on: opsx-kb-ingest
10
11
  allowed-tools:
11
12
  - Bash
12
13
  - Read
@@ -16,6 +17,8 @@ allowed-tools:
16
17
 
17
18
  你是一个 SDD(Specification-Driven Development)变更归档专家。激活本技能后,你要安全地结束变更生命周期:真实归档文档、同步正式 specs、记录 archive telemetry,并生成最终中文度量报告。
18
19
 
20
+ > **硬依赖(收尾入库)**:zip 生成后的上传依赖同级已部署的 **`opsx-kb-ingest`**。进入 §5.5 前必须先 `Read` 该技能的 `SKILL.md` 并完成其 Session 启动。缺失则提示用户重新 `kld-sdd-init`,**不要**自造另一套入库协议。
21
+
19
22
  > **跨平台执行规则**
20
23
  > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
21
24
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
@@ -108,13 +111,19 @@ node skywalk-sdd/log.cjs archive-docs --project=. --change=<变更名称> --reas
108
111
  该命令成功后必须已经完成:
109
112
  - 活动目录 `openspec/changes/<name>/` 被移入 `openspec/changes/archive/<日期>-<name>/`。
110
113
  - 归档目录写入 `archive-ontology.json`、`canonical-facts.json`、`conversion-report.json` 和新版 `archive-manifest.json`。
111
- - 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`,包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
114
+ - 生成知识库可直接消费的 `openspec/changes/archive/<日期>-<name>.zip`(canonical-facts **v2**,含 `external_refs`),包内文件必须由 manifest 完整列举并通过 SHA-256 校验。
112
115
  - Full Spec 的 `specs/<capability>/spec.md` 同步到 `openspec/specs/<capability>/spec.md`。
113
116
  - archive 阶段写入 `stage_end`。
114
117
  - 未勾选 tasks 被写入 `archive_result.task_completion`。
115
118
  - 最终中文报告生成到 `openspec/changes/archive/<日期>-<name>/reports/<name>-report.md` 及同名 `<name>-report.html`(默认同时生成 .md 与 .html 双产物,默认归档后 archive 目录,可用 --report-output 自定义)。
116
119
  - 执行日志 `openspec/changes/archive/<日期>-<name>/logs/execution-log.md` 随归档整目录迁移(人读审计层)。
117
120
 
121
+ ### 5.5 收尾入库(opsx-kb-ingest)
122
+
123
+ 1. 确认 `${AGENT_SKILL_DIR}/opsx-kb-ingest/SKILL.md` 存在并 Read;按该技能完成 API Key / targets。
124
+ 2. 归档 zip 生成后,**加载并执行** **`opsx-kb-ingest`** 上传(勿只口头提示而不走技能流程);成功则写 `ingest-receipt.json`。
125
+ 3. 若返回 `EXTERNAL_REF_CONFLICT`,引导回 spec/check 修正后重入,**禁止**在 KB 内现场改绑。
126
+
118
127
  > 注意:`archive-docs` 成功执行后已经在内部写入 `stage_end`,因此**不要在成功的归档后再单独运行 `node skywalk-sdd/log.cjs end --command=archive ...`**。仅在第 5 步归档命令失败时,才需要运行下方的失败分支 `end`。
119
128
 
120
129
  如果该命令失败,以失败状态结束 telemetry:
@@ -216,6 +216,16 @@ node skywalk-sdd/log.cjs record --type=conformance_review --command=check --proj
216
216
 
217
217
  ---
218
218
 
219
+ ## Continuity / external_ref 确定性门禁
220
+
221
+ `opsx-check` **不联网提问**。Agent 应在 propose/spec 已问完;本阶段只验证并入既有 apply 前门禁:
222
+
223
+ - Continuity=`iteration` 时每个 CAP 具备 KB 回传 `entity-id` / `version-id`
224
+ - 已写 spec 的 Capability:场景 `external-ref` 与 `continuity-resolution.json` 决议一致;同 key+同锚点未偷偷换 entity_id
225
+ - 用户选「原对象」却仍用新 id、或选「新对象」却仍共用旧锚点 → 失败(`CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT`)
226
+ - 决议缺失 / pending / 与产物不一致 → `CONTINUITY_DECISION_REQUIRED`
227
+ - CI/非交互:失败即非零退出并打印修复说明,不挂起等待输入
228
+
219
229
  ## 本体语义关系门禁
220
230
 
221
231
  在其他质量检查前必须运行:
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: opsx-kb-ingest
3
+ description: >-
4
+ Uploads knowledge archive packages (zip) to the Engineering KB via API Key
5
+ (archive:ingest scope). Prompts for API key and multi-selects spaces/KBs into
6
+ local skill state. Supports upload, job status query, job list, and retry.
7
+ Use when ingesting new or updated knowledge archives into the ontology KB.
8
+ argument-hint: "[path-to-archive.zip]"
9
+ license: MIT
10
+ compatibility: Requires Engineering KB API (API Key with archive:ingest).
11
+ metadata:
12
+ author: sdd-team
13
+ version: "1.0"
14
+ source: "kb-sdd/skills/opsx-kb-ingest"
15
+ allowed-tools:
16
+ - Bash
17
+ - Read
18
+ - Write
19
+ - Edit
20
+ ---
21
+
22
+ # 本体知识库 · 入库
23
+
24
+ > **部署说明**:本技能随 `kld-sdd-init` 安装到项目 skills 目录。权威源在工程知识库仓 `skills/opsx-kb-ingest`;`opsx-archive` **硬依赖**本技能完成收尾入库。
25
+
26
+ 只负责**入库**(zip 上传)。鉴权只用 **API Key**(`Authorization: Bearer sk_sdd_…`),**禁止**走手机号登录。
27
+
28
+ > 控制台知识库页另有浮动 Ontology Agent(会话登录 + SSE);本 Skill 仍走 API Key,二者分开。
29
+
30
+ 本地状态文件(含密钥,勿提交):
31
+
32
+ ```text
33
+ skills/opsx-kb-ingest/.local/state.json
34
+ ```
35
+
36
+ 字段说明 → [reference.md](reference.md)。
37
+
38
+ ## Session 启动(每次用本 Skill 必做)
39
+
40
+ ```
41
+ Task Progress:
42
+ - [ ] 1. 读 .local/state.json(没有则当空)
43
+ - [ ] 2. 无 apiKey → 向用户索取并写入 state(勿把完整 key 打进聊天摘要)
44
+ - [ ] 3. 无 targets 或用户要重置 → 拉空间/KB 列表,让用户多选后写入
45
+ - [ ] 4. 按意图执行入库操作(上传 / 查状态 / 列表 / 重试)
46
+ - [ ] 5. 按模板输出结果
47
+ ```
48
+
49
+ ### 1–2. API Key
50
+
51
+ 若 `state.apiKey` 为空或无效(401/403):
52
+
53
+ 1. 请用户提供 API Key(控制台「API 密钥」创建,至少含 `archive:ingest`)。
54
+ 2. 可选:请用户确认 `api`(默认 `http://localhost:8090/api`)与 `tenantKey`(默认 `default`)。
55
+ 3. 写入 `.local/state.json`(创建目录若不存在)。
56
+ 4. 用 `GET $API/health` 探活;再用 `GET $API/v1/spaces?tenantKey=…` + Bearer 校验 key。
57
+
58
+ 用户说「换密钥 / 重置 API Key」→ 清空 `apiKey`(可保留 targets),回到本步。
59
+
60
+ ### 3. 选择空间与知识库(支持多选)
61
+
62
+ 若 `state.targets` 为空,或用户说「重新选择 / 重置空间 / 重置知识库」:
63
+
64
+ 1. `GET $API/v1/spaces?tenantKey=$TENANT_KEY`
65
+ 2. 对每个相关 space:`GET $API/v1/spaces/{spaceId}/knowledge-bases`
66
+ 3. 向用户展示「空间名 / spaceId → KB 名 / kbId」清单,**允许多选**。
67
+ 4. 写入 `targets: [{ spaceId, spaceName, spaceKey, kbId, kbName }, …]`。
68
+ 5. 仅清空 targets、保留 apiKey 即完成「重置空间和知识库」。
69
+
70
+ 入库时:用户指定目标 KB(从 `targets` 中选择),仅对选中的 KB 执行上传。
71
+
72
+ ### 4. 入库操作
73
+
74
+ 路径前缀:`/api/v1/spaces/{spaceId}/knowledge-bases/{kbId}/ingestions`
75
+ 所有请求:`-H "Authorization: Bearer $API_KEY"`
76
+
77
+ | 意图 | 调用 |
78
+ |------|------|
79
+ | 上传 zip 包入库 | `POST …/ingestions`(multipart,字段名 `package`) |
80
+ | 列出入库任务 | `GET …/ingestions` |
81
+ | 查看任务状态 | `GET …/ingestions/{jobId}` |
82
+ | 重试失败任务 | `POST …/ingestions/{jobId}/retry` |
83
+
84
+ **上传入库**
85
+
86
+ ```bash
87
+ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions" \
88
+ -H "Authorization: Bearer $API_KEY" \
89
+ -F "package=@/path/to/your-archive.zip;type=application/zip"
90
+ ```
91
+
92
+ **查看任务状态**
93
+
94
+ ```bash
95
+ curl -sS -X GET "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JOB_ID" \
96
+ -H "Authorization: Bearer $API_KEY"
97
+ ```
98
+
99
+ **列出入库任务**
100
+
101
+ ```bash
102
+ curl -sS -X GET "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions" \
103
+ -H "Authorization: Bearer $API_KEY"
104
+ ```
105
+
106
+ **重试失败任务**
107
+
108
+ ```bash
109
+ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JOB_ID/retry" \
110
+ -H "Authorization: Bearer $API_KEY"
111
+ ```
112
+
113
+ ### Archive Package 要求
114
+
115
+ 上传的 zip 包必须满足以下条件(否则入库会失败):
116
+
117
+ - 包含三个必需 JSON 文件:`archive-manifest.json`、`canonical-facts.json`、`conversion-report.json`
118
+ - `canonical-facts.json` 支持 `kld-sdd-canonical-facts/v1` 与 **`v2`**(v2 可选实体级 `external_refs`)
119
+ - 所有文件需通过 SHA-256 校验(manifest 中记录每个文件的 `content_hash`)
120
+ - 允许的文件类型:`.md`、`.json`、`.jsonl`、`.yaml`、`.yml`
121
+ - 大小限制:包 ≤ 50MB,条目 ≤ 2000 个,单文件 ≤ 10MB,解压后 ≤ 200MB
122
+ - `archive-manifest.json` 中的 `project_id` 必须与目标 Space 的 `spaceKey` 一致
123
+
124
+ **v2 本地预检(skill 侧,上传前)**
125
+
126
+ - `external_refs[]` 每项含 `system` / `object_type` / `external_id`
127
+ - object_type 与实体类型匹配:`requirement`/`feature`→Capability;`scenario`→SpecificationStatement|AcceptanceCriterion
128
+ - 包内无重复 `(external key, entity_id)` 绑定对
129
+ - `project_id` / `spaceKey` 既有规则保留
130
+
131
+ 详细字段说明 → [reference.md](reference.md)。
132
+
133
+ ### 5. 输出模板
134
+
135
+ ```markdown
136
+ ### 入库结果
137
+ - 目标 KB:{spaceName}/{kbName}
138
+ - 操作:{上传 | 查状态 | 列表 | 重试}
139
+ - 状态:{succeeded | failed | running | idempotent}
140
+ - Job ID:{jobId}
141
+
142
+ ### 详情
143
+ - archiveId:{archiveId}
144
+ - contentHash:{contentHash}
145
+ - stage:{stage}
146
+ - relationStatus:{relationStatus}
147
+ - projectionStatus:{projectionStatus}
148
+ - 外部引用写入数:{externalRefsWritten 或 0}
149
+ - 幂等命中:{是 / 否}
150
+ - 错误信息:{errorCode — errorMessage 或 无}
151
+ ```
152
+
153
+ 成功时展示 `report.externalRefsWritten`。冲突**不**出现在成功模板。
154
+
155
+ 若 `errorCode=EXTERNAL_REF_CONFLICT`(整包已回滚):
156
+
157
+ 1. 展示 `report.details`(externalRef / anchor / existingEntityId / incomingEntityId)。
158
+ 2. 向用户给出两个修正选项:
159
+ - **复用历史身份**:回到 kld-sdd,复用 KB 中原有 entity_id / current version,重新 check → 归档 → 入库
160
+ - **改为新锚点**:回到 kld-sdd,分配新锚点与新 entity_id,重新 check → 归档 → 入库
161
+ 3. **禁止**在 KB 内现场改绑或解绑。
162
+
163
+ **硬规则**
164
+
165
+ - 无 `apiKey` 不得猜密钥、不得改走 login。
166
+ - 无 `targets` 不得臆造 spaceId/kbId。
167
+ - 上传前确认 zip 包路径存在且为有效 zip 文件。
168
+ - 入库失败时展示 `errorCode` 和 `errorMessage`,不编造原因。
169
+ - 完整 apiKey 只写 state 文件;聊天里最多显示前缀(如 `sk_sdd_****`)。
170
+ - 401/403 时清掉 `apiKey`,请用户重贴;勿循环重试。
171
+
172
+ ## 用户口令
173
+
174
+ | 用户说 | Agent 做 |
175
+ |--------|----------|
176
+ | (首次使用) | 要 key → 选 KB(多选)→ 再操作 |
177
+ | 换密钥 / 重置 API Key | 清 apiKey,重走第 2 步 |
178
+ | 重新选择 / 重置空间或知识库 | 清 targets,重走第 3 步 |
179
+ | 上传 / 入库 | 用当前 targets 中选中的 KB 上传 zip |
180
+ | 查状态 / 看任务 | 用 jobId 查询任务状态 |
181
+ | 列任务 / 看历史 | 列出最近入库任务 |
182
+ | 重试 | 对失败的 jobId 执行重试 |
183
+
184
+ ## 心智模型(简述)
185
+
186
+ - 入库是同步操作:上传后后端立即解析 zip、校验、写入数据库,返回最终 job 状态。
187
+ - 幂等机制:同一 `archive_id` + `content_hash` 重复上传会命中幂等,不重复写入。
188
+ - `archive_id` 不可变:同一 `archive_id` 上传不同 `content_hash` 会被拒绝(`ARCHIVE_IMMUTABILITY_VIOLATION`)。
189
+ - `project_id` 必须匹配:zip 包 manifest 中的 `project_id` 必须等于目标 Space 的 `spaceKey`。
190
+ - 细节与 state schema → [reference.md](reference.md)。