kld-sdd 2.6.12 → 2.6.14

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 (91) hide show
  1. package/README.md +29 -7
  2. package/kld-sdd-guide.html +428 -159
  3. package/lib/init.js +37 -7
  4. package/package.json +5 -2
  5. package/skywalk-sdd/index.cjs +2668 -415
  6. package/skywalk-sdd/metrics-v3.cjs +1153 -0
  7. package/skywalk-sdd/ontology/archive-package.cjs +120 -3
  8. package/skywalk-sdd/ontology/identity-index.cjs +9 -2
  9. package/skywalk-sdd/ontology/ontology-paths.cjs +73 -0
  10. package/skywalk-sdd/ontology/resolve-spec-root.cjs +20 -80
  11. package/skywalk-sdd/ontology/runtime.cjs +32 -22
  12. package/skywalk-sdd/ontology/structural-identity.cjs +11 -2
  13. package/skywalk-sdd/ontology/traceability-validator.cjs +16 -8
  14. package/skywalk-sdd/ontology/working-artifacts.cjs +4 -2
  15. package/skywalk-sdd/reporting/change-report-model.cjs +309 -0
  16. package/skywalk-sdd/reporting/change-report-renderer.cjs +425 -0
  17. package/skywalk-sdd/runtime-metadata.cjs +21 -0
  18. package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +39 -9
  19. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +87 -8
  20. package/templates/hooks/claude/hooks/sdd-prompt.cjs +3 -1
  21. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +39 -9
  22. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +87 -9
  23. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +3 -1
  24. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +14 -13
  25. package/templates/skills/kld-sdd/opsx-apply/checklist.md +20 -20
  26. package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +12 -12
  27. package/templates/skills/kld-sdd/opsx-apply/reference.md +32 -31
  28. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +44 -6
  29. package/templates/skills/kld-sdd/opsx-archive/checklist.md +5 -2
  30. package/templates/skills/kld-sdd/opsx-check/SKILL.md +21 -13
  31. package/templates/skills/kld-sdd/opsx-check/checklist.md +8 -7
  32. package/templates/skills/kld-sdd/opsx-design/SKILL.md +1 -1
  33. package/templates/skills/kld-sdd/opsx-design/checklist.md +1 -1
  34. package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +14 -0
  35. package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +31 -0
  36. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +12 -4
  37. package/templates/skills/kld-sdd/opsx-propose/reference.md +29 -3
  38. package/templates/skills/kld-sdd/opsx-rules/reference.md +3 -1
  39. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +2 -2
  40. package/templates/skills/kld-sdd/opsx-spec/checklist.md +1 -1
  41. package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -10
  42. package/templates/skills/kld-sdd/opsx-task/checklist.md +3 -3
  43. package/templates/skills/kld-sdd/opsx-task/reference.md +3 -3
  44. package/templates/skills/kld-sdd/opsx-test/SKILL.md +7 -5
  45. package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/SKILL.md +4 -4
  46. package/templates/skills/kld-sdd/{opsx-tdd-anti-patterns → tdd-anti-patterns}/reference.md +4 -4
  47. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/SKILL.md +13 -13
  48. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/checklist.md +5 -5
  49. package/templates/skills/kld-sdd/{opsx-tdd-core → tdd-core}/reference.md +12 -12
  50. package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/SKILL.md +3 -3
  51. package/templates/skills/kld-sdd/{opsx-tdd-metrics → tdd-metrics}/checklist.md +2 -2
  52. package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/SKILL.md +2 -2
  53. package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/parameterized-testing.md +1 -1
  54. package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/SKILL.md +5 -5
  55. package/templates/skills/kld-sdd/{opsx-tdd-review → tdd-review}/checklist.md +5 -5
  56. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/SKILL.md +3 -3
  57. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/non-tdd-modules.md +1 -1
  58. package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/refactor-checklist.md +1 -1
  59. package/templates/skills/kld-sdd/tdd-rules/rules/test-skeleton-telemetry.md +19 -0
  60. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +0 -19
  61. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/cause-effect-clarity.md +0 -0
  62. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/clean-test-data.md +0 -0
  63. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/existing-test-awareness.md +0 -0
  64. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/given-when-then.md +0 -0
  65. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/good-test-qualities.md +0 -0
  66. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/mock-boundary.md +0 -0
  67. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/naming-conventions.md +0 -0
  68. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/no-logic-in-tests.md +0 -0
  69. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/one-test-one-scenario.md +0 -0
  70. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/prefer-public-apis.md +0 -0
  71. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/general/test-behaviors-not-methods.md +0 -0
  72. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/argument-matching.md +0 -0
  73. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/controller-test-rules.md +0 -0
  74. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/domain-service-rules.md +0 -0
  75. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/java-test-template.md +0 -0
  76. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/json-serialization.md +0 -0
  77. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/java/logging-rules.md +0 -0
  78. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/compilation-verification.md +0 -0
  79. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/post-generation/execution-verification.md +0 -0
  80. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/python/py-test-template.md +0 -0
  81. /package/templates/skills/kld-sdd/{opsx-tdd-quality → tdd-quality}/rules/typescript/ts-test-template.md +0 -0
  82. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/controller-strategy.md +0 -0
  83. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/dag-generation-rules.md +0 -0
  84. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/des-step-annotation.md +0 -0
  85. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/exception-path-coverage.md +0 -0
  86. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-scope-declaration.md +0 -0
  87. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/green-yagni-fence.md +0 -0
  88. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/multi-validation-split.md +0 -0
  89. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/task-type-definitions.md +0 -0
  90. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/tdd-strategy-selection.md +0 -0
  91. /package/templates/skills/kld-sdd/{opsx-tdd-rules → tdd-rules}/rules/test-execution-gate.md +0 -0
@@ -4,9 +4,11 @@ const crypto = require('crypto');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const { generateUuidV7 } = require('./id.cjs');
7
+ const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
7
8
 
8
- const PRODUCER_VERSION = require('../../package.json').version;
9
- const PRODUCER = `kld-sdd@${PRODUCER_VERSION}`;
9
+ const RUNTIME_METADATA = resolveRuntimeMetadata();
10
+ const PRODUCER_VERSION = RUNTIME_METADATA.version;
11
+ const PRODUCER = RUNTIME_METADATA.producer;
10
12
  const MANIFEST_SCHEMA_VERSION = 'kld-sdd-archive-manifest/v2';
11
13
  const CANONICAL_FACTS_SCHEMA_VERSION = 'kld-sdd-canonical-facts/v2';
12
14
  const CONVERSION_REPORT_SCHEMA_VERSION = 'kld-sdd-conversion-report/v1';
@@ -15,7 +17,7 @@ const MANIFEST_PATH = 'archive-manifest.json';
15
17
  const ONTOLOGY_PATH = 'archive-ontology.json';
16
18
  const CANONICAL_FACTS_PATH = 'canonical-facts.json';
17
19
  const CONVERSION_REPORT_PATH = 'conversion-report.json';
18
- const PACKAGE_SUFFIXES = new Set(['.md', '.json', '.jsonl', '.yaml', '.yml']);
20
+ const PACKAGE_SUFFIXES = new Set(['.md', '.html', '.json', '.jsonl', '.yaml', '.yml']);
19
21
 
20
22
  function sha256(content) {
21
23
  return crypto.createHash('sha256').update(content).digest('hex');
@@ -63,10 +65,16 @@ function ensureProjectIdentity(projectRoot) {
63
65
  }
64
66
  return existing;
65
67
  }
68
+ // 兜底:project-identity.json 未通过 opsx-ontology-query 配置 KB 时创建
69
+ // 此时 project_id 为随机值,KB 入库时会因 project_id !== spaceKey 而失败
70
+ console.warn('⚠️ project-identity.json 不存在 — 未通过 opsx-ontology-query 配置 KB。');
71
+ console.warn('⚠️ 将生成临时 project_id,KB 入库时需确保与 Space spaceKey 一致。');
72
+ console.warn('⚠️ 建议在首次使用 opsx-ontology-query 配置 KB 时自动创建正确的 project-identity.json。');
66
73
  const identity = {
67
74
  schema_version: PROJECT_IDENTITY_SCHEMA_VERSION,
68
75
  project_id: `urn:kld:sdd:project:${generateUuidV7()}`,
69
76
  created_at: new Date().toISOString(),
77
+ _warning: 'UNCONFIGURED — 请通过 opsx-ontology-query 配置 KB 后更新 project_id 为 Space spaceKey',
70
78
  };
71
79
  writeJson(identityPath, identity);
72
80
  return identity;
@@ -495,6 +503,77 @@ function writeArchiveZip(archiveDir, packagePath, manifest) {
495
503
  return packagePath;
496
504
  }
497
505
 
506
+ function readStoredZipEntries(packagePath) {
507
+ const bytes = fs.readFileSync(packagePath);
508
+ const entries = new Map();
509
+ let offset = 0;
510
+ while (offset + 4 <= bytes.length && bytes.readUInt32LE(offset) === 0x04034b50) {
511
+ if (offset + 30 > bytes.length) {
512
+ throw new Error('Archive Package ZIP local header 不完整');
513
+ }
514
+ const flags = bytes.readUInt16LE(offset + 6);
515
+ const method = bytes.readUInt16LE(offset + 8);
516
+ const checksum = bytes.readUInt32LE(offset + 14);
517
+ const compressedSize = bytes.readUInt32LE(offset + 18);
518
+ const uncompressedSize = bytes.readUInt32LE(offset + 22);
519
+ const nameLength = bytes.readUInt16LE(offset + 26);
520
+ const extraLength = bytes.readUInt16LE(offset + 28);
521
+ const nameStart = offset + 30;
522
+ const contentStart = nameStart + nameLength + extraLength;
523
+ const contentEnd = contentStart + compressedSize;
524
+ if ((flags & 0x0800) !== 0x0800 || method !== 0 || contentEnd > bytes.length) {
525
+ throw new Error('Archive Package ZIP 只允许完整的 UTF-8 stored entry');
526
+ }
527
+ const name = bytes.subarray(nameStart, nameStart + nameLength).toString('utf8');
528
+ const safeName = safePackagePath(name);
529
+ if (entries.has(safeName)) {
530
+ throw new Error(`Archive Package ZIP 包含重复路径: ${safeName}`);
531
+ }
532
+ const content = bytes.subarray(contentStart, contentEnd);
533
+ if (compressedSize !== uncompressedSize || crc32(content) !== checksum) {
534
+ throw new Error(`Archive Package ZIP entry 校验失败: ${safeName}`);
535
+ }
536
+ entries.set(safeName, content);
537
+ offset = contentEnd;
538
+ }
539
+ if (entries.size === 0 || offset + 4 > bytes.length || bytes.readUInt32LE(offset) !== 0x02014b50) {
540
+ throw new Error('Archive Package ZIP 缺少 central directory');
541
+ }
542
+ return entries;
543
+ }
544
+
545
+ function verifyArchivePackage(archiveDir, packagePath, manifest) {
546
+ const expectedPaths = [MANIFEST_PATH, ...(manifest.files || []).map((item) => safePackagePath(item.path))];
547
+ const expectedUnique = new Set(expectedPaths);
548
+ if (expectedUnique.size !== expectedPaths.length) {
549
+ throw new Error('Archive Package Manifest 包含重复路径');
550
+ }
551
+ const entries = readStoredZipEntries(packagePath);
552
+ const actualPaths = [...entries.keys()];
553
+ if (
554
+ actualPaths.length !== expectedPaths.length
555
+ || expectedPaths.some((entryPath) => !entries.has(entryPath))
556
+ ) {
557
+ throw new Error('Archive Package ZIP 与 Manifest 文件清单不一致');
558
+ }
559
+ const manifestBytes = fs.readFileSync(path.join(archiveDir, MANIFEST_PATH));
560
+ if (!entries.get(MANIFEST_PATH)?.equals(manifestBytes)) {
561
+ throw new Error('Archive Package ZIP 中的 Manifest 不是最终落盘版本');
562
+ }
563
+ for (const item of manifest.files || []) {
564
+ const content = entries.get(item.path);
565
+ const expectedHash = `sha256:${sha256(content)}`;
566
+ if (item.content_hash !== expectedHash) {
567
+ throw new Error(`Archive Package 内容哈希不一致: ${item.path}`);
568
+ }
569
+ }
570
+ return {
571
+ entry_count: entries.size,
572
+ package_hash: `sha256:${sha256(fs.readFileSync(packagePath))}`,
573
+ paths: actualPaths,
574
+ };
575
+ }
576
+
498
577
  function materializeArchivePackage(options = {}) {
499
578
  const projectRoot = path.resolve(options.projectRoot || process.cwd());
500
579
  const archiveDir = path.resolve(requiredText(options.archiveDir, 'archiveDir', 'Archive Package'));
@@ -588,6 +667,41 @@ function materializeArchivePackage(options = {}) {
588
667
  };
589
668
  }
590
669
 
670
+ function finalizeArchivePackageAfterReports(options = {}) {
671
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
672
+ const changeName = requiredText(options.changeName, 'changeName', 'Archive finalizer');
673
+ const runIdentity = requiredText(options.runIdentity, 'runIdentity', 'Archive finalizer');
674
+ const { withChangeLock } = require('./change-lock.cjs');
675
+ return withChangeLock(projectRoot, changeName, options.lockOptions || {}, (lock) => {
676
+ const prepared = typeof options.prepare === 'function'
677
+ ? options.prepare(lock)
678
+ : {};
679
+ const produced = materializeArchivePackage({
680
+ ...options,
681
+ ...prepared,
682
+ projectRoot,
683
+ changeName,
684
+ });
685
+ if (!produced.package_path) {
686
+ throw new Error('Archive finalizer 必须生成 ZIP');
687
+ }
688
+ const verification = verifyArchivePackage(
689
+ path.resolve(prepared.archiveDir || options.archiveDir),
690
+ produced.package_path,
691
+ produced.manifest,
692
+ );
693
+ return {
694
+ ...produced,
695
+ finalizer: {
696
+ run_identity: runIdentity,
697
+ lock_token: lock.token,
698
+ verified: true,
699
+ ...verification,
700
+ },
701
+ };
702
+ });
703
+ }
704
+
591
705
  module.exports = {
592
706
  PRODUCER_VERSION,
593
707
  PRODUCER,
@@ -605,5 +719,8 @@ module.exports = {
605
719
  buildCanonicalFacts,
606
720
  collectPackageFilePaths,
607
721
  writeArchiveZip,
722
+ readStoredZipEntries,
723
+ verifyArchivePackage,
608
724
  materializeArchivePackage,
725
+ finalizeArchivePackageAfterReports,
609
726
  };
@@ -5,6 +5,10 @@ const path = require('path');
5
5
  const { parseChangeArtifacts } = require('./artifact-parser.cjs');
6
6
  const { normalizeFacts } = require('./normalizer.cjs');
7
7
  const { DIAGNOSTIC_CODES } = require('./schema.cjs');
8
+ const {
9
+ ONTOLOGY_STATE_FILES,
10
+ resolveOntologyStateFile,
11
+ } = require('./ontology-paths.cjs');
8
12
 
9
13
  function catalogDiagnostic(code, message, context = {}) {
10
14
  return {
@@ -97,11 +101,14 @@ function validateArchiveCandidate(candidate, facts) {
97
101
  return { eligible: false, snapshot, manifest, diagnostics };
98
102
  }
99
103
 
100
- const artifactIndexPath = path.join(candidate.changeDir, 'artifact-index.json');
104
+ const artifactIndexPath = resolveOntologyStateFile(
105
+ candidate.changeDir,
106
+ ONTOLOGY_STATE_FILES.artifactIndex,
107
+ );
101
108
  if (!fs.existsSync(artifactIndexPath)) {
102
109
  diagnostics.push(catalogDiagnostic(
103
110
  DIAGNOSTIC_CODES.ARCHIVE_NOT_CONFIRMED,
104
- `Archive 缺少 change 目录下的 artifact-index.json: ${candidate.change}`,
111
+ `Archive 缺少 change 目录下的 ontology/artifact-index.json: ${candidate.change}`,
105
112
  { file: artifactIndexPath, suggestion: '通过 KLD-SDD reconcile/archive 在 change 目录生成工作态 JSON 后再归档' },
106
113
  ));
107
114
  return { eligible: false, snapshot, manifest, diagnostics };
@@ -0,0 +1,73 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ONTOLOGY_STATE_DIR = 'ontology';
7
+
8
+ const ONTOLOGY_STATE_FILES = {
9
+ working: 'working-ontology.json',
10
+ diagnostics: 'diagnostics.json',
11
+ fileIndex: 'file-index.json',
12
+ artifactIndex: 'artifact-index.json',
13
+ identities: 'ontology-identities.json',
14
+ continuityResolution: 'continuity-resolution.json',
15
+ };
16
+
17
+ function ontologyStateRelative(basename) {
18
+ return `${ONTOLOGY_STATE_DIR}/${basename}`;
19
+ }
20
+
21
+ function changeOntologyFilePaths(changeDir) {
22
+ const resolved = path.resolve(changeDir);
23
+ const ontologyDir = path.join(resolved, ONTOLOGY_STATE_DIR);
24
+ return {
25
+ changeDir: resolved,
26
+ ontologyDir,
27
+ working: path.join(ontologyDir, ONTOLOGY_STATE_FILES.working),
28
+ diagnostics: path.join(ontologyDir, ONTOLOGY_STATE_FILES.diagnostics),
29
+ fileIndex: path.join(ontologyDir, ONTOLOGY_STATE_FILES.fileIndex),
30
+ artifactIndex: path.join(ontologyDir, ONTOLOGY_STATE_FILES.artifactIndex),
31
+ continuityResolution: path.join(ontologyDir, ONTOLOGY_STATE_FILES.continuityResolution),
32
+ identities: path.join(ontologyDir, ONTOLOGY_STATE_FILES.identities),
33
+ artifactDir: path.join(resolved, 'artifacts'),
34
+ };
35
+ }
36
+
37
+ function resolveOntologyStateFile(changeDir, basename) {
38
+ const resolved = path.resolve(changeDir);
39
+ const modern = path.join(resolved, ONTOLOGY_STATE_DIR, basename);
40
+ if (fs.existsSync(modern)) return modern;
41
+ const legacy = path.join(resolved, basename);
42
+ if (fs.existsSync(legacy)) return legacy;
43
+ return modern;
44
+ }
45
+
46
+ function resolveRevisionBundlePaths(changeDir) {
47
+ const preferred = changeOntologyFilePaths(changeDir);
48
+ return {
49
+ ...preferred,
50
+ working: resolveOntologyStateFile(changeDir, ONTOLOGY_STATE_FILES.working),
51
+ diagnostics: resolveOntologyStateFile(changeDir, ONTOLOGY_STATE_FILES.diagnostics),
52
+ fileIndex: resolveOntologyStateFile(changeDir, ONTOLOGY_STATE_FILES.fileIndex),
53
+ artifactIndex: resolveOntologyStateFile(changeDir, ONTOLOGY_STATE_FILES.artifactIndex),
54
+ };
55
+ }
56
+
57
+ function removeLegacyOntologyStateFiles(changeDir) {
58
+ const resolved = path.resolve(changeDir);
59
+ for (const basename of Object.values(ONTOLOGY_STATE_FILES)) {
60
+ const legacy = path.join(resolved, basename);
61
+ if (fs.existsSync(legacy)) fs.rmSync(legacy, { force: true });
62
+ }
63
+ }
64
+
65
+ module.exports = {
66
+ ONTOLOGY_STATE_DIR,
67
+ ONTOLOGY_STATE_FILES,
68
+ ontologyStateRelative,
69
+ changeOntologyFilePaths,
70
+ resolveOntologyStateFile,
71
+ resolveRevisionBundlePaths,
72
+ removeLegacyOntologyStateFiles,
73
+ };
@@ -5,98 +5,38 @@ const path = require('path');
5
5
  const sddConfig = require('./sdd-config.cjs');
6
6
  const workspaceLayout = require('./workspace-layout.cjs');
7
7
 
8
- function isSpecPackageRoot(dir) {
9
- const root = path.resolve(dir);
10
- return (
11
- fs.existsSync(path.join(root, 'openspec', 'changes'))
12
- || fs.existsSync(path.join(root, 'modules.yaml'))
13
- || fs.existsSync(path.join(root, 'sdd.config.yaml'))
14
- );
8
+ function resolveHint(anchorRoot) {
9
+ const hintPath = path.join(anchorRoot, '.sdd-spec-root');
10
+ if (!fs.existsSync(hintPath)) return null;
11
+ const hint = fs.readFileSync(hintPath, 'utf8').trim();
12
+ if (!hint) return null;
13
+ const resolved = path.resolve(anchorRoot, hint);
14
+ return fs.existsSync(resolved) ? resolved : null;
15
15
  }
16
16
 
17
- /**
18
- * 解析 SDD 文档根(*-sdd-specs 包裹包或旧平铺 spec 根)。
19
- * openspec CLI 只认 cwd 下的 openspec/;本函数供 log.cjs / ontology cli 自动定位包裹包。
20
- *
21
- * 解析顺序:
22
- * 1. start 本身已是 spec 包根
23
- * 2. KLD_SDD_SPEC_ROOT(绝对或相对 start)
24
- * 3. 单仓 layout: mono → spec_path / 子目录 *-sdd-specs
25
- * 4. 工作区 layout → specRepo
26
- * 5. start 下嵌套包裹包
27
- * 6. 代码仓 git config sdd.specPath(含仓内 nested 包裹)
28
- * 7. 向上最多 3 层找 mono / 嵌套包(Git 根、工作区根常见)
29
- * 8. 回退 start
30
- */
31
- function resolveSpecProjectRoot(startDir = process.cwd(), options = {}) {
32
- const start = path.resolve(startDir || process.cwd());
33
- if (options.skipResolve || options.forCodeRepo) return start;
34
- if (isSpecPackageRoot(start)) return start;
17
+ function resolveSpecProjectRoot(projectRoot = process.cwd()) {
18
+ const root = path.resolve(projectRoot);
19
+ const hinted = resolveHint(root);
20
+ if (hinted) return hinted;
35
21
 
36
- const envRoot = process.env.KLD_SDD_SPEC_ROOT && String(process.env.KLD_SDD_SPEC_ROOT).trim();
37
- if (envRoot) {
38
- const candidate = path.isAbsolute(envRoot) ? envRoot : path.resolve(start, envRoot);
39
- if (fs.existsSync(candidate)) return candidate;
22
+ if (sddConfig.isMonoLayout(root)) {
23
+ return sddConfig.resolveMonoSpecRoot(root);
40
24
  }
41
25
 
42
- if (sddConfig.isMonoLayout(start)) {
43
- const monoRoot = sddConfig.resolveMonoSpecRoot(start);
44
- if (fs.existsSync(monoRoot)) return monoRoot;
45
- }
46
-
47
- const layout = workspaceLayout.detectWorkspaceLayout(start);
48
- if (layout.specRepo && fs.existsSync(layout.specRepo.abs)) {
49
- return layout.specRepo.abs;
50
- }
26
+ const layout = workspaceLayout.detectWorkspaceLayout(root);
27
+ if (layout.specRepo) return layout.specRepo.abs;
51
28
 
52
- const nested = workspaceLayout.findNestedSpecPackage(start);
29
+ const nested = workspaceLayout.findNestedSpecPackage(root);
53
30
  if (nested) return nested.abs;
54
31
 
55
- const specPathResult = sddConfig.resolveSpecPath(start);
56
- if (specPathResult.ok) {
57
- const valid = sddConfig.validateSpecPath(specPathResult.path, { allowNestedPackage: true });
58
- if (valid.ok) return valid.path;
59
- }
60
-
61
- let dir = start;
62
- for (let depth = 0; depth < 3; depth += 1) {
63
- const parent = path.dirname(dir);
64
- if (parent === dir) break;
65
- if (sddConfig.isMonoLayout(parent)) {
66
- const monoRoot = sddConfig.resolveMonoSpecRoot(parent);
67
- if (fs.existsSync(monoRoot)) return monoRoot;
68
- }
69
- const parentNested = workspaceLayout.findNestedSpecPackage(parent);
70
- if (parentNested) return parentNested.abs;
71
- const parentLayout = workspaceLayout.detectWorkspaceLayout(parent);
72
- if (parentLayout.specRepo && fs.existsSync(parentLayout.specRepo.abs)) {
73
- return parentLayout.specRepo.abs;
74
- }
75
- dir = parent;
76
- }
77
-
78
- return start;
32
+ return root;
79
33
  }
80
34
 
81
- /**
82
- * CLI / skills 用:解析 --project 参数。
83
- * '.' 或未显式绝对 spec 根时走 resolve;link-spec 等代码仓命令传 forCodeRepo:true。
84
- */
85
- function resolveProjectArg(projectArg, startDir = process.cwd(), options = {}) {
86
- const raw = projectArg == null || projectArg === '' ? '.' : String(projectArg);
87
- const resolved = path.resolve(startDir, raw);
88
- if (options.forCodeRepo) return resolved;
89
- if (raw !== '.' && raw !== './' && path.isAbsolute(raw)) {
90
- return isSpecPackageRoot(resolved) ? resolved : resolveSpecProjectRoot(resolved, options);
91
- }
92
- if (raw !== '.' && raw !== './') {
93
- if (isSpecPackageRoot(resolved)) return resolved;
94
- }
95
- return resolveSpecProjectRoot(startDir, options);
35
+ function resolveProjectArg(projectArg = '.', cwd = process.cwd()) {
36
+ return resolveSpecProjectRoot(path.resolve(cwd, projectArg));
96
37
  }
97
38
 
98
39
  module.exports = {
99
- isSpecPackageRoot,
100
- resolveSpecProjectRoot,
101
40
  resolveProjectArg,
41
+ resolveSpecProjectRoot,
102
42
  };
@@ -20,6 +20,14 @@ const {
20
20
  assertChangeLock,
21
21
  releaseChangeLock,
22
22
  } = require('./change-lock.cjs');
23
+ const {
24
+ ONTOLOGY_STATE_FILES,
25
+ ontologyStateRelative,
26
+ changeOntologyFilePaths,
27
+ resolveOntologyStateFile,
28
+ resolveRevisionBundlePaths,
29
+ removeLegacyOntologyStateFiles,
30
+ } = require('./ontology-paths.cjs');
23
31
 
24
32
  function sha256(value) {
25
33
  return crypto.createHash('sha256').update(value).digest('hex');
@@ -31,15 +39,7 @@ function resolveChangeDir(projectRoot, changeName) {
31
39
  }
32
40
 
33
41
  function changeOntologyPaths(projectRoot, changeName) {
34
- const changeDir = resolveChangeDir(projectRoot, changeName);
35
- return {
36
- changeDir,
37
- working: path.join(changeDir, 'working-ontology.json'),
38
- diagnostics: path.join(changeDir, 'diagnostics.json'),
39
- fileIndex: path.join(changeDir, 'file-index.json'),
40
- artifactIndex: path.join(changeDir, 'artifact-index.json'),
41
- artifactDir: path.join(changeDir, 'artifacts'),
42
- };
42
+ return changeOntologyFilePaths(resolveChangeDir(projectRoot, changeName));
43
43
  }
44
44
 
45
45
  function statePaths(projectRoot, changeName) {
@@ -160,14 +160,16 @@ function commitRevision(projectRoot, changeName, result, lock) {
160
160
  assertChangeLock(lock);
161
161
  const basePaths = statePaths(projectRoot, changeName);
162
162
  const paths = changeOntologyPaths(projectRoot, changeName);
163
+ const readablePaths = resolveRevisionBundlePaths(paths.changeDir);
163
164
  fs.mkdirSync(basePaths.dir, { recursive: true });
164
165
  fs.mkdirSync(paths.changeDir, { recursive: true });
166
+ fs.mkdirSync(paths.ontologyDir, { recursive: true });
165
167
 
166
- if (fs.existsSync(paths.working)) {
167
- const existing = readJson(paths.working, 'SEM_STATE_CORRUPT');
168
+ if (fs.existsSync(readablePaths.working)) {
169
+ const existing = readJson(readablePaths.working, 'SEM_STATE_CORRUPT');
168
170
  if (existing.revision === result.revision) {
169
- readRevisionBundle(paths, result.revision);
170
- const artifactIndex = readJson(paths.artifactIndex, 'SEM_STATE_CORRUPT');
171
+ readRevisionBundle(readablePaths, result.revision);
172
+ const artifactIndex = readJson(readablePaths.artifactIndex, 'SEM_STATE_CORRUPT');
171
173
  return {
172
174
  changed: false,
173
175
  paths: {
@@ -225,6 +227,7 @@ function commitRevision(projectRoot, changeName, result, lock) {
225
227
  atomicWriteIfChanged(path.join(paths.changeDir, artifactFile.path), fs.readFileSync(stagedPath, 'utf8'));
226
228
  }
227
229
  pruneStaleArtifacts(paths.changeDir, artifactBundle.index);
230
+ removeLegacyOntologyStateFiles(paths.changeDir);
228
231
  fs.rmSync(stagingDir, { recursive: true, force: true });
229
232
 
230
233
  const pointer = {
@@ -232,7 +235,7 @@ function commitRevision(projectRoot, changeName, result, lock) {
232
235
  change: safeChangeName(changeName),
233
236
  revision: result.revision,
234
237
  change_dir: path.relative(path.resolve(projectRoot), paths.changeDir).replace(/\\/g, '/'),
235
- artifact_index: 'artifact-index.json',
238
+ artifact_index: ontologyStateRelative(ONTOLOGY_STATE_FILES.artifactIndex),
236
239
  };
237
240
  assertChangeLock(lock);
238
241
  const changed = atomicWriteIfChanged(basePaths.current, pointer);
@@ -272,9 +275,9 @@ function scanChange(projectRoot, changeName, options = {}) {
272
275
  identityHistory,
273
276
  effectiveGraph,
274
277
  projectRoot,
275
- continuityResolutionPath: path.join(
278
+ continuityResolutionPath: resolveOntologyStateFile(
276
279
  parsed.changeDir,
277
- 'continuity-resolution.json',
280
+ ONTOLOGY_STATE_FILES.continuityResolution,
278
281
  ),
279
282
  });
280
283
  const reviewStatus = options.markPending && validation.valid ? 'pending' : 'draft';
@@ -358,16 +361,17 @@ function reconcileChange(projectRoot, changeName, options = {}) {
358
361
  function readWorkingState(projectRoot, changeName) {
359
362
  const basePaths = statePaths(projectRoot, changeName);
360
363
  const paths = changeOntologyPaths(projectRoot, changeName);
364
+ const readablePaths = resolveRevisionBundlePaths(paths.changeDir);
361
365
  if (fs.existsSync(basePaths.current)) {
362
366
  const pointer = readJson(basePaths.current, 'SEM_STATE_POINTER_CORRUPT');
363
367
  if (!pointer.revision || pointer.change !== safeChangeName(changeName)) {
364
368
  throw stateError('SEM_STATE_POINTER_CORRUPT', `current.json 不属于 Change ${changeName}`);
365
369
  }
366
- return readRevisionBundle(paths, pointer.revision).working;
370
+ return readRevisionBundle(readablePaths, pointer.revision).working;
367
371
  }
368
- if (!fs.existsSync(paths.working)) return null;
369
- const working = readJson(paths.working, 'SEM_STATE_CORRUPT');
370
- readRevisionBundle(paths, working.revision);
372
+ if (!fs.existsSync(readablePaths.working)) return null;
373
+ const working = readJson(readablePaths.working, 'SEM_STATE_CORRUPT');
374
+ readRevisionBundle(readablePaths, working.revision);
371
375
  return working;
372
376
  }
373
377
 
@@ -396,9 +400,12 @@ function createArchiveSnapshot(projectRoot, changeName, archiveDir, inputState)
396
400
  if (!state.facts_hash || state.facts_hash !== actualFacts.facts_hash) {
397
401
  throw new Error(`归档正文与待确认本体事实不一致: ${changeName}`);
398
402
  }
399
- const artifactIndexPath = path.join(targetDir, 'artifact-index.json');
403
+ const artifactIndexPath = resolveOntologyStateFile(
404
+ targetDir,
405
+ ONTOLOGY_STATE_FILES.artifactIndex,
406
+ );
400
407
  if (!fs.existsSync(artifactIndexPath)) {
401
- throw new Error(`归档目录缺少 artifact-index.json: ${targetDir}`);
408
+ throw new Error(`归档目录缺少 ontology/artifact-index.json: ${targetDir}`);
402
409
  }
403
410
  const artifactIndex = readJson(artifactIndexPath, 'SEM_STATE_CORRUPT');
404
411
  if (artifactIndex.revision !== state.revision) {
@@ -468,4 +475,7 @@ module.exports = {
468
475
  reconcileChange,
469
476
  readWorkingState,
470
477
  createArchiveSnapshot,
478
+ resolveOntologyStateFile,
479
+ resolveRevisionBundlePaths,
480
+ ONTOLOGY_STATE_FILES,
471
481
  };
@@ -4,15 +4,20 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { SCHEMA_VERSION } = require('./schema.cjs');
6
6
  const { generateUuidV7, isValidUuidV7 } = require('./id.cjs');
7
+ const {
8
+ ONTOLOGY_STATE_FILES,
9
+ ontologyStateRelative,
10
+ resolveOntologyStateFile,
11
+ } = require('./ontology-paths.cjs');
7
12
 
8
- const REGISTRY_FILE = 'ontology-identities.json';
13
+ const REGISTRY_FILE = ontologyStateRelative(ONTOLOGY_STATE_FILES.identities);
9
14
 
10
15
  function registryPath(changeDir) {
11
16
  return path.join(path.resolve(changeDir), REGISTRY_FILE);
12
17
  }
13
18
 
14
19
  function readRegistry(changeDir) {
15
- const filePath = registryPath(changeDir);
20
+ const filePath = resolveOntologyStateFile(changeDir, ONTOLOGY_STATE_FILES.identities);
16
21
  if (!fs.existsSync(filePath)) {
17
22
  return { schema_version: SCHEMA_VERSION, identities: {} };
18
23
  }
@@ -35,6 +40,10 @@ function writeRegistry(changeDir, registry) {
35
40
  const tempPath = `${filePath}.${process.pid}.tmp`;
36
41
  fs.writeFileSync(tempPath, content, 'utf8');
37
42
  fs.renameSync(tempPath, filePath);
43
+ const legacy = path.join(path.resolve(changeDir), ONTOLOGY_STATE_FILES.identities);
44
+ if (legacy !== filePath && fs.existsSync(legacy)) {
45
+ fs.rmSync(legacy, { force: true });
46
+ }
38
47
  return true;
39
48
  }
40
49
 
@@ -1,11 +1,18 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
4
+ const fs = require('fs');
3
5
  const {
4
6
  DIAGNOSTIC_CODES,
5
7
  normalizeProfile,
6
8
  relationDefinition,
7
9
  } = require('./schema.cjs');
8
10
  const { isValidEntityId, isValidUuid } = require('./id.cjs');
11
+ const {
12
+ ONTOLOGY_STATE_FILES,
13
+ ontologyStateRelative,
14
+ resolveOntologyStateFile,
15
+ } = require('./ontology-paths.cjs');
9
16
 
10
17
  function diagnostic(code, severity, message, context = {}) {
11
18
  return {
@@ -350,18 +357,19 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
350
357
  const kind = String(continuity.kind || '').toLowerCase();
351
358
  const resolutionPath = options.continuityResolutionPath
352
359
  || (facts.change
353
- ? require('path').join(
354
- options.projectRoot || process.cwd(),
355
- 'openspec',
356
- 'changes',
357
- facts.change,
358
- 'continuity-resolution.json',
360
+ ? resolveOntologyStateFile(
361
+ path.join(
362
+ options.projectRoot || process.cwd(),
363
+ 'openspec',
364
+ 'changes',
365
+ facts.change,
366
+ ),
367
+ ONTOLOGY_STATE_FILES.continuityResolution,
359
368
  )
360
369
  : '');
361
370
  let resolution = null;
362
371
  if (resolutionPath) {
363
372
  try {
364
- const fs = require('fs');
365
373
  if (fs.existsSync(resolutionPath)) {
366
374
  resolution = JSON.parse(fs.readFileSync(resolutionPath, 'utf8'));
367
375
  }
@@ -392,7 +400,7 @@ function validateContinuityAndExternalRefs(facts, diagnostics, options = {}) {
392
400
  'error',
393
401
  'Continuity=iteration 但 continuity-resolution.json 缺失或仍为 pending',
394
402
  {
395
- file: resolutionPath || 'continuity-resolution.json',
403
+ file: resolutionPath || ontologyStateRelative(ONTOLOGY_STATE_FILES.continuityResolution),
396
404
  suggestion: '在 propose/spec 完成 A/B/C 决议后再 check',
397
405
  },
398
406
  ));
@@ -2,6 +2,8 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const path = require('path');
5
+ const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
6
+ const { ontologyStateRelative, ONTOLOGY_STATE_FILES } = require('./ontology-paths.cjs');
5
7
 
6
8
  const WORKING_ARTIFACT_SCHEMA_VERSION = 'kld-sdd-working-artifact-facts/v1';
7
9
  const ARTIFACT_INDEX_SCHEMA_VERSION = 'kld-sdd-working-artifact-index/v1';
@@ -141,7 +143,7 @@ function compareFacts(left, right) {
141
143
 
142
144
  function buildWorkingArtifactFacts(result, options = {}) {
143
145
  const state = result.state || {};
144
- const producerVersion = String(options.producerVersion || require('../../package.json').version);
146
+ const producerVersion = String(options.producerVersion || resolveRuntimeMetadata().version);
145
147
  const reviewStatus = state.review_status === 'pending' ? 'pending' : 'draft';
146
148
  const rawEntities = Array.isArray(state.entities) ? state.entities : [];
147
149
  const entityByAnchor = new Map(rawEntities.map((entity) => [
@@ -227,7 +229,7 @@ function buildWorkingArtifactFacts(result, options = {}) {
227
229
  review_status: reviewStatus,
228
230
  valid: Boolean(result.valid),
229
231
  revision: result.revision,
230
- working_ontology_path: 'working-ontology.json',
232
+ working_ontology_path: ontologyStateRelative(ONTOLOGY_STATE_FILES.working),
231
233
  artifacts: artifactIndex,
232
234
  },
233
235
  files: artifactFiles,