kld-sdd 2.7.8-6 → 2.7.8-7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.7.8-6",
3
+ "version": "2.7.8-7",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "lib/init.js",
6
6
  "bin": {
@@ -7706,7 +7706,11 @@ function cmdCheckRecord(args) {
7706
7706
  const preflight = JSON.parse(fs.readFileSync(path.resolve(args['preflight-file']), 'utf8'));
7707
7707
  const review = JSON.parse(fs.readFileSync(path.resolve(args['review-file']), 'utf8'));
7708
7708
  const state = require('./ontology/runtime.cjs').reconcileChange(projectRoot, args.change, { profile: preflight.profile || 'auto', markPending: true });
7709
- if (!preflight.valid || !state.valid || preflight.revision !== state.revision
7709
+ const { splitCheckDiagnostics } = require('./ontology/spec-ontology.cjs');
7710
+ const preflightBlocked = preflight.product_valid === false
7711
+ || (preflight.product_valid == null && !splitCheckDiagnostics(preflight.diagnostics).product_valid);
7712
+ const stateBlocked = !splitCheckDiagnostics(state.diagnostics).product_valid;
7713
+ if (preflightBlocked || stateBlocked || preflight.revision !== state.revision
7710
7714
  || preflight.review_inputs?.identity_history_hash !== state.state.identity_history_hash
7711
7715
  || JSON.stringify(preflight.review_inputs?.files) !== JSON.stringify(state.state.files)) {
7712
7716
  throw new Error('CHECK_REVIEW_STALE: 文档或历史身份已变化,重新预检并评审受影响内容');
@@ -8239,16 +8243,20 @@ function cmdSemanticIdentity(args) {
8239
8243
  }
8240
8244
 
8241
8245
  function semanticResultPayload(result) {
8246
+ const split = require('./ontology/spec-ontology.cjs').splitCheckDiagnostics(result.diagnostics);
8242
8247
  return {
8243
8248
  change: result.state.change,
8244
8249
  profile: result.profile,
8245
8250
  valid: result.valid,
8251
+ product_valid: split.product_valid,
8246
8252
  review_status: result.state.review_status,
8247
8253
  revision: result.revision,
8248
8254
  changed: Boolean(result.changed),
8249
8255
  counts: result.counts,
8250
8256
  paths: result.paths,
8251
8257
  diagnostics: result.diagnostics,
8258
+ tool_diagnostics: split.tool_diagnostics,
8259
+ product_diagnostics: split.product_diagnostics,
8252
8260
  };
8253
8261
  }
8254
8262
 
@@ -8290,13 +8298,32 @@ function cmdSemanticReconcile(args, options = {}) {
8290
8298
  total: Number(finished - started) / 1e6,
8291
8299
  };
8292
8300
  }
8301
+ if (options.productGate && payload.tool_diagnostics.length > 0) {
8302
+ try {
8303
+ const repair = require('./ontology/spec-ontology.cjs').materializeChangeSpecOntology(
8304
+ context.projectRoot,
8305
+ context.changeName,
8306
+ );
8307
+ payload.tool_repair = {
8308
+ repaired: repair.validation.valid,
8309
+ output_path: repair.output_path,
8310
+ mapping_path: repair.mapping_path,
8311
+ };
8312
+ } catch (error) {
8313
+ payload.tool_repair = { repaired: false };
8314
+ }
8315
+ }
8293
8316
  console.log(JSON.stringify(payload, null, 2));
8294
- if (!result.valid) process.exitCode = 1;
8317
+ if (options.productGate) {
8318
+ if (!payload.product_valid) process.exitCode = 1;
8319
+ } else if (!result.valid) {
8320
+ process.exitCode = 1;
8321
+ }
8295
8322
  return result;
8296
8323
  }
8297
8324
 
8298
8325
  function cmdSemanticCheck(args) {
8299
- return cmdSemanticReconcile(args, { markPending: true, includeTasks: Boolean(args['include-tasks']) });
8326
+ return cmdSemanticReconcile(args, { markPending: true, includeTasks: Boolean(args['include-tasks']), productGate: true });
8300
8327
  }
8301
8328
 
8302
8329
  function cmdSemanticStatus(args) {
@@ -12,6 +12,11 @@
12
12
  * node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]
13
13
  * node skywalk-sdd/kb-upload.cjs --package=<zip-path>
14
14
  * node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>
15
+ * node skywalk-sdd/kb-upload.cjs --folder=<path> --require-review
16
+ *
17
+ * 历史产物(缺 entity-id / version-id):
18
+ * 自动旁路物化 spec-ontology.json,无 semantic_key 冲突时自动确认并发布为 current;
19
+ * 有冲突或 --require-review 时只上传候选,需人工 approve-spec-ontology 后再上传。
15
20
  *
16
21
  * project_id 解析:
17
22
  * 1. $SPEC_ROOT/skywalk-sdd/project-identity.json 的 project_id
@@ -243,9 +248,65 @@ function preCheckProjectId(projectId, state, selectedTarget) {
243
248
  * 5. materializeArchivePackage(生成 manifest + canonical-facts + conversion-report + zip)
244
249
  * 6. 返回 zip 路径
245
250
  */
251
+ /**
252
+ * 历史产物的 Spec 本体解析:
253
+ * - 已审核且与当前 Markdown 一致 → 直接复用
254
+ * - 否则重新物化。重复锚点、非法 UUID、身份冲突由物化自动改写为稳定身份并确认发布
255
+ * - 只有显式 requireReview,或正文没有结构化事实时,才保留候选
256
+ * 全程不改写 Markdown,只读写 <change>/ontology/spec-ontology.json 与 legacy-mapping.json。
257
+ */
258
+ function resolveLegacySpecOntology(specRoot, changeName, changeDir, facts, options = {}) {
259
+ const {
260
+ buildSpecOntology,
261
+ materializeChangeSpecOntology,
262
+ approveSpecOntologyFile,
263
+ specOntologyContentSignature,
264
+ } = require('./ontology/spec-ontology.cjs');
265
+ const { ensureProjectIdentity } = require('./ontology/archive-package.cjs');
266
+ const identity = ensureProjectIdentity(specRoot);
267
+ const ontologyPath = path.join(changeDir, 'ontology', 'spec-ontology.json');
268
+
269
+ const fresh = buildSpecOntology(facts, {
270
+ projectId: identity.project_id,
271
+ compatibilityMode: 'LEGACY_MATERIALIZED',
272
+ reviewStatus: 'pending',
273
+ });
274
+ const freshSignature = specOntologyContentSignature(fresh);
275
+ const existing = readJsonSafe(ontologyPath);
276
+ const existingApproved = existing
277
+ && existing.compatibility_mode === 'LEGACY_MATERIALIZED'
278
+ && existing.review_status === 'confirmed'
279
+ && existing.publication_eligible === true
280
+ && existing.project_id === identity.project_id;
281
+ if (existingApproved && specOntologyContentSignature(existing) === freshSignature) {
282
+ return { approved: existing, status: 'reused', path: ontologyPath, collisions: [] };
283
+ }
284
+
285
+ const materialized = materializeChangeSpecOntology(specRoot, changeName, {
286
+ changeDir,
287
+ projectId: identity.project_id,
288
+ compatibilityMode: 'LEGACY_MATERIALIZED',
289
+ reviewStatus: 'pending',
290
+ });
291
+ if (!materialized.validation.valid) {
292
+ throw new Error(`历史 Spec 本体物化失败: ${materialized.validation.errors.join('; ')}`);
293
+ }
294
+ const repairs = (materialized.payload.warnings || [])
295
+ .filter((warning) => warning.startsWith('LEGACY_DUPLICATE_ANCHOR:') || warning.startsWith('LEGACY_IDENTITY_REPAIRED:'));
296
+ if (options.requireReview) {
297
+ return { approved: null, status: 'pending_review', path: ontologyPath, collisions: repairs };
298
+ }
299
+ if (materialized.payload.entities.length === 0) {
300
+ return { approved: null, status: 'needs_review', path: ontologyPath, collisions: repairs };
301
+ }
302
+ const approved = approveSpecOntologyFile(materialized.output_path, { allowCollisions: true });
303
+ return { approved: approved.payload, status: 'auto_approved', path: ontologyPath, collisions: repairs };
304
+ }
305
+
246
306
  function buildPackageFromFolder(specRoot, changeName, options = {}) {
247
307
  const runtime = require('./ontology/runtime.cjs');
248
308
  const archivePackage = require('./ontology/archive-package.cjs');
309
+ const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
249
310
 
250
311
  // 1. 语义管线
251
312
  console.log('→ 运行语义管线 (semantic-reconcile)...');
@@ -253,15 +314,17 @@ function buildPackageFromFolder(specRoot, changeName, options = {}) {
253
314
  profile: 'auto',
254
315
  markPending: true,
255
316
  });
256
- const blockingDiagnostics = semanticResult.diagnostics.filter(d => d.severity === 'error');
257
- const legacyIdentityOnly = blockingDiagnostics.length > 0
258
- && blockingDiagnostics.every(d => d.code === 'SEM_UUID_MISSING');
317
+ const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
318
+ const legacyIdentityOnly = legacy.legacyCompatible;
259
319
  if (!semanticResult.valid) {
260
- const errors = blockingDiagnostics.map(d => `${d.code}: ${d.message}`);
261
320
  if (!legacyIdentityOnly) {
321
+ const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
262
322
  throw new Error(`语义校验失败:\n${errors.join('\n')}`);
263
323
  }
264
- console.warn('⚠️ 检测到历史产物缺少身份标识,将生成旁路映射和待审核本体包,不改写 Markdown。');
324
+ console.warn('⚠️ 检测到历史产物缺少身份标识,将生成旁路映射与本体包,不改写 Markdown。');
325
+ for (const item of legacy.tolerated) {
326
+ console.warn(` ↳ 兼容放行 ${item.code}: ${item.message}`);
327
+ }
265
328
  } else {
266
329
  console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
267
330
  }
@@ -278,25 +341,31 @@ function buildPackageFromFolder(specRoot, changeName, options = {}) {
278
341
  copyDirRecursive(changeDir, stagingDir);
279
342
 
280
343
  let result;
344
+ let legacyStatus = null;
281
345
  if (legacyIdentityOnly) {
282
- const approvedPath = path.join(changeDir, 'ontology', 'spec-ontology.json');
283
- const approvedOntology = readJsonSafe(approvedPath);
284
- const useApproved = approvedOntology?.review_status === 'confirmed'
285
- && approvedOntology?.publication_eligible === true;
286
- if (useApproved) {
287
- console.log('✓ 已发现审核通过的历史 Spec 本体,将校验内容一致性后发布');
288
- }
346
+ const resolved = resolveLegacySpecOntology(specRoot, changeName, changeDir, semanticResult.state, {
347
+ requireReview: Boolean(options.requireReview),
348
+ });
349
+ legacyStatus = resolved.status;
350
+ const messages = {
351
+ reused: '✓ 已发现审核通过且与当前 Markdown 一致的历史 Spec 本体,直接发布',
352
+ auto_approved: '✓ 历史 Spec 本体已自动物化并确认,身份和重复锚点已在旁路改写,将发布为 current 事实',
353
+ needs_review: '⚠️ 历史正文没有可发布的结构化事实,仅保留为证据候选',
354
+ pending_review: 'ℹ️ 按 --require-review 要求,历史 Spec 本体保留为待审核候选',
355
+ };
356
+ console.log(messages[resolved.status]);
289
357
  result = archivePackage.materializeCompatibilityPackage({
290
358
  projectRoot: specRoot,
291
359
  archiveDir: stagingDir,
292
360
  changeName,
293
361
  facts: semanticResult.state,
294
- specOntology: useApproved ? approvedOntology : null,
362
+ specOntology: resolved.approved,
363
+ toleratedDiagnostics: legacy.tolerated,
295
364
  packagePath: zipPath,
296
365
  reason: options.reason || '历史 Spec 兼容入库',
297
366
  method: 'kb-upload-legacy-folder',
298
367
  });
299
- console.log(useApproved
368
+ console.log(resolved.approved
300
369
  ? '✓ 已生成审核通过的 LEGACY_MATERIALIZED 发布包'
301
370
  : '✓ 已生成 LEGACY_MATERIALIZED 本体候选包');
302
371
  } else {
@@ -325,6 +394,8 @@ function buildPackageFromFolder(specRoot, changeName, options = {}) {
325
394
  manifest: result.manifest,
326
395
  stagingDir,
327
396
  requiresReview: result.conversion_report?.status === 'materialized_pending_review',
397
+ legacyStatus,
398
+ toleratedDiagnostics: legacyIdentityOnly ? legacy.tolerated : [],
328
399
  };
329
400
  } catch (err) {
330
401
  // 出错时清理 staging
@@ -365,6 +436,7 @@ async function main() {
365
436
  console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> [--minimum-level=propose-spec] [--validate-only]');
366
437
  console.error(' node skywalk-sdd/kb-upload.cjs --package=<zip-path>');
367
438
  console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --project=<spec-root> --change=<name>');
439
+ console.error(' node skywalk-sdd/kb-upload.cjs --folder=<path> --require-review # 历史产物只上传候选,不自动确认本体');
368
440
  process.exit(1);
369
441
  }
370
442
 
@@ -448,12 +520,17 @@ async function main() {
448
520
  markPending: true,
449
521
  });
450
522
  if (!semanticResult.valid) {
451
- const errors = semanticResult.diagnostics
452
- .filter(d => d.severity === 'error')
453
- .map(d => `${d.code}: ${d.message}`);
454
- throw new Error(`语义校验失败:\n${errors.join('\n')}`);
523
+ const { classifyLegacyDiagnostics } = require('./ontology/spec-ontology.cjs');
524
+ const legacy = classifyLegacyDiagnostics(semanticResult.diagnostics);
525
+ if (!legacy.legacyCompatible) {
526
+ const errors = legacy.blocking.map(d => `${d.code}: ${d.message}`);
527
+ throw new Error(`语义校验失败:\n${errors.join('\n')}`);
528
+ }
529
+ console.log('⚠️ 历史产物缺少身份标识,正式上传时将走 LEGACY_MATERIALIZED 旁路物化,不改写 Markdown');
530
+ for (const item of legacy.tolerated) console.log(` ↳ 兼容放行 ${item.code}: ${item.message}`);
531
+ } else {
532
+ console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
455
533
  }
456
- console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
457
534
  console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
458
535
  return;
459
536
  }
@@ -473,8 +550,9 @@ async function main() {
473
550
 
474
551
  // 5. 构建 Archive Package
475
552
  console.log('');
476
- const { zipPath, manifest, stagingDir, requiresReview } = buildPackageFromFolder(specRoot, changeName, {
553
+ const { zipPath, manifest, stagingDir, requiresReview, legacyStatus } = buildPackageFromFolder(specRoot, changeName, {
477
554
  reason: args.reason,
555
+ requireReview: Boolean(args['require-review']),
478
556
  });
479
557
 
480
558
  try {
@@ -546,7 +624,11 @@ async function main() {
546
624
  if (publicationStatus === 'needs_review') {
547
625
  console.log('\n⚠️ 历史 Spec 已安全保存为候选,尚未进入 current 召回');
548
626
  console.log(` → 审核 ${path.join(folderPath, 'ontology', 'spec-ontology.json')}`);
549
- console.log(' → 运行 approve-spec-ontology 后再次上传即可发布');
627
+ if (legacyStatus === 'needs_review') {
628
+ console.log(' → 正文没有足够的结构化事实,只能作为证据保留');
629
+ } else {
630
+ console.log(' → 运行 approve-spec-ontology 后再次上传即可发布;不带 --require-review 上传则自动确认');
631
+ }
550
632
  } else {
551
633
  // Published commit has been verified; a receipt does not imply projection completion.
552
634
  updateKbStateCache(specRoot, changeName, archiveId, contentHash, cacheContext);
@@ -9,6 +9,7 @@ const {
9
9
  SPEC_ONTOLOGY_PATH,
10
10
  buildSpecOntology,
11
11
  validateSpecOntology,
12
+ specOntologyContentSignature,
12
13
  } = require('./spec-ontology.cjs');
13
14
  const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
14
15
  const {
@@ -708,14 +709,7 @@ function materializeCompatibilityPackage(options = {}) {
708
709
  compatibilityMode: 'LEGACY_MATERIALIZED',
709
710
  reviewStatus: 'pending',
710
711
  });
711
- const entityContentSignature = (payload) => (payload.entities || [])
712
- .map((entity) => `${entity.anchor_id}:${entity.content_hash}`)
713
- .sort();
714
- const relationContentSignature = (payload) => (payload.relations || [])
715
- .map((relation) => `${relation.type}:${relation.source?.anchor_id || ''}:${relation.source?.content_hash || ''}`)
716
- .sort();
717
- if (JSON.stringify(entityContentSignature(specOntology)) !== JSON.stringify(entityContentSignature(currentOntology))
718
- || JSON.stringify(relationContentSignature(specOntology)) !== JSON.stringify(relationContentSignature(currentOntology))) {
712
+ if (specOntologyContentSignature(specOntology) !== specOntologyContentSignature(currentOntology)) {
719
713
  throw new Error('已审核 Spec 本体与当前 Markdown 内容不一致,请重新物化并审核');
720
714
  }
721
715
  specOntology.archive_id = archiveId;
@@ -735,6 +729,8 @@ function materializeCompatibilityPackage(options = {}) {
735
729
  if (!validation.valid) {
736
730
  throw new Error(`历史 Spec 本体物化失败: ${validation.errors.join('; ')}`);
737
731
  }
732
+ const toleratedDiagnostics = (options.toleratedDiagnostics || [])
733
+ .map((item) => `LEGACY_DIAGNOSTIC_TOLERATED: ${item.code}: ${item.message}`);
738
734
  const specOntologyPath = path.join(archiveDir, SPEC_ONTOLOGY_PATH);
739
735
  const specOntologyBytes = writeJsonAtomic(specOntologyPath, specOntology);
740
736
  const conversionReport = {
@@ -748,7 +744,7 @@ function materializeCompatibilityPackage(options = {}) {
748
744
  status: specOntology.publication_eligible ? 'materialized_reviewed' : 'materialized_pending_review',
749
745
  entity_count: specOntology.entities.length,
750
746
  relation_count: specOntology.relations.length,
751
- warnings: specOntology.warnings,
747
+ warnings: [...new Set([...(specOntology.warnings || []), ...toleratedDiagnostics])],
752
748
  };
753
749
  writeJsonAtomic(path.join(archiveDir, CONVERSION_REPORT_PATH), conversionReport);
754
750
 
@@ -8,6 +8,8 @@ const { normalizeFacts } = require('./normalizer.cjs');
8
8
  const { resolveChangeDir } = require('./runtime.cjs');
9
9
  const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
10
10
  const { ensureDir, readJsonSafe, writeJsonAtomic } = require('../lib/shared.cjs');
11
+ const { DIAGNOSTIC_CODES } = require('./schema.cjs');
12
+ const { isValidUuid } = require('./id.cjs');
11
13
 
12
14
  const SPEC_ONTOLOGY_SCHEMA_VERSION = 'kld-sdd-spec-ontology/v1';
13
15
  const LEGACY_MAPPING_SCHEMA_VERSION = 'kld-sdd-legacy-identity-mapping/v1';
@@ -32,10 +34,92 @@ const RELATION_MAP = Object.freeze({
32
34
  constrains: 'constrainedBy',
33
35
  });
34
36
 
37
+ // 工具升级后新增的身份、归档和任务校验。历史稿上传遇到这些诊断时旁路物化,
38
+ // 由上传流程自动消解,不把整包失败交回给使用者。
39
+ const LEGACY_TOLERATED_CODES = Object.freeze([
40
+ DIAGNOSTIC_CODES.UUID_MISSING,
41
+ DIAGNOSTIC_CODES.UUID_INVALID,
42
+ DIAGNOSTIC_CODES.ID_MISSING,
43
+ DIAGNOSTIC_CODES.ID_INVALID,
44
+ DIAGNOSTIC_CODES.ID_DUPLICATE,
45
+ DIAGNOSTIC_CODES.DELTA_STATE_INVALID,
46
+ DIAGNOSTIC_CODES.SOURCE_MISSING,
47
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
48
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
49
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
50
+ DIAGNOSTIC_CODES.INHERITED_SOURCE_MISMATCH,
51
+ DIAGNOSTIC_CODES.INHERITED_RELATION_MISSING,
52
+ DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
53
+ DIAGNOSTIC_CODES.ARCHIVE_REVISION_MISMATCH,
54
+ DIAGNOSTIC_CODES.TASK_DEPENDENCY_CYCLE,
55
+ DIAGNOSTIC_CODES.TASK_DEPENDENCY_LAYER_GAP,
56
+ DIAGNOSTIC_CODES.TASK_DEPENDENCY_MISSING_EDGE,
57
+ DIAGNOSTIC_CODES.STMT_WITHOUT_AC,
58
+ DIAGNOSTIC_CODES.STMT_WITHOUT_DESIGN,
59
+ DIAGNOSTIC_CODES.DESIGN_WITHOUT_TASK,
60
+ DIAGNOSTIC_CODES.AC_WITHOUT_STMT,
61
+ DIAGNOSTIC_CODES.TASK_WITHOUT_UPSTREAM,
62
+ ]);
63
+
64
+ // Check 对用户隐藏并自行处理的工具诊断。需求、设计、任务之间的覆盖缺口不在此列。
65
+ const CHECK_TOOL_CODES = Object.freeze([
66
+ DIAGNOSTIC_CODES.UUID_MISSING,
67
+ DIAGNOSTIC_CODES.UUID_INVALID,
68
+ DIAGNOSTIC_CODES.ID_MISSING,
69
+ DIAGNOSTIC_CODES.ID_INVALID,
70
+ DIAGNOSTIC_CODES.ID_DUPLICATE,
71
+ DIAGNOSTIC_CODES.DELTA_STATE_INVALID,
72
+ DIAGNOSTIC_CODES.SOURCE_MISSING,
73
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
74
+ DIAGNOSTIC_CODES.ENTITY_IDENTITY_CONFLICT,
75
+ DIAGNOSTIC_CODES.VERSION_IDENTITY_CONFLICT,
76
+ DIAGNOSTIC_CODES.INHERITED_SOURCE_MISMATCH,
77
+ DIAGNOSTIC_CODES.INHERITED_RELATION_MISSING,
78
+ DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
79
+ DIAGNOSTIC_CODES.ARCHIVE_REVISION_MISMATCH,
80
+ ]);
81
+
35
82
  function sha256(value) {
36
83
  return crypto.createHash('sha256').update(value).digest('hex');
37
84
  }
38
85
 
86
+ function splitCheckDiagnostics(diagnostics = []) {
87
+ const tool = [];
88
+ const product = [];
89
+ for (const item of diagnostics || []) {
90
+ if (item && item.severity === 'error' && CHECK_TOOL_CODES.includes(item.code)) tool.push(item);
91
+ else product.push(item);
92
+ }
93
+ return {
94
+ tool_diagnostics: tool,
95
+ product_diagnostics: product,
96
+ product_valid: product.every((item) => !item || item.severity !== 'error'),
97
+ };
98
+ }
99
+
100
+ function classifyLegacyDiagnostics(diagnostics = []) {
101
+ const blocking = (diagnostics || []).filter((item) => item && item.severity === 'error');
102
+ const tolerated = blocking.filter((item) => LEGACY_TOLERATED_CODES.includes(item.code));
103
+ const rejected = blocking.filter((item) => !LEGACY_TOLERATED_CODES.includes(item.code));
104
+ return {
105
+ blocking,
106
+ tolerated,
107
+ rejected,
108
+ legacyCompatible: blocking.length > 0 && rejected.length === 0,
109
+ };
110
+ }
111
+
112
+ // 同一份 Markdown 反复物化时用于判断“已审核本体是否仍对应当前正文”。
113
+ function specOntologyContentSignature(payload) {
114
+ const entities = (payload?.entities || [])
115
+ .map((entity) => `${entity.anchor_id}:${entity.content_hash}`)
116
+ .sort();
117
+ const relations = (payload?.relations || [])
118
+ .map((relation) => `${relation.type}:${relation.source?.anchor_id || ''}:${relation.source?.content_hash || ''}`)
119
+ .sort();
120
+ return JSON.stringify({ entities, relations });
121
+ }
122
+
39
123
  function withOntologyHash(payload) {
40
124
  const result = { ...payload };
41
125
  delete result.ontology_hash;
@@ -91,16 +175,25 @@ function compatibilityMode(facts, selectedEntities) {
91
175
  return 'LEGACY_MATERIALIZED';
92
176
  }
93
177
 
94
- function identityFor(entity, projectId, semanticKey) {
178
+ function identityFor(entity, projectId, semanticKey, options = {}) {
95
179
  const declaredEntityId = normalizedText(entity.entity_id).toLowerCase();
96
180
  const declaredVersionId = normalizedText(entity.entity_version_id || entity.version_id).toLowerCase();
97
- const entityId = declaredEntityId || deterministicUuid(`${projectId}\n${semanticKey}`);
181
+ const declaredUsable = isValidUuid(declaredEntityId) && isValidUuid(declaredVersionId);
98
182
  const contentHash = normalizedText(entity.content_hash) || `sha256:${sha256(JSON.stringify(entity))}`;
99
- const entityVersionId = declaredVersionId || deterministicUuid(`${entityId}\n${contentHash}`);
183
+ if (!options.repairLegacy || declaredUsable) {
184
+ return {
185
+ entity_id: declaredEntityId || deterministicUuid(`${projectId}\n${semanticKey}`),
186
+ entity_version_id: declaredVersionId || deterministicUuid(`${declaredEntityId || semanticKey}\n${contentHash}`),
187
+ identity_origin: declaredUsable ? 'declared' : 'derived_legacy',
188
+ repaired: false,
189
+ };
190
+ }
100
191
  return {
101
- entity_id: entityId,
102
- entity_version_id: entityVersionId,
103
- identity_origin: declaredEntityId && declaredVersionId ? 'declared' : 'derived_legacy',
192
+ entity_id: deterministicUuid(`${projectId}\n${semanticKey}`),
193
+ entity_version_id: deterministicUuid(`${projectId}\n${semanticKey}\n${contentHash}`),
194
+ identity_origin: 'derived_legacy',
195
+ repaired: true,
196
+ repair_reason: declaredEntityId || declaredVersionId ? 'invalid_declared_uuid' : 'missing_uuid',
104
197
  };
105
198
  }
106
199
 
@@ -124,12 +217,36 @@ function buildSpecOntology(facts, options = {}) {
124
217
  const baseKey = baseKeys[index];
125
218
  const source = normalizeSource(sourceEntity.source);
126
219
  const semanticKey = keyCounts.get(baseKey) > 1
127
- ? `${baseKey}@${normalizedText(source.file).toLowerCase() || index + 1}`
220
+ ? `${baseKey}@${normalizedText(source.file).toLowerCase() || 'source'}#${index + 1}`
128
221
  : baseKey;
129
222
  if (keyCounts.get(baseKey) > 1) {
130
- warnings.push(`SEMANTIC_KEY_COLLISION: ${baseKey} 已按来源路径拆分,需要人工判定是否合并`);
223
+ warnings.push(`LEGACY_DUPLICATE_ANCHOR: ${baseKey} 已按来源路径拆成独立事实,上传不再等待人工合并`);
224
+ }
225
+ const repairLegacy = mode === 'LEGACY_MATERIALIZED';
226
+ let identity = identityFor(sourceEntity, projectId, semanticKey, { repairLegacy });
227
+ if (repairLegacy && entities.some((item) => item.entity_id === identity.entity_id)) {
228
+ identity = {
229
+ entity_id: deterministicUuid(`${projectId}\n${semanticKey}\n${index}`),
230
+ entity_version_id: deterministicUuid(`${projectId}\n${semanticKey}\n${index}\nversion`),
231
+ identity_origin: 'derived_legacy',
232
+ repaired: true,
233
+ repair_reason: 'entity_id_conflict',
234
+ };
131
235
  }
132
- const identity = identityFor(sourceEntity, projectId, semanticKey);
236
+ if (repairLegacy && entities.some((item) => item.entity_version_id === identity.entity_version_id)) {
237
+ identity = {
238
+ ...identity,
239
+ entity_version_id: deterministicUuid(`${identity.entity_id}\n${semanticKey}\n${index}\nversion`),
240
+ identity_origin: 'derived_legacy',
241
+ repaired: true,
242
+ repair_reason: identity.repair_reason || 'version_id_conflict',
243
+ };
244
+ }
245
+ if (identity.repaired) {
246
+ warnings.push(`LEGACY_IDENTITY_REPAIRED: ${anchor} ${identity.repair_reason}`);
247
+ }
248
+ const declaredPredecessor = normalizedText(sourceEntity.predecessor_version_id);
249
+ const predecessor = repairLegacy || !isValidUuid(declaredPredecessor) ? null : declaredPredecessor;
133
250
  const ontologyEntity = {
134
251
  semantic_key: semanticKey,
135
252
  type: TYPE_MAP[sourceEntity.type],
@@ -141,8 +258,10 @@ function buildSpecOntology(facts, options = {}) {
141
258
  || anchor,
142
259
  ),
143
260
  anchor_id: anchor,
144
- ...identity,
145
- predecessor_version_id: sourceEntity.predecessor_version_id || null,
261
+ entity_id: identity.entity_id,
262
+ entity_version_id: identity.entity_version_id,
263
+ identity_origin: identity.identity_origin,
264
+ predecessor_version_id: predecessor,
146
265
  content_hash: normalizedText(sourceEntity.content_hash),
147
266
  review_status: reviewStatus,
148
267
  publication_status: reviewStatus === 'confirmed' ? 'publishable' : 'candidate',
@@ -375,8 +494,12 @@ module.exports = {
375
494
  SPEC_ONTOLOGY_PATH,
376
495
  LEGACY_MAPPING_PATH,
377
496
  TYPE_MAP,
497
+ LEGACY_TOLERATED_CODES,
378
498
  deterministicUuid,
379
499
  semanticKeyFor,
500
+ classifyLegacyDiagnostics,
501
+ splitCheckDiagnostics,
502
+ specOntologyContentSignature,
380
503
  buildSpecOntology,
381
504
  validateSpecOntology,
382
505
  approveSpecOntologyFile,
@@ -47,14 +47,17 @@ node "<spec-package>/skywalk-sdd/log.cjs" start --command=check --project="<spec
47
47
  node "<spec-package>/skywalk-sdd/log.cjs" semantic-check --project="<spec-package>" --change=<变更名称> --profile=auto --include-tasks > "<spec-package>/skywalk-sdd/state/<变更名称>-preflight.json"
48
48
  ```
49
49
 
50
- 输出 `profile / valid / revision / counts / diagnostics`、`review_inputs`(文档路径与哈希、历史身份哈希)、`task_completion` 和 `timings_ms`。程序时间不等于模型评审总耗时。
50
+ 输出 `profile / valid / product_valid / revision / counts`、`product_diagnostics`、`tool_diagnostics`、`review_inputs`、`task_completion` 和 `timings_ms`。程序时间不等于模型评审总耗时。
51
51
 
52
- - semantic-check 已包含全量对账、工作态 JSON 原子刷新和 pending 标记;无需再调用 semantic-reconcile。pending 仅表示程序校验通过,不能宣称已完成独立评审或已入库。
52
+ - `product_valid` 只看产物质量。需求没有验收、需求没有设计、设计没有任务、任务没有上游、验收没有归属需求、产物缺失、任务依赖成环,这些进入报告并阻断 Check。
53
+ - `tool_diagnostics` 是工具升级带来的身份、来源、谱系和归档诊断。预检已把可物化的部分写入 `ontology/spec-ontology.json` 与 `ontology/legacy-mapping.json`,不改 Markdown。不要向用户展示这些诊断,不要要求用户补 UUID、改锚点或重做归档。
54
+ - 交给独立评审者的程序诊断只使用 `product_diagnostics`。报告只写产物缺口和 design 与 tasks 之间的覆盖缺失。
55
+ - semantic-check 已包含全量对账、工作态 JSON 原子刷新和 pending 标记;无需再调用 semantic-reconcile。`product_valid=true` 只表示产物结构没有阻断,不能宣称已完成独立评审或已入库。
53
56
  - 默认从产物推断 simple/full;用户或项目明确要求 strict 时传 `--profile=strict`,不能为通过而降级。
54
- - `simple` 阻断 STMT 无 AC,未生成 design/tasks 时跳过对应链路;`full` 阻断 STMT→AC→Design→Task 断链;`strict` 还要求完整来源字段。
55
- - 所有模式保留身份合法性、重复锚点、跨 Change/Archive 身份误复用、版本谱系、悬空引用、domain/range 和依赖成环门禁。added 使用新身份;modified/removed 复用实体并指向直接前驱;unchanged 引用历史版本且不复制正文。
56
- - Continuity、external_ref 与 `ontology/continuity-resolution.json` 必须一致。`CONTINUITY_DECISION_REQUIRED` / `CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT` 和编号诊断原样进入报告;不得临时改身份来绕过门禁。
57
- - 文件观察结果只作提示,不复用历史“通过”代替本轮全量校验。命令失败、诊断 error 或 valid=false 时先展示具体问题,在授权内修复后重试;无需模型再读全仓来重复确认确定性错误。若缺业务决定则本轮结束为 failure、说明“预检阻断,尚未独立评审”,保留阶段日志,不伪造 check_result。用户明确要求同时审阅其余问题时可继续独立评审,但总体仍失败。
57
+ - `simple` 阻断 STMT 无 AC,未生成 design/tasks 时跳过对应链路;`full` 阻断 STMT→AC→Design→Task 断链;`strict` 还要求完整来源字段,来源缺失按工具诊断自行旁路处理,不单独向用户追问。
58
+ - 产物门禁保留追溯断链、悬空业务引用、domain/range 和任务依赖成环。身份、重复锚点、归档版本和谱系由工具旁路处理。
59
+ - Continuity、external_ref 与 `ontology/continuity-resolution.json` 仍属于产物决定。`CONTINUITY_DECISION_REQUIRED` / `CONTINUITY_IDENTITY_MISMATCH` / `EXTERNAL_REF_CONFLICT` 进入报告;不得临时改身份来绕过这些业务门禁。
60
+ - 只有 `product_diagnostics` 里的 error 才向用户展示并在授权内修复后重试。仅有工具诊断时继续独立评审,不把本轮记成预检失败。缺业务决定时本轮结束为 failure,说明“预检阻断,尚未独立评审”。
58
61
 
59
62
  ### 多仓库关联配置
60
63
 
@@ -75,7 +78,7 @@ node "<spec-package>/skywalk-sdd/log.cjs" semantic-check --project="<spec-packag
75
78
 
76
79
  ## 3. 一次独立评审
77
80
 
78
- 程序预检后交给评审者:`review_inputs` 对应的最终文件路径、profile/test-strategy、任务状态、程序诊断、用户指定的上下文路径、检查范围和作者会话 ID。只传客观输入,不传作者对话或主 Agent 的结论摘要。主 Agent 从 proposal 元数据确定测试策略即可,无需预读正文。
81
+ 程序预检后交给评审者:`review_inputs` 对应的最终文件路径、profile/test-strategy、任务状态、`product_diagnostics`、用户指定的上下文路径、检查范围和作者会话 ID。不传 `tool_diagnostics`,不传作者对话或主 Agent 的结论摘要。评审重点是产物是否完整,以及 spec、design、tasks 之间有没有覆盖缺失。
79
82
 
80
83
  ${CHECK_REVIEW_DELEGATION_GUIDANCE}
81
84
 
@@ -105,6 +108,6 @@ node "<spec-package>/skywalk-sdd/log.cjs" check-record --project="<spec-package>
105
108
 
106
109
  check-record 按预检的同一 profile 重新核验正文、文件集合和历史身份。输入变化返回 CHECK_REVIEW_STALE,不写本轮通过记录;重新预检并复核受影响内容。成功时仍通过原 strict 校验和阶段结束入口,不另手动执行 end。评审输入始终保留,记录失败可以修复后重试。未真实开展评审时不用此命令,按原 end 命令记录 failure 和原因。
107
110
 
108
- 报告输出五维结论、多仓配置、KB 状态、证据与未执行项,说明覆盖范围。只请求检查则结束;已授权实现且无阻断项时进入 apply。修复文档后再检查相关依赖,禁止携带旧结论直接通过;不把 Capability 局部评审写成全 Change 通过。
111
+ 报告输出五维结论、多仓配置、KB 状态、产物证据与未执行项。只写需求、设计、任务之间的缺口和质量问题。身份、UUID、归档版本、重复锚点等工具处理结果不写入给用户的报告。只请求检查则结束;已授权实现且没有产物阻断项时进入 apply。修复文档后再检查相关依赖,禁止携带旧结论直接通过;不把 Capability 局部评审写成全 Change 通过。
109
112
 
110
113
  [checklist.md](checklist.md) 仅供日志异常/自检时读取,正常路径不再完整加载一遍。
@@ -7,7 +7,8 @@
7
7
 
8
8
  - 自己读取完整的相关业务文档正文一次;批量读取相互独立的文件,同一文件同一版本复用,输出截断时补读缺失部分。
9
9
  - Simple 支持根目录 `spec.md/design.md/tasks.md`;Full 使用 `specs/<capability>/`。兼容 `propose.md/task.md`。
10
- - 程序已核验的 UUID、谱系、引用和 DAG 结果直接引用;模型负责语义覆盖、业务矛盾、可行性和测试质量,不能把“结构合法”当作“设计正确”。
10
+ - 程序已核验结构。身份、UUID、谱系和归档诊断由主 Agent 自行处理,不写入给用户的评审结论。
11
+ - 模型负责产物质量:语义覆盖、design 是否覆盖 spec、tasks 是否拆解 design、业务矛盾、可行性和测试质量。不能把“结构合法”当作“设计正确”,也不能用工具诊断代替这些缺口。
11
12
  - 代码只读 design/tasks 实际引用的锚点及其必要依赖;不做全仓 diff、不运行测试。用户提供的上下文文件必须读取。
12
13
  - 仅指定 Capability 时,补读 proposal 和明确的跨能力依赖;报告覆盖边界,不能宣称全 Change 已独立评审。全 Change 的程序语义门禁仍执行。
13
14
  - 不适用的算法、TDD 或实施后检查写 `N/A` 和理由,不能计成通过;仅在 test-strategy=tdd 时加载下面 TDD 资料。
@@ -80,7 +80,7 @@ curl -sS -X POST "$API/v1/spaces/$SPACE_ID/knowledge-bases/$KB_ID/ingestions/$JO
80
80
  上传的 zip 包必须满足以下条件(否则入库会失败):
81
81
 
82
82
  - 当前产物包含:`archive-manifest.json`、`canonical-facts.json`、`spec-ontology.json`、`conversion-report.json`
83
- - 历史兼容包可不带 `canonical-facts.json`,但必须带 `spec-ontology.json`,且 `compatibility_mode=LEGACY_MATERIALIZED`、`review_status=pending`
83
+ - 历史兼容包可不带 `canonical-facts.json`,但必须带 `spec-ontology.json`,且 `compatibility_mode=LEGACY_MATERIALIZED`。重复锚点、非法 UUID、身份冲突、依赖成环、归档版本不一致由上传自动消解;无结构化事实时才是 `pending`。不要让用户处理这些历史诊断。
84
84
  - `canonical-facts.json` 支持 `kld-sdd-canonical-facts/v1` 与 **`v2`**(v2 可选实体级 `external_refs`)
85
85
  - 所有文件需通过 SHA-256 校验(manifest 中记录每个文件的 `content_hash`)
86
86
  - 允许的文件类型:`.md`、`.json`、`.jsonl`、`.yaml`、`.yml`
@@ -22,9 +22,10 @@ allowed-tools:
22
22
 
23
23
  ## 始终遵守
24
24
 
25
- - 不改写历史 Markdown。缺少 `entity-id` / `version-id` 时,身份写入 `ontology/legacy-mapping.json`。
26
- - 兼容转换不因缺少旧标识失败。无法确定的合并只生成 candidate,不自动发布为 current。
27
- - 正式发布仍校验证据、哈希、版本和租户边界。
25
+ - 不改写历史 Markdown。缺少、非法或互相冲突的身份由上传写进 `ontology/legacy-mapping.json`,并生成稳定 UUID。
26
+ - 重复锚点、非法 UUID、身份冲突、任务依赖成环、归档版本对不上,都由上传自动消解并继续发布。不要停下来请用户处理,也不要改 Markdown 去“修”这些历史诊断。
27
+ - `opsx-check` 仍会报告这些问题。入库和召回不依赖 Check 通过。
28
+ - 正式发布仍校验证据、哈希、版本和租户边界。正文没有结构化事实时只入证据,不发布本体事实。
28
29
  - 当前 `canonical-facts/v2` 与 `spec-ontology/v1` 同包产出,支持新旧知识库过渡。
29
30
 
30
31
  ## 执行流程
@@ -48,28 +49,29 @@ node skywalk-sdd/ontology/cli.cjs validate-spec-ontology \
48
49
  --file=openspec/changes/<change-key>/ontology/spec-ontology.json
49
50
  ```
50
51
 
51
- 5. 若有 `SEMANTIC_KEY_COLLISION` 或候选合并,展示锚点、来源和建议,让用户只审核这些差异。历史产物默认可以先上传为 candidate,不会进入 current 召回。
52
- 6. 审核语义键、身份映射、实体和关系无误后,显式确认旁路本体:
52
+ 5. 上传本身会自动物化并确认。重复锚点按来源拆开,非法或冲突的 UUID 改成稳定派生身份,依赖成环和归档版本不一致只记入转换说明。不要向用户复述这些诊断,不要要求用户选择合并或补 UUID。完成后直接入库并用 spec-generation 召回验证。
53
+ 6. 仅当用户明确要求先审阅,或正文没有任何结构化事实时,才保留 candidate。此时再运行:
53
54
 
54
55
  ```bash
55
56
  node skywalk-sdd/ontology/cli.cjs approve-spec-ontology \
56
57
  --file=openspec/changes/<change-key>/ontology/spec-ontology.json
57
58
  ```
58
59
 
59
- 有 semantic key 冲突时,此命令会拒绝通过;先修正本体 JSON 中的实体、关系和 `identity_map`,再重新校验与确认。不要通过 `--allow-collisions` 跳过真实冲突。
60
- 7. 通过 `opsx-kb-ingest` 打包上传。上传脚本会复核已确认本体与当前 Markdown 的事实哈希;内容已变化时拒绝发布,并要求重新物化和审核。
61
- 8. 通过 `context-client.cjs --mode=spec-generation` 执行一次召回验证,报告是否命中能力、场景、规则和 Spec 模式。
60
+ 用户没有要求审阅时不要执行这一步。上传已经自动确认,不要因为重复锚点或身份改写再停下来。
61
+ 7. 通过 `opsx-kb-ingest` 打包上传。正文变化时重新物化并自动确认。已确认本体与当前 Markdown 不一致时不沿用旧确认。
62
+ 8. 通过 `context-client.cjs --mode=spec-generation` 执行一次召回验证。向用户只报告是否入库、是否召回到 current 事实。自动改写过的身份、拆开的重复锚点、忽略的成环和归档差异写在命令输出里,不单独向用户确认。
62
63
 
63
- ## 历史产物审核输出
64
+ ## 完成后的简报
64
65
 
65
- 只展示需要人判断的项:
66
+ 只向用户说明入库和召回结果。下面这些由工具自动处理,不列入需要用户决定的事项:
66
67
 
67
68
  ```text
68
69
  兼容模式:LEGACY_MATERIALIZED
69
- 可直接物化:12
70
- 需审核合并:2
71
- 仅证据:1
72
- 原文改动:0
70
+ 已发布事实:12
71
+ 自动改写身份:3
72
+ 重复锚点已拆分:1
73
+ 归档/成环已忽略:2
74
+ 仅证据:0
73
75
  ```
74
76
 
75
77
  若 KB 不可用,保留已物化产物并标记 `remoteContextVerified=false`;本地编写可继续,入库与发布状态明确为未完成。