kld-sdd 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +95 -8
  2. package/kld-sdd-guide.html +1109 -0
  3. package/lib/command-bridge.js +156 -0
  4. package/lib/deploy-codebuddy-hooks.js +99 -0
  5. package/lib/hook-gate-core.js +333 -0
  6. package/lib/init.js +137 -82
  7. package/lib/settings-merge.js +85 -0
  8. package/lib/skills-bundle.js +142 -5
  9. package/lib/tool-profiles.js +270 -0
  10. package/package.json +4 -2
  11. package/skywalk-sdd/index.cjs +329 -24
  12. package/skywalk-sdd/ontology/artifact-observer.cjs +91 -0
  13. package/skywalk-sdd/ontology/artifact-parser.cjs +621 -0
  14. package/skywalk-sdd/ontology/change-lock.cjs +126 -0
  15. package/skywalk-sdd/ontology/cli.cjs +146 -0
  16. package/skywalk-sdd/ontology/effective-graph.cjs +158 -0
  17. package/skywalk-sdd/ontology/id.cjs +126 -0
  18. package/skywalk-sdd/ontology/identity-index.cjs +262 -0
  19. package/skywalk-sdd/ontology/normalizer.cjs +107 -0
  20. package/skywalk-sdd/ontology/runtime.cjs +341 -0
  21. package/skywalk-sdd/ontology/schema.cjs +139 -0
  22. package/skywalk-sdd/ontology/structural-identity.cjs +77 -0
  23. package/skywalk-sdd/ontology/traceability-validator.cjs +610 -0
  24. package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
  25. package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
  26. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
  27. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
  28. package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
  29. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
  30. package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
  31. package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
  32. package/templates/hooks/codebuddy/settings.json +72 -0
  33. package/templates/openspec/design.md +18 -0
  34. package/templates/openspec/proposal.md +19 -6
  35. package/templates/openspec/spec.md +62 -8
  36. package/templates/openspec/tasks.md +28 -6
  37. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +5 -5
  38. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +9 -0
  39. package/templates/skills/kld-sdd/opsx-check/SKILL.md +17 -1
  40. package/templates/skills/kld-sdd/opsx-design/SKILL.md +10 -1
  41. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
  42. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +11 -1
  43. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
  44. package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
  45. package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
  46. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +12 -1
  47. package/templates/skills/kld-sdd/opsx-task/SKILL.md +10 -1
  48. package/templates/skills/kld-sdd/opsx-test/SKILL.md +1 -1
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { safeChangeName } = require('./artifact-parser.cjs');
7
+
8
+ const DEFAULT_STALE_MS = 5 * 60 * 1000;
9
+
10
+ function lockPaths(projectRoot, changeName) {
11
+ const root = path.resolve(projectRoot || process.cwd());
12
+ const safeName = safeChangeName(changeName);
13
+ const dir = path.join(root, 'skywalk-sdd', 'state', 'ontology', '.locks');
14
+ return {
15
+ dir,
16
+ file: path.join(dir, `${safeName}.lock`),
17
+ };
18
+ }
19
+
20
+ function isProcessAlive(pid) {
21
+ if (!Number.isInteger(pid) || pid <= 0) return false;
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (error) {
26
+ return error && error.code === 'EPERM';
27
+ }
28
+ }
29
+
30
+ function readLock(filePath) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
33
+ } catch (error) {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ function isStaleLock(lock, staleMs) {
39
+ if (!lock || !lock.created_at) return true;
40
+ const createdAt = Date.parse(lock.created_at);
41
+ if (!Number.isFinite(createdAt)) return true;
42
+ return Date.now() - createdAt > staleMs || !isProcessAlive(Number(lock.pid));
43
+ }
44
+
45
+ function lockedError(changeName, lock) {
46
+ const owner = lock && lock.pid ? `,持有进程 PID=${lock.pid}` : '';
47
+ const error = new Error(`Change 正在进行语义事务: ${changeName}${owner}`);
48
+ error.code = 'SEM_CHANGE_LOCKED';
49
+ error.lock = lock || null;
50
+ return error;
51
+ }
52
+
53
+ function acquireChangeLock(projectRoot, changeName, options = {}) {
54
+ const paths = lockPaths(projectRoot, changeName);
55
+ const staleMs = Math.max(1000, Number(options.staleMs || DEFAULT_STALE_MS));
56
+ fs.mkdirSync(paths.dir, { recursive: true });
57
+
58
+ for (let attempt = 0; attempt < 2; attempt += 1) {
59
+ const token = crypto.randomUUID();
60
+ const lock = {
61
+ schema_version: 'kld-sdd-change-lock/v1',
62
+ change: safeChangeName(changeName),
63
+ token,
64
+ pid: process.pid,
65
+ created_at: new Date().toISOString(),
66
+ };
67
+ let descriptor;
68
+ try {
69
+ descriptor = fs.openSync(paths.file, 'wx');
70
+ fs.writeFileSync(descriptor, JSON.stringify(lock, null, 2) + '\n', 'utf8');
71
+ fs.closeSync(descriptor);
72
+ return { ...lock, path: paths.file };
73
+ } catch (error) {
74
+ if (descriptor !== undefined) fs.closeSync(descriptor);
75
+ if (!error || error.code !== 'EEXIST') throw error;
76
+ const existing = readLock(paths.file);
77
+ if (attempt === 0 && isStaleLock(existing, staleMs)) {
78
+ fs.rmSync(paths.file, { force: true });
79
+ continue;
80
+ }
81
+ throw lockedError(changeName, existing);
82
+ }
83
+ }
84
+ throw lockedError(changeName, readLock(paths.file));
85
+ }
86
+
87
+ function assertChangeLock(lock) {
88
+ if (!lock || !lock.path || !lock.token) {
89
+ const error = new Error('缺少 Change 事务锁');
90
+ error.code = 'SEM_CHANGE_LOCK_REQUIRED';
91
+ throw error;
92
+ }
93
+ const current = readLock(lock.path);
94
+ if (!current || current.token !== lock.token || current.pid !== lock.pid) {
95
+ const error = new Error(`Change 事务锁已失效: ${lock.change || 'unknown'}`);
96
+ error.code = 'SEM_CHANGE_LOCK_LOST';
97
+ throw error;
98
+ }
99
+ return current;
100
+ }
101
+
102
+ function releaseChangeLock(lock) {
103
+ if (!lock || !lock.path || !lock.token) return false;
104
+ const current = readLock(lock.path);
105
+ if (!current || current.token !== lock.token) return false;
106
+ fs.rmSync(lock.path, { force: true });
107
+ return true;
108
+ }
109
+
110
+ function withChangeLock(projectRoot, changeName, options, action) {
111
+ const lock = acquireChangeLock(projectRoot, changeName, options);
112
+ try {
113
+ return action(lock);
114
+ } finally {
115
+ releaseChangeLock(lock);
116
+ }
117
+ }
118
+
119
+ module.exports = {
120
+ DEFAULT_STALE_MS,
121
+ lockPaths,
122
+ acquireChangeLock,
123
+ assertChangeLock,
124
+ releaseChangeLock,
125
+ withChangeLock,
126
+ };
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const {
6
+ scanChange,
7
+ reconcileChange,
8
+ readWorkingState,
9
+ createArchiveSnapshot,
10
+ } = require('./runtime.cjs');
11
+ const { observeChangeArtifacts } = require('./artifact-observer.cjs');
12
+ const { allocateIdentity } = require('./id.cjs');
13
+
14
+ function parseArgs(argv) {
15
+ const result = { _: [] };
16
+ for (let index = 0; index < argv.length; index += 1) {
17
+ const value = argv[index];
18
+ if (!value.startsWith('--')) {
19
+ result._.push(value);
20
+ continue;
21
+ }
22
+ const equal = value.indexOf('=');
23
+ if (equal >= 0) {
24
+ result[value.slice(2, equal)] = value.slice(equal + 1);
25
+ } else if (argv[index + 1] && !argv[index + 1].startsWith('--')) {
26
+ result[value.slice(2)] = argv[index + 1];
27
+ index += 1;
28
+ } else {
29
+ result[value.slice(2)] = true;
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+
35
+ function summary(result) {
36
+ return {
37
+ change: result.state.change,
38
+ profile: result.profile,
39
+ valid: result.valid,
40
+ revision: result.revision,
41
+ changed: result.changed,
42
+ counts: result.counts,
43
+ paths: result.paths,
44
+ diagnostics: result.diagnostics,
45
+ };
46
+ }
47
+
48
+ function showHelp() {
49
+ console.log(`KLD-SDD Ontology Runtime
50
+
51
+ 用法:
52
+ node skywalk-sdd/ontology/cli.cjs scan --project=. --change=<name> [--profile=auto|simple|full|strict]
53
+ node skywalk-sdd/ontology/cli.cjs identity --delta-state=added
54
+ node skywalk-sdd/ontology/cli.cjs identity --delta-state=modified --entity-id=<uuid> --predecessor-version=<uuid>
55
+ node skywalk-sdd/ontology/cli.cjs identity --delta-state=unchanged --entity-id=<uuid> --version-id=<uuid>
56
+ node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
57
+ node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
58
+ node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
59
+ node skywalk-sdd/ontology/cli.cjs observe --project=. --change=<name> [--interval=1500]
60
+ node skywalk-sdd/ontology/cli.cjs snapshot --project=. --change=<name> --archive=<path>
61
+
62
+ observe 只负责采集和同步;check/archive 前仍必须执行 reconcile。`);
63
+ }
64
+
65
+ function main(argv = process.argv.slice(2)) {
66
+ const args = parseArgs(argv);
67
+ const command = args._[0];
68
+ if (!command || command === 'help' || args.help) {
69
+ showHelp();
70
+ return;
71
+ }
72
+ if (command === 'identity') {
73
+ console.log(JSON.stringify(allocateIdentity(args), null, 2));
74
+ return;
75
+ }
76
+ const projectRoot = path.resolve(args.project || '.');
77
+ const changeName = args.change;
78
+ if (!changeName) throw new Error('缺少 --change 参数');
79
+ const profile = args.profile || 'auto';
80
+
81
+ if (command === 'scan') {
82
+ const result = scanChange(projectRoot, changeName, { profile });
83
+ console.log(JSON.stringify(summary({ ...result, changed: false, paths: undefined }), null, 2));
84
+ if (!result.valid) process.exitCode = 1;
85
+ return;
86
+ }
87
+ if (command === 'reconcile' || command === 'check') {
88
+ const result = reconcileChange(projectRoot, changeName, {
89
+ profile,
90
+ markPending: command === 'check',
91
+ });
92
+ console.log(JSON.stringify(summary(result), null, 2));
93
+ if (!result.valid) process.exitCode = 1;
94
+ return;
95
+ }
96
+ if (command === 'status') {
97
+ const state = readWorkingState(projectRoot, changeName);
98
+ if (!state) throw new Error(`不存在工作态本体实例: ${changeName}`);
99
+ console.log(JSON.stringify(state, null, 2));
100
+ return;
101
+ }
102
+ if (command === 'snapshot') {
103
+ if (!args.archive) throw new Error('snapshot 缺少 --archive 参数');
104
+ const snapshot = createArchiveSnapshot(projectRoot, changeName, path.resolve(args.archive));
105
+ console.log(JSON.stringify(snapshot, null, 2));
106
+ return;
107
+ }
108
+ if (command === 'observe') {
109
+ const observer = observeChangeArtifacts(projectRoot, changeName, {
110
+ profile,
111
+ pollIntervalMs: Number(args.interval || 1500),
112
+ onReconciled(result) {
113
+ console.log(JSON.stringify(summary(result)));
114
+ },
115
+ onError(error) {
116
+ console.error(`[ontology-observer] ${error.message}`);
117
+ },
118
+ });
119
+ console.log(JSON.stringify({ change: changeName, mode: observer.mode, change_dir: observer.changeDir }));
120
+ const close = () => {
121
+ observer.close();
122
+ process.exit(0);
123
+ };
124
+ process.once('SIGINT', close);
125
+ process.once('SIGTERM', close);
126
+ return;
127
+ }
128
+
129
+ throw new Error(`未知 ontology 命令: ${command}`);
130
+ }
131
+
132
+ if (require.main === module) {
133
+ try {
134
+ main();
135
+ } catch (error) {
136
+ console.error(`错误: ${error.message}`);
137
+ process.exit(1);
138
+ }
139
+ }
140
+
141
+ module.exports = {
142
+ parseArgs,
143
+ summary,
144
+ showHelp,
145
+ main,
146
+ };
@@ -0,0 +1,158 @@
1
+ 'use strict';
2
+
3
+ const { DIAGNOSTIC_CODES } = require('./schema.cjs');
4
+
5
+ function resolutionDiagnostic(code, message, item, suggestion = '') {
6
+ return {
7
+ code,
8
+ severity: 'error',
9
+ message,
10
+ file: item && item.source && item.source.file || '',
11
+ line: Number(item && item.source && item.source.line || 0),
12
+ entity_id: item && (item.anchor_id || item.from) || undefined,
13
+ suggestion: suggestion || undefined,
14
+ };
15
+ }
16
+
17
+ function sourceMatches(record, sourceRef, anchorId) {
18
+ const normalized = String(sourceRef || '').replace(/\\/g, '/');
19
+ if (!normalized) return false;
20
+ const expectedTail = `${record.source && record.source.file || ''}#${anchorId}`;
21
+ return normalized.includes(`/archive/${record.change}/`) && normalized.endsWith(expectedTail);
22
+ }
23
+
24
+ function relationSourceMatches(root, sourceRef) {
25
+ const normalized = String(sourceRef || '').replace(/\\/g, '/');
26
+ return Boolean(normalized && normalized.includes(`/archive/${root.change}/`));
27
+ }
28
+
29
+ function buildEffectiveGraph(currentFacts, identityCatalog) {
30
+ const catalog = identityCatalog || { records: [], factsByRoot: new Map() };
31
+ const currentEntities = (currentFacts.entities || []).filter((entity) => entity.delta_state !== 'removed');
32
+ const currentRelations = (currentFacts.relations || []).slice();
33
+ const effectiveEntities = currentEntities.map((entity) => ({ ...entity, generation_role: 'current' }));
34
+ const effectiveRelations = currentRelations.map((relation) => ({ ...relation, generation_role: 'current' }));
35
+ const inheritedResolutions = [];
36
+ const resolutionDiagnostics = [];
37
+ const resolvedRoots = new Map();
38
+
39
+ for (const reference of currentFacts.inherited_references || []) {
40
+ const matches = (catalog.records || []).filter((record) => (
41
+ record.scope === 'archive'
42
+ && record.anchor_id === reference.anchor_id
43
+ && record.entity_id === reference.entity_id
44
+ && record.version_id === reference.version_id
45
+ && record.content_hash === reference.source_version_hash
46
+ && sourceMatches(record, reference.source_ref, reference.anchor_id)
47
+ ));
48
+ if (matches.length !== 1) {
49
+ resolutionDiagnostics.push(resolutionDiagnostic(
50
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
51
+ `unchanged 引用必须唯一解析到 confirmed Archive: ${reference.anchor_id}`,
52
+ reference,
53
+ matches.length > 1 ? '消除重复 Archive 版本,禁止任选候选' : '修正 archive scope、UUID、anchor、source 和内容哈希',
54
+ ));
55
+ continue;
56
+ }
57
+ const record = matches[0];
58
+ const root = catalog.factsByRoot && catalog.factsByRoot.get(record.root_key);
59
+ const historicalEntity = root && root.facts.entities.find((entity) => (
60
+ entity.id === reference.anchor_id && entity.version_id === reference.version_id
61
+ ));
62
+ if (!root || !historicalEntity) {
63
+ resolutionDiagnostics.push(resolutionDiagnostic(
64
+ DIAGNOSTIC_CODES.VERSION_LINEAGE_MISSING,
65
+ `confirmed Archive 缺少可投影实体: ${reference.anchor_id}`,
66
+ reference,
67
+ ));
68
+ continue;
69
+ }
70
+ effectiveEntities.push({
71
+ ...historicalEntity,
72
+ delta_state: 'unchanged',
73
+ generation_role: 'inherited',
74
+ inherited_from: {
75
+ change: record.change,
76
+ source_revision: record.source_revision,
77
+ source_ref: reference.source_ref,
78
+ },
79
+ });
80
+ resolvedRoots.set(reference.anchor_id, root);
81
+ inheritedResolutions.push({
82
+ kind: 'entity',
83
+ anchor_id: reference.anchor_id,
84
+ root_key: record.root_key,
85
+ source_revision: record.source_revision,
86
+ });
87
+ }
88
+
89
+ const entityIds = new Set(effectiveEntities.map((entity) => entity.id));
90
+ for (const inherited of currentFacts.inherited_relations || []) {
91
+ const candidateRoots = [...new Set([
92
+ resolvedRoots.get(inherited.from),
93
+ resolvedRoots.get(inherited.to),
94
+ ].filter(Boolean))];
95
+ const matches = [];
96
+ for (const root of candidateRoots) {
97
+ if (!relationSourceMatches(root, inherited.source_ref)) continue;
98
+ for (const relation of root.facts.relations || []) {
99
+ if (relation.type === inherited.type && relation.from === inherited.from && relation.to === inherited.to) {
100
+ matches.push({ root, relation });
101
+ }
102
+ }
103
+ }
104
+ if (matches.length !== 1 || !entityIds.has(inherited.from) || !entityIds.has(inherited.to)) {
105
+ resolutionDiagnostics.push(resolutionDiagnostic(
106
+ DIAGNOSTIC_CODES.INHERITED_RELATION_MISSING,
107
+ `继承关系必须唯一存在且两端都在 Effective Graph: ${inherited.from} -${inherited.type}-> ${inherited.to}`,
108
+ inherited,
109
+ '显式继承关系两端实体,并核对 confirmed Archive relation source',
110
+ ));
111
+ continue;
112
+ }
113
+ effectiveRelations.push({
114
+ ...matches[0].relation,
115
+ generation_role: 'inherited',
116
+ inherited_from: {
117
+ change: matches[0].root.change,
118
+ source_revision: matches[0].root.source_revision,
119
+ source_ref: inherited.source_ref,
120
+ },
121
+ });
122
+ inheritedResolutions.push({
123
+ kind: 'relation',
124
+ type: inherited.type,
125
+ from: inherited.from,
126
+ to: inherited.to,
127
+ root_key: matches[0].root.rootKey,
128
+ source_revision: matches[0].root.source_revision,
129
+ });
130
+ }
131
+
132
+ const seenEntities = new Set();
133
+ const uniqueEntities = effectiveEntities.filter((entity) => {
134
+ const key = `${entity.id}:${entity.version_id || ''}`;
135
+ if (seenEntities.has(key)) return false;
136
+ seenEntities.add(key);
137
+ return true;
138
+ });
139
+ const seenRelations = new Set();
140
+ const uniqueRelations = effectiveRelations.filter((relation) => {
141
+ const key = `${relation.type}:${relation.from}:${relation.to}`;
142
+ if (seenRelations.has(key)) return false;
143
+ seenRelations.add(key);
144
+ return true;
145
+ });
146
+
147
+ return {
148
+ ...currentFacts,
149
+ effective_entities: uniqueEntities,
150
+ effective_relations: uniqueRelations,
151
+ inherited_resolutions: inheritedResolutions,
152
+ resolution_diagnostics: resolutionDiagnostics,
153
+ };
154
+ }
155
+
156
+ module.exports = {
157
+ buildEffectiveGraph,
158
+ };
@@ -0,0 +1,126 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { ID_PREFIX_TYPES, entityTypeForId } = require('./schema.cjs');
5
+
6
+ function normalizeSlug(value) {
7
+ const raw = String(value || '').trim();
8
+ const ascii = raw
9
+ .normalize('NFKD')
10
+ .toUpperCase()
11
+ .replace(/[^A-Z0-9]+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .replace(/-{2,}/g, '-');
14
+ if (ascii) return ascii;
15
+ if (!raw) return 'UNSCOPED';
16
+ return `U${crypto.createHash('sha1').update(raw).digest('hex').slice(0, 8).toUpperCase()}`;
17
+ }
18
+
19
+ function normalizePrefix(prefix) {
20
+ const normalized = String(prefix || '').trim().toUpperCase();
21
+ if (!ID_PREFIX_TYPES[normalized]) {
22
+ throw new Error(`不支持的实体 ID 前缀: ${prefix}`);
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ function nextEntityId(prefix, scope, existingIds = []) {
28
+ const normalizedPrefix = normalizePrefix(prefix);
29
+ const normalizedScope = normalizeSlug(scope);
30
+ const matcher = new RegExp(`^${normalizedPrefix}-${normalizedScope}-(\\d+)$`, 'i');
31
+ let max = 0;
32
+ for (const value of existingIds) {
33
+ const match = String(value || '').trim().match(matcher);
34
+ if (!match) continue;
35
+ max = Math.max(max, Number(match[1]) || 0);
36
+ }
37
+ return `${normalizedPrefix}-${normalizedScope}-${String(max + 1).padStart(3, '0')}`;
38
+ }
39
+
40
+ function isValidEntityId(entityId, expectedType = '') {
41
+ const value = String(entityId || '').trim().toUpperCase();
42
+ if (!/^(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value)) {
43
+ return false;
44
+ }
45
+ if (expectedType && entityTypeForId(value) !== expectedType) {
46
+ return false;
47
+ }
48
+ return true;
49
+ }
50
+
51
+ function generateUuidV7(timestamp = Date.now()) {
52
+ const milliseconds = Number(timestamp);
53
+ if (!Number.isSafeInteger(milliseconds) || milliseconds < 0 || milliseconds > 0xffffffffffff) {
54
+ throw new Error(`UUIDv7 时间戳非法: ${timestamp}`);
55
+ }
56
+ const bytes = crypto.randomBytes(16);
57
+ let value = BigInt(milliseconds);
58
+ for (let index = 5; index >= 0; index -= 1) {
59
+ bytes[index] = Number(value & 0xffn);
60
+ value >>= 8n;
61
+ }
62
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
63
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
64
+ const hex = bytes.toString('hex');
65
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
66
+ }
67
+
68
+ function isValidUuid(value) {
69
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(value || '').trim());
70
+ }
71
+
72
+ const isValidUuidV7 = isValidUuid;
73
+
74
+ function allocateIdentity(options = {}) {
75
+ const deltaState = String(options.deltaState || options['delta-state'] || 'added').trim().toLowerCase();
76
+ const entityId = String(options.entityId || options['entity-id'] || '').trim().toLowerCase();
77
+ const versionId = String(options.versionId || options['version-id'] || '').trim().toLowerCase();
78
+ const predecessorVersionId = String(
79
+ options.predecessorVersionId || options['predecessor-version'] || '',
80
+ ).trim().toLowerCase();
81
+
82
+ if (!['added', 'modified', 'unchanged', 'removed'].includes(deltaState)) {
83
+ throw new Error(`不支持的 delta-state: ${deltaState}`);
84
+ }
85
+ if (deltaState === 'added') {
86
+ return {
87
+ entity_id: generateUuidV7(),
88
+ version_id: generateUuidV7(),
89
+ predecessor_version_id: null,
90
+ delta_state: deltaState,
91
+ };
92
+ }
93
+ if (!isValidUuid(entityId)) {
94
+ throw new Error(`${deltaState} 必须提供有效的 --entity-id`);
95
+ }
96
+ if (deltaState === 'unchanged') {
97
+ if (!isValidUuid(versionId)) {
98
+ throw new Error('unchanged 必须提供既有 --version-id');
99
+ }
100
+ return {
101
+ entity_id: entityId,
102
+ version_id: versionId,
103
+ predecessor_version_id: null,
104
+ delta_state: deltaState,
105
+ };
106
+ }
107
+ if (!isValidUuid(predecessorVersionId)) {
108
+ throw new Error(`${deltaState} 必须提供有效的 --predecessor-version`);
109
+ }
110
+ return {
111
+ entity_id: entityId,
112
+ version_id: generateUuidV7(),
113
+ predecessor_version_id: predecessorVersionId,
114
+ delta_state: deltaState,
115
+ };
116
+ }
117
+
118
+ module.exports = {
119
+ normalizeSlug,
120
+ nextEntityId,
121
+ isValidEntityId,
122
+ generateUuidV7,
123
+ isValidUuid,
124
+ isValidUuidV7,
125
+ allocateIdentity,
126
+ };