kld-sdd 2.7.8 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/USABILITY.md +72 -0
- package/bin/kld-sdd-init.js +718 -34
- package/lib/command-bridge.js +26 -17
- package/lib/deploy-codebuddy-hooks.js +19 -26
- package/lib/deploy-strategy.js +293 -0
- package/lib/hook-gate-core.js +7 -8
- package/lib/hook-install.js +75 -0
- package/lib/init.js +507 -726
- package/lib/maintenance.js +221 -0
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +5 -5
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +453 -0
- package/package.json +7 -4
- package/skywalk-sdd/context-client.cjs +38 -7
- package/skywalk-sdd/index.cjs +153 -27
- package/skywalk-sdd/kb-sync-identity.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +242 -85
- package/skywalk-sdd/lib/check-review.cjs +41 -0
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +130 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/archive-package.cjs +141 -0
- package/skywalk-sdd/ontology/artifact-parser.cjs +24 -3
- package/skywalk-sdd/ontology/cli.cjs +53 -0
- package/skywalk-sdd/ontology/id.cjs +1 -1
- package/skywalk-sdd/ontology/identity-index.cjs +5 -1
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/schema.cjs +6 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/spec-ontology.cjs +507 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +65 -17
- package/templates/git-hooks/commit-msg +17 -85
- package/templates/git-hooks/commit-msg-runner.cjs +44 -0
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
- package/templates/git-hooks/consistency-check-core.cjs +92 -261
- package/templates/git-hooks/delivery-check.cjs +231 -0
- package/templates/git-hooks/delivery-snapshot.cjs +181 -0
- package/templates/git-hooks/hooks.config +8 -6
- package/templates/git-hooks/pre-commit +19 -5
- package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push +19 -5
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +2 -2
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +2 -2
- package/templates/openspec/proposal.md +1 -1
- package/templates/openspec/spec.md +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +28 -24
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +12 -3
- package/templates/skills/kld-sdd/opsx-apply/reference.md +21 -1
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +20 -18
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +18 -13
- package/templates/skills/kld-sdd/opsx-check/reference.md +3 -1
- package/templates/skills/kld-sdd/opsx-check/review-template.json +22 -0
- package/templates/skills/kld-sdd/opsx-check/reviewer.md +27 -6
- package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
- package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
- package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +93 -61
- package/templates/skills/kld-sdd/opsx-kb-config/reference.md +14 -12
- package/templates/skills/kld-sdd/opsx-kb-ingest/SKILL.md +16 -5
- package/templates/skills/kld-sdd/opsx-kb-ingest/reference.md +13 -5
- package/templates/skills/kld-sdd/opsx-kb-ingest/state.example.json +18 -0
- package/templates/skills/kld-sdd/opsx-ontology-ingest/SKILL.md +77 -0
- package/templates/skills/kld-sdd/opsx-ontology-query/SKILL.md +5 -4
- package/templates/skills/kld-sdd/opsx-ontology-query/reference.md +14 -1
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +1 -1
- package/templates/skills/kld-sdd/opsx-propose/interaction-policy.md +9 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -3
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +2 -2
- package/index.js +0 -13
|
@@ -289,61 +289,81 @@ function resolveSpecRootFast(anchorDir = __dirname) {
|
|
|
289
289
|
// 7. KB 配置加载
|
|
290
290
|
// ═══════════════════════════════════════════════════════════════
|
|
291
291
|
|
|
292
|
-
/**
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
292
|
+
/** kb-state.json 文件名(唯一权威位置:spec 包裹包根目录) */
|
|
293
|
+
const KB_STATE_FILENAME = 'kb-state.json';
|
|
294
|
+
|
|
295
|
+
/** 向上搜索 kb-state.json 的最大层数(从 skywalk-sdd/lib/ 到 spec 根最多 3 层) */
|
|
296
|
+
const MAX_KB_STATE_LEVELS = 6;
|
|
297
|
+
|
|
298
|
+
let _isSpecPackageRoot = null;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 惰性加载 resolve-spec-root.cjs 的 isSpecPackageRoot(避免加载期循环依赖)。
|
|
302
|
+
*/
|
|
303
|
+
function getIsSpecPackageRoot() {
|
|
304
|
+
if (!_isSpecPackageRoot) {
|
|
305
|
+
const mod = require(path.join(__dirname, '..', 'ontology', 'resolve-spec-root.cjs'));
|
|
306
|
+
_isSpecPackageRoot = mod.isSpecPackageRoot;
|
|
307
|
+
}
|
|
308
|
+
return _isSpecPackageRoot;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* kb-state.json 位置校验:宿主目录必须是 spec 包裹包根。
|
|
313
|
+
*
|
|
314
|
+
* 判据复用 `ontology/resolve-spec-root.cjs#isSpecPackageRoot`(与全仓 spec 识别
|
|
315
|
+
* 单一真相源对齐),不在本模块另立一套结构判据。
|
|
316
|
+
*
|
|
317
|
+
* @param {string} dir 候选宿主目录
|
|
318
|
+
* @returns {boolean}
|
|
319
|
+
*/
|
|
320
|
+
function isKbStateHostDir(dir) {
|
|
321
|
+
try {
|
|
322
|
+
return Boolean(getIsSpecPackageRoot()(dir));
|
|
323
|
+
} catch {
|
|
324
|
+
// ontology 层不可用(理论上部署布局下不会发生):保守判否,避免误用未知位置的配置
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
297
328
|
|
|
298
329
|
/**
|
|
299
330
|
* 加载 KB 配置(kb-state.json)。
|
|
300
331
|
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
332
|
+
* **唯一权威位置**:spec 包裹包根目录 `<spec-root>/kb-state.json`
|
|
333
|
+
* —— 由 `/opsx-kb-config` 写入(见 opsx-kb-config/reference.md §kb-state.json),
|
|
334
|
+
* spec 仓根、已 gitignore。
|
|
335
|
+
*
|
|
336
|
+
* 位置校验(本函数同时承担「位置正确性」判定,而不只是「能否读到」):
|
|
337
|
+
* 从 anchorDir 逐级向上查找同名文件,但**只接受宿主目录为 spec 包裹包根**的命中
|
|
338
|
+
* (isKbStateHostDir)。因此从 `<spec-root>/skywalk-sdd/`、`<spec-root>/skywalk-sdd/lib/`
|
|
339
|
+
* 等子目录调用都能正确命中 spec 根;而落在工作区根、代码仓根等错误位置的文件
|
|
340
|
+
* 不会被误用 —— 读到「不该读的配置」比读不到更危险。
|
|
306
341
|
*
|
|
307
342
|
* @param {string} [anchorDir=__dirname] — 搜索起点
|
|
308
|
-
* @param {object} [env=process.env] —
|
|
343
|
+
* @param {object} [env=process.env] — 保留参数(环境变量兜底由 resolveKbParams 处理)
|
|
309
344
|
* @returns {{ config: object|null, path: string|null, warnings: string[] }}
|
|
310
345
|
*/
|
|
311
346
|
function loadKbState(anchorDir = __dirname, env = process.env) {
|
|
347
|
+
void env;
|
|
312
348
|
const warnings = [];
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
let current =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
candidatePaths.push(path.join(current, '.shared', 'kb-state.json'));
|
|
327
|
-
for (const ide of IDE_DIRS) {
|
|
328
|
-
candidatePaths.push(path.join(current, ide, 'skills', '.shared', 'kb-state.json'));
|
|
349
|
+
const start = path.resolve(anchorDir);
|
|
350
|
+
const fileRoot = path.parse(start).root;
|
|
351
|
+
|
|
352
|
+
let current = start;
|
|
353
|
+
for (let level = 0; level < MAX_KB_STATE_LEVELS; level += 1) {
|
|
354
|
+
const candidate = path.join(current, KB_STATE_FILENAME);
|
|
355
|
+
if (fs.existsSync(candidate) && isKbStateHostDir(current)) {
|
|
356
|
+
try {
|
|
357
|
+
const config = JSON.parse(stripBom(fs.readFileSync(candidate, 'utf8')));
|
|
358
|
+
return { config, path: candidate, warnings };
|
|
359
|
+
} catch (e) {
|
|
360
|
+
warnings.push(`kb-state.json 解析失败 (${candidate}): ${e.message}`);
|
|
361
|
+
}
|
|
329
362
|
}
|
|
330
363
|
if (current === fileRoot) break;
|
|
331
364
|
current = path.dirname(current);
|
|
332
365
|
}
|
|
333
366
|
|
|
334
|
-
for (const p of candidatePaths) {
|
|
335
|
-
try {
|
|
336
|
-
if (fs.existsSync(p)) {
|
|
337
|
-
let raw = fs.readFileSync(p, 'utf8');
|
|
338
|
-
raw = stripBom(raw);
|
|
339
|
-
const config = JSON.parse(raw);
|
|
340
|
-
return { config, path: p, warnings };
|
|
341
|
-
}
|
|
342
|
-
} catch (e) {
|
|
343
|
-
warnings.push(`kb-state.json 解析失败 (${p}): ${e.message}`);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
367
|
return { config: null, path: null, warnings };
|
|
348
368
|
}
|
|
349
369
|
|
|
@@ -797,7 +817,8 @@ module.exports = {
|
|
|
797
817
|
// Spec root
|
|
798
818
|
resolveSpecRootFast,
|
|
799
819
|
// KB state
|
|
800
|
-
|
|
820
|
+
KB_STATE_FILENAME,
|
|
821
|
+
isKbStateHostDir,
|
|
801
822
|
loadKbState,
|
|
802
823
|
resolveKbParams,
|
|
803
824
|
resolveKbScopes,
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const crypto = require('node:crypto');
|
|
6
|
+
const { spawnSync } = require('node:child_process');
|
|
7
|
+
|
|
8
|
+
function parseCounts(output) {
|
|
9
|
+
// Prefer runner totals: counting TAP parent suites as tests double-counts nested tests.
|
|
10
|
+
const value = name => {
|
|
11
|
+
const matches = [...output.matchAll(new RegExp(`^(?:#|ℹ)\\s+${name}\\s+(\\d+)\\s*$`, 'gm'))];
|
|
12
|
+
return matches.length ? Number(matches[matches.length - 1][1]) : null;
|
|
13
|
+
};
|
|
14
|
+
const total = value('tests'), passed = value('pass'), failed = value('fail');
|
|
15
|
+
const skipped = value('skipped'), todo = value('todo');
|
|
16
|
+
if ([total, passed, failed].every(Number.isInteger)
|
|
17
|
+
&& total === passed + failed + (skipped || 0) + (todo || 0)) {
|
|
18
|
+
return { counts_known: true, passed, failed, skipped: (skipped || 0) + (todo || 0) };
|
|
19
|
+
}
|
|
20
|
+
// Surefire/Failsafe print per-class counts and a Results section. Count only
|
|
21
|
+
// the latter, once per module/execution, to avoid double-counting classes.
|
|
22
|
+
const summaries = [];
|
|
23
|
+
let inResults = false;
|
|
24
|
+
for (const raw of output.replace(/\u001b\[[0-9;]*m/g, '').split(/\r?\n/)) {
|
|
25
|
+
const line = raw.replace(/^\[(?:INFO|WARNING|ERROR)\]\s*/, '').trim();
|
|
26
|
+
if (line === 'Results:') { inResults = true; continue; }
|
|
27
|
+
if (!inResults) continue;
|
|
28
|
+
const match = /^Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)\b/.exec(line);
|
|
29
|
+
if (!match) continue;
|
|
30
|
+
const [, count, failures, errors, ignored] = match.map(Number);
|
|
31
|
+
if (count < failures + errors + ignored) return { counts_known: false };
|
|
32
|
+
summaries.push({ passed: count - failures - errors - ignored, failed: failures + errors, skipped: ignored });
|
|
33
|
+
inResults = false;
|
|
34
|
+
}
|
|
35
|
+
if (summaries.length) {
|
|
36
|
+
return summaries.reduce((sum, item) => ({ counts_known: true, passed: sum.passed + item.passed,
|
|
37
|
+
failed: sum.failed + item.failed, skipped: sum.skipped + item.skipped }),
|
|
38
|
+
{ counts_known: true, passed: 0, failed: 0, skipped: 0 });
|
|
39
|
+
}
|
|
40
|
+
return { counts_known: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function classifyFailure(output, exitCode, error, signal) {
|
|
44
|
+
if (error?.code === 'ETIMEDOUT') return 'timeout';
|
|
45
|
+
if (error || signal) return 'infrastructure';
|
|
46
|
+
if (exitCode === 0) return 'none';
|
|
47
|
+
// A missing executable/module/test path is not evidence of a failing business assertion.
|
|
48
|
+
if (/MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND|Cannot find module|Could not find|ENOENT|SyntaxError|ERR_TEST_FAILURE.*cancelled/i.test(output)) return 'infrastructure';
|
|
49
|
+
if (/ERR_ASSERTION|AssertionError|AssertionFailedError|org\.opentest4j\.AssertionFailedError/.test(output)) return 'assertion';
|
|
50
|
+
return 'infrastructure';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function captureTest(args, projectRoot, change) {
|
|
54
|
+
let command;
|
|
55
|
+
try { command = JSON.parse(args['command-json'] || 'null'); } catch { /* validated below */ }
|
|
56
|
+
if (!Array.isArray(command) || !command.length || command.some(x => typeof x !== 'string' || x.includes('\0')) || !command[0]) {
|
|
57
|
+
throw new Error('TEST_COMMAND_REQUIRED: --command-json 必须是非空命令参数数组,不通过 shell 执行');
|
|
58
|
+
}
|
|
59
|
+
const phase = args['tdd-phase'] || 'regression';
|
|
60
|
+
if (!['red', 'green', 'refactor', 'regression', 'not-applicable'].includes(phase)) throw new Error('TEST_PHASE_INVALID');
|
|
61
|
+
const taskIds = [...new Set(String(args['task-ids'] || args['task-id'] || '').split(',').map(x => x.trim()).filter(Boolean))];
|
|
62
|
+
if (!taskIds.length) throw new Error('TEST_TASKS_REQUIRED: 明确指定 --task-ids,工具不猜测测试覆盖的任务');
|
|
63
|
+
const timeout = Number(args['timeout-ms'] || 120000);
|
|
64
|
+
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 3600000) throw new Error('TEST_TIMEOUT_INVALID');
|
|
65
|
+
const cwd = path.resolve(args.cwd || projectRoot);
|
|
66
|
+
if (!fs.statSync(cwd).isDirectory()) throw new Error('TEST_CWD_INVALID');
|
|
67
|
+
const runId = args['run-id'] || crypto.randomUUID();
|
|
68
|
+
const key = crypto.createHash('sha256').update(`${change}:${runId}`).digest('hex');
|
|
69
|
+
const dir = path.join(projectRoot, 'skywalk-sdd', 'state', 'test-runs', key);
|
|
70
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
71
|
+
// A run ID identifies one execution. Never re-execute a command under an old receipt.
|
|
72
|
+
try { fs.mkdirSync(dir); } catch (error) {
|
|
73
|
+
if (error.code === 'EEXIST') throw new Error('TEST_RUN_EXISTS: 此 run-id 已执行;使用保存的 details.json 重试记录,重新执行须使用新 run-id');
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
const startedAt = new Date().toISOString();
|
|
77
|
+
const started = process.hrtime.bigint();
|
|
78
|
+
const childEnv = { ...process.env };
|
|
79
|
+
// The wrapper can itself run inside node:test; its child is a fresh test runner.
|
|
80
|
+
delete childEnv.NODE_TEST_CONTEXT;
|
|
81
|
+
const child = spawnSync(command[0], command.slice(1), {
|
|
82
|
+
cwd, env: childEnv, shell: false, encoding: 'utf8', timeout, maxBuffer: 16 * 1024 * 1024,
|
|
83
|
+
});
|
|
84
|
+
const duration = Math.round(Number(process.hrtime.bigint() - started) / 1e6);
|
|
85
|
+
const stdout = child.stdout || '', stderr = child.stderr || '';
|
|
86
|
+
const output = stdout + '\n' + stderr;
|
|
87
|
+
const exitCode = Number.isInteger(child.status) ? child.status : (child.error?.code === 'ETIMEDOUT' ? 124 : 1);
|
|
88
|
+
const failureType = classifyFailure(output, exitCode, child.error, child.signal);
|
|
89
|
+
const counts = parseCounts(output);
|
|
90
|
+
const files = {};
|
|
91
|
+
for (const [name, content] of Object.entries({ 'stdout.log': stdout, 'stderr.log': stderr })) {
|
|
92
|
+
fs.writeFileSync(path.join(dir, name), content, { mode: 0o600 });
|
|
93
|
+
files[name] = crypto.createHash('sha256').update(content).digest('hex');
|
|
94
|
+
}
|
|
95
|
+
const execution = {
|
|
96
|
+
origin: 'command-execution', argv: command, cwd, started_at: startedAt,
|
|
97
|
+
finished_at: new Date().toISOString(), signal: child.signal || null,
|
|
98
|
+
error: child.error ? { code: child.error.code, message: child.error.message } : null,
|
|
99
|
+
output_files: Object.entries(files).map(([name, sha256]) => ({ path: path.join(dir, name), sha256 })),
|
|
100
|
+
task_association: 'explicit-caller-declaration',
|
|
101
|
+
receipt_path: path.join(dir, 'execution.json'),
|
|
102
|
+
};
|
|
103
|
+
const testResults = {
|
|
104
|
+
run_id: runId, verified_task_ids: taskIds, tdd_phase: phase,
|
|
105
|
+
command: JSON.stringify(command), result: exitCode === 0 ? 'success' : 'failure',
|
|
106
|
+
exit_code: exitCode, duration_ms: duration, ...counts, failure_type: failureType,
|
|
107
|
+
expected_failure: phase === 'red' && args['expected-failure'] === true,
|
|
108
|
+
execution,
|
|
109
|
+
};
|
|
110
|
+
const receipt = path.join(dir, 'details.json');
|
|
111
|
+
fs.writeFileSync(execution.receipt_path, JSON.stringify(testResults, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
112
|
+
fs.writeFileSync(receipt, JSON.stringify({ test_results: testResults }, null, 2) + '\n', { mode: 0o600 });
|
|
113
|
+
return { testResults, runId, receipt };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function verifyCapture(input, projectRoot) {
|
|
117
|
+
const base = fs.realpathSync(path.join(projectRoot, 'skywalk-sdd/state/test-runs')) + path.sep;
|
|
118
|
+
const receiptPath = fs.realpathSync(input.execution.receipt_path);
|
|
119
|
+
if (input.execution.origin !== 'command-execution' || !receiptPath.startsWith(base)) throw new Error('TEST_CAPTURE_INVALID: 执行回执不在当前项目');
|
|
120
|
+
const receipt = JSON.parse(fs.readFileSync(receiptPath, 'utf8'));
|
|
121
|
+
for (const key of Object.keys(receipt)) {
|
|
122
|
+
if (JSON.stringify(receipt[key]) !== JSON.stringify(input[key])) throw new Error(`TEST_CAPTURE_MISMATCH: ${key} 与真实执行回执不一致`);
|
|
123
|
+
}
|
|
124
|
+
for (const output of receipt.execution.output_files) {
|
|
125
|
+
const file = fs.realpathSync(output.path);
|
|
126
|
+
if (!file.startsWith(base) || crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex') !== output.sha256) throw new Error('TEST_CAPTURE_HASH_MISMATCH: 原始测试输出已变化');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { captureTest, parseCounts, classifyFailure, verifyCapture };
|
|
@@ -23,6 +23,8 @@ const {
|
|
|
23
23
|
readUserConfig,
|
|
24
24
|
} = require('./user-config.cjs');
|
|
25
25
|
const gitIdentity = require('./git-identity.cjs');
|
|
26
|
+
// kb-state.json 的唯一读取入口(含「必须位于 spec 包裹包根」的位置校验)
|
|
27
|
+
const { loadKbState } = require('./shared.cjs');
|
|
26
28
|
|
|
27
29
|
const PENDING_FILE_NAME = 'pending-usage.jsonl';
|
|
28
30
|
const PENDING_MAX_BYTES = 1024 * 1024; // 1 MB
|
|
@@ -93,6 +95,16 @@ function _resetGitIdentityCache() {
|
|
|
93
95
|
* 四级解析上报地址:config.server → 项目 kb-state.json api → env KLD_SDD_SERVER → 内置默认。
|
|
94
96
|
* 每级均经 normalizeServerUrl;结果永远非空(默认地址兜底)。
|
|
95
97
|
*
|
|
98
|
+
* kb-state.json 的读取走 `loadKbState`(唯一 kb-state 读取入口),因此沿用
|
|
99
|
+
* 其位置校验:只有位于 spec 包裹包根的那份才被采用。位置不符时行为与
|
|
100
|
+
* 「文件不存在」一致 —— 静默落到下一级(env → 内置默认)。
|
|
101
|
+
*
|
|
102
|
+
* 这里刻意**不**把 loadKbState 的 warnings 打到 stderr:上报链路是
|
|
103
|
+
* 「永不抛错、静默兜底」的后台路径,配置告警应由 KB 技能链路(kb-config /
|
|
104
|
+
* ontology-query / kb-ingest)呈现,避免每次 log start 都刷屏。
|
|
105
|
+
* 且宁可上报到默认地址,也不能因为某处残留的陈旧 kb-state.json
|
|
106
|
+
* 把使用数据发到错误服务器。
|
|
107
|
+
*
|
|
96
108
|
* @param {{homeDir?:string, projectRoot?:string, env?:object}} [options]
|
|
97
109
|
* @returns {string}
|
|
98
110
|
*/
|
|
@@ -107,14 +119,11 @@ function resolveReportServer(options = {}) {
|
|
|
107
119
|
}
|
|
108
120
|
// 2. 项目级 kb-state.json 的 api(kb-state.api 是唯一带 /api 契约的来源,先剥离尾部 /api 再标准化)
|
|
109
121
|
try {
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const normalized = normalizeServerUrl(stripped);
|
|
116
|
-
if (normalized) return normalized;
|
|
117
|
-
}
|
|
122
|
+
const { config: kbState } = loadKbState(projectRoot);
|
|
123
|
+
if (kbState && typeof kbState.api === 'string') {
|
|
124
|
+
const stripped = kbState.api.trim().replace(/\/api(\/v\d+)?\/?$/i, '');
|
|
125
|
+
const normalized = normalizeServerUrl(stripped);
|
|
126
|
+
if (normalized) return normalized;
|
|
118
127
|
}
|
|
119
128
|
} catch {
|
|
120
129
|
// kb-state 读取失败时静默,继续下一级
|
|
@@ -4,6 +4,13 @@ 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 {
|
|
8
|
+
SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
9
|
+
SPEC_ONTOLOGY_PATH,
|
|
10
|
+
buildSpecOntology,
|
|
11
|
+
validateSpecOntology,
|
|
12
|
+
specOntologyContentSignature,
|
|
13
|
+
} = require('./spec-ontology.cjs');
|
|
7
14
|
const { resolveRuntimeMetadata } = require('../runtime-metadata.cjs');
|
|
8
15
|
const {
|
|
9
16
|
readJson,
|
|
@@ -581,6 +588,18 @@ function materializeArchivePackage(options = {}) {
|
|
|
581
588
|
);
|
|
582
589
|
const canonicalPath = path.join(archiveDir, CANONICAL_FACTS_PATH);
|
|
583
590
|
const canonicalBytes = writeJsonAtomic(canonicalPath, canonicalFacts);
|
|
591
|
+
const specOntology = buildSpecOntology(snapshot, {
|
|
592
|
+
projectId: projectIdentity.project_id,
|
|
593
|
+
archiveId,
|
|
594
|
+
compatibilityMode: 'ADAPTED_V2',
|
|
595
|
+
reviewStatus: 'confirmed',
|
|
596
|
+
});
|
|
597
|
+
const specOntologyValidation = validateSpecOntology(specOntology);
|
|
598
|
+
if (!specOntologyValidation.valid) {
|
|
599
|
+
throw new Error(`Spec 本体事实包校验失败: ${specOntologyValidation.errors.join('; ')}`);
|
|
600
|
+
}
|
|
601
|
+
const specOntologyPath = path.join(archiveDir, SPEC_ONTOLOGY_PATH);
|
|
602
|
+
writeJsonAtomic(specOntologyPath, specOntology);
|
|
584
603
|
|
|
585
604
|
const conversionReport = {
|
|
586
605
|
schema_version: CONVERSION_REPORT_SCHEMA_VERSION,
|
|
@@ -589,6 +608,9 @@ function materializeArchivePackage(options = {}) {
|
|
|
589
608
|
source_schema: String(snapshot.schema_version || 'unknown'),
|
|
590
609
|
target_file: CANONICAL_FACTS_PATH,
|
|
591
610
|
target_schema: CANONICAL_FACTS_SCHEMA_VERSION,
|
|
611
|
+
spec_ontology_file: SPEC_ONTOLOGY_PATH,
|
|
612
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
613
|
+
compatibility_mode: specOntology.compatibility_mode,
|
|
592
614
|
status: 'converted',
|
|
593
615
|
entity_count: canonicalFacts.entities.length,
|
|
594
616
|
relation_count: canonicalFacts.relations.length,
|
|
@@ -618,6 +640,9 @@ function materializeArchivePackage(options = {}) {
|
|
|
618
640
|
change_id: changeId,
|
|
619
641
|
content_hash: `sha256:${sha256(canonicalBytes)}`,
|
|
620
642
|
canonical_facts_path: CANONICAL_FACTS_PATH,
|
|
643
|
+
spec_ontology_path: SPEC_ONTOLOGY_PATH,
|
|
644
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
645
|
+
compatibility_mode: specOntology.compatibility_mode,
|
|
621
646
|
conversion_report_path: CONVERSION_REPORT_PATH,
|
|
622
647
|
created_at: createdAt,
|
|
623
648
|
files,
|
|
@@ -647,6 +672,121 @@ function materializeArchivePackage(options = {}) {
|
|
|
647
672
|
conversion_report: conversionReport,
|
|
648
673
|
manifest_path: existingManifestPath,
|
|
649
674
|
canonical_facts_path: canonicalPath,
|
|
675
|
+
spec_ontology_path: specOntologyPath,
|
|
676
|
+
conversion_report_path: path.join(archiveDir, CONVERSION_REPORT_PATH),
|
|
677
|
+
package_path: packagePath,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function materializeCompatibilityPackage(options = {}) {
|
|
682
|
+
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
683
|
+
const archiveDir = path.resolve(requiredText(options.archiveDir, 'archiveDir', 'Compatibility Package'));
|
|
684
|
+
const facts = options.facts;
|
|
685
|
+
if (!facts || !Array.isArray(facts.entities)) {
|
|
686
|
+
throw new Error('Compatibility Package 缺少已解析 facts');
|
|
687
|
+
}
|
|
688
|
+
const projectIdentity = ensureProjectIdentity(projectRoot);
|
|
689
|
+
const changeName = requiredText(options.changeName || facts.change, 'changeName', 'Compatibility Package');
|
|
690
|
+
const changeId = String(facts.change_id || `CHG-LEGACY-${sha256(changeName).slice(0, 12)}`).toUpperCase();
|
|
691
|
+
const publicationVariant = options.specOntology?.review_status === 'confirmed' ? 'reviewed' : 'candidate';
|
|
692
|
+
const archiveId = `urn:kld:sdd:archive:${sha256(
|
|
693
|
+
`${projectIdentity.project_id}\n${changeId}\n${facts.facts_hash || sha256(JSON.stringify(facts))}\n${publicationVariant}`,
|
|
694
|
+
)}`;
|
|
695
|
+
let specOntology;
|
|
696
|
+
if (options.specOntology) {
|
|
697
|
+
specOntology = JSON.parse(JSON.stringify(options.specOntology));
|
|
698
|
+
if (specOntology.project_id !== projectIdentity.project_id) {
|
|
699
|
+
throw new Error('已审核 Spec 本体的 project_id 与当前项目不一致,请重新物化并审核');
|
|
700
|
+
}
|
|
701
|
+
if (specOntology.compatibility_mode !== 'LEGACY_MATERIALIZED') {
|
|
702
|
+
throw new Error('兼容包只接受 LEGACY_MATERIALIZED 本体');
|
|
703
|
+
}
|
|
704
|
+
if (specOntology.review_status !== 'confirmed' || specOntology.publication_eligible !== true) {
|
|
705
|
+
throw new Error('历史 Spec 本体尚未审核,不能发布为 current 事实');
|
|
706
|
+
}
|
|
707
|
+
const currentOntology = buildSpecOntology({ ...facts, change_id: changeId }, {
|
|
708
|
+
projectId: projectIdentity.project_id,
|
|
709
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
710
|
+
reviewStatus: 'pending',
|
|
711
|
+
});
|
|
712
|
+
if (specOntologyContentSignature(specOntology) !== specOntologyContentSignature(currentOntology)) {
|
|
713
|
+
throw new Error('已审核 Spec 本体与当前 Markdown 内容不一致,请重新物化并审核');
|
|
714
|
+
}
|
|
715
|
+
specOntology.archive_id = archiveId;
|
|
716
|
+
specOntology.change = changeName;
|
|
717
|
+
specOntology.change_id = changeId;
|
|
718
|
+
delete specOntology.ontology_hash;
|
|
719
|
+
specOntology.ontology_hash = `sha256:${sha256(JSON.stringify(specOntology))}`;
|
|
720
|
+
} else {
|
|
721
|
+
specOntology = buildSpecOntology({ ...facts, change_id: changeId }, {
|
|
722
|
+
projectId: projectIdentity.project_id,
|
|
723
|
+
archiveId,
|
|
724
|
+
compatibilityMode: 'LEGACY_MATERIALIZED',
|
|
725
|
+
reviewStatus: 'pending',
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
const validation = validateSpecOntology(specOntology);
|
|
729
|
+
if (!validation.valid) {
|
|
730
|
+
throw new Error(`历史 Spec 本体物化失败: ${validation.errors.join('; ')}`);
|
|
731
|
+
}
|
|
732
|
+
const toleratedDiagnostics = (options.toleratedDiagnostics || [])
|
|
733
|
+
.map((item) => `LEGACY_DIAGNOSTIC_TOLERATED: ${item.code}: ${item.message}`);
|
|
734
|
+
const specOntologyPath = path.join(archiveDir, SPEC_ONTOLOGY_PATH);
|
|
735
|
+
const specOntologyBytes = writeJsonAtomic(specOntologyPath, specOntology);
|
|
736
|
+
const conversionReport = {
|
|
737
|
+
schema_version: CONVERSION_REPORT_SCHEMA_VERSION,
|
|
738
|
+
producer: PRODUCER,
|
|
739
|
+
source_file: 'legacy-markdown',
|
|
740
|
+
source_schema: String(facts.schema_version || 'legacy-markdown'),
|
|
741
|
+
target_file: SPEC_ONTOLOGY_PATH,
|
|
742
|
+
target_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
743
|
+
compatibility_mode: 'LEGACY_MATERIALIZED',
|
|
744
|
+
status: specOntology.publication_eligible ? 'materialized_reviewed' : 'materialized_pending_review',
|
|
745
|
+
entity_count: specOntology.entities.length,
|
|
746
|
+
relation_count: specOntology.relations.length,
|
|
747
|
+
warnings: [...new Set([...(specOntology.warnings || []), ...toleratedDiagnostics])],
|
|
748
|
+
};
|
|
749
|
+
writeJsonAtomic(path.join(archiveDir, CONVERSION_REPORT_PATH), conversionReport);
|
|
750
|
+
|
|
751
|
+
const createdAt = options.archivedAt || new Date().toISOString();
|
|
752
|
+
const filePaths = collectPackageFilePaths(archiveDir);
|
|
753
|
+
const files = filePaths.map((relativePath) => ({
|
|
754
|
+
path: relativePath,
|
|
755
|
+
content_hash: `sha256:${sha256(fs.readFileSync(path.join(archiveDir, relativePath)))}`,
|
|
756
|
+
}));
|
|
757
|
+
const manifest = {
|
|
758
|
+
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
759
|
+
producer: PRODUCER,
|
|
760
|
+
project_id: projectIdentity.project_id,
|
|
761
|
+
archive_id: archiveId,
|
|
762
|
+
change_id: changeId,
|
|
763
|
+
content_hash: `sha256:${sha256(specOntologyBytes)}`,
|
|
764
|
+
canonical_facts_path: null,
|
|
765
|
+
spec_ontology_path: SPEC_ONTOLOGY_PATH,
|
|
766
|
+
spec_ontology_schema: SPEC_ONTOLOGY_SCHEMA_VERSION,
|
|
767
|
+
compatibility_mode: 'LEGACY_MATERIALIZED',
|
|
768
|
+
conversion_report_path: CONVERSION_REPORT_PATH,
|
|
769
|
+
created_at: createdAt,
|
|
770
|
+
files,
|
|
771
|
+
change: changeName,
|
|
772
|
+
archived_at: createdAt,
|
|
773
|
+
reason: options.reason || '历史 Spec 本体兼容入库',
|
|
774
|
+
method: options.method || 'legacy-spec-ontology-materialization',
|
|
775
|
+
source_path: options.sourcePath || '',
|
|
776
|
+
archive_path: toPosix(path.relative(projectRoot, archiveDir)),
|
|
777
|
+
copied_specs: [],
|
|
778
|
+
evidence_events_path: null,
|
|
779
|
+
};
|
|
780
|
+
const manifestPath = path.join(archiveDir, MANIFEST_PATH);
|
|
781
|
+
writeJsonAtomic(manifestPath, manifest);
|
|
782
|
+
const packagePath = path.resolve(options.packagePath || `${archiveDir}.zip`);
|
|
783
|
+
writeArchiveZip(archiveDir, packagePath, manifest);
|
|
784
|
+
return {
|
|
785
|
+
manifest,
|
|
786
|
+
conversion_report: conversionReport,
|
|
787
|
+
validation,
|
|
788
|
+
manifest_path: manifestPath,
|
|
789
|
+
spec_ontology_path: specOntologyPath,
|
|
650
790
|
conversion_report_path: path.join(archiveDir, CONVERSION_REPORT_PATH),
|
|
651
791
|
package_path: packagePath,
|
|
652
792
|
};
|
|
@@ -707,5 +847,6 @@ module.exports = {
|
|
|
707
847
|
readStoredZipEntries,
|
|
708
848
|
verifyArchivePackage,
|
|
709
849
|
materializeArchivePackage,
|
|
850
|
+
materializeCompatibilityPackage,
|
|
710
851
|
finalizeArchivePackageAfterReports,
|
|
711
852
|
};
|
|
@@ -6,7 +6,7 @@ const path = require('path');
|
|
|
6
6
|
const { DIAGNOSTIC_CODES, entityTypeForId } = require('./schema.cjs');
|
|
7
7
|
const { ensureStructuralIdentities } = require('./structural-identity.cjs');
|
|
8
8
|
|
|
9
|
-
const ENTITY_ID_PATTERN = /\b(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
|
|
9
|
+
const ENTITY_ID_PATTERN = /\b(?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*\b/gi;
|
|
10
10
|
const IDENTITY_META_PATTERN = /^\s*-\s+\*\*(entity-id|version-id|predecessor-version|delta-state|external-ref)\*\*\s*[::]\s*(.*?)\s*$/i;
|
|
11
11
|
|
|
12
12
|
function toPosix(value) {
|
|
@@ -444,7 +444,7 @@ function parseSpec(target, file, lines, frontmatter) {
|
|
|
444
444
|
});
|
|
445
445
|
continue;
|
|
446
446
|
}
|
|
447
|
-
const inheritedRelationMatch = line.match(/^\s*-\s+\*\*relation\*\*\s*[::]\s*`?((?:CHG|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)\s+(contains|acceptedBy|constrainedBy|realizes|implements|covers|dependsOn)\s+((?:CHG|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)`?\s*$/i);
|
|
447
|
+
const inheritedRelationMatch = line.match(/^\s*-\s+\*\*relation\*\*\s*[::]\s*`?((?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)\s+(contains|usesObject|acceptedBy|constrainedBy|realizes|implements|covers|dependsOn)\s+((?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK)-[A-Z0-9-]+)`?\s*$/i);
|
|
448
448
|
if (inheritedRelationMatch) {
|
|
449
449
|
let source = '';
|
|
450
450
|
for (let offset = index + 1; offset < Math.min(lines.length, index + 6); offset += 1) {
|
|
@@ -461,7 +461,28 @@ function parseSpec(target, file, lines, frontmatter) {
|
|
|
461
461
|
});
|
|
462
462
|
continue;
|
|
463
463
|
}
|
|
464
|
-
let match = line.match(/^\s*####\s
|
|
464
|
+
let match = line.match(/^\s*####\s+业务对象[::]\s*[【\[]?(OBJ-[A-Z0-9-]+)[】\]]?\s*(.*?)\s*$/i);
|
|
465
|
+
if (match) {
|
|
466
|
+
const objectId = match[1].toUpperCase();
|
|
467
|
+
const endIndex = sectionEnd(lines, index, 4);
|
|
468
|
+
addEntity(
|
|
469
|
+
target,
|
|
470
|
+
'BusinessObject',
|
|
471
|
+
objectId,
|
|
472
|
+
match[2],
|
|
473
|
+
file,
|
|
474
|
+
index + 1,
|
|
475
|
+
{ capability_id: capabilityId || undefined },
|
|
476
|
+
parseIdentityBlock(lines, index + 1, endIndex),
|
|
477
|
+
lines.slice(index, endIndex).join('\n'),
|
|
478
|
+
);
|
|
479
|
+
if (capabilityId) {
|
|
480
|
+
addRelation(target, 'usesObject', capabilityId, objectId, file, index + 1, 'asserted');
|
|
481
|
+
}
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
match = line.match(/^\s*####\s+需求项[::]\s*[【\[]?(STMT-[A-Z0-9-]+)[】\]]?\s*(.*?)\s*$/i);
|
|
465
486
|
if (match) {
|
|
466
487
|
currentStatement = match[1].toUpperCase();
|
|
467
488
|
currentConstraint = '';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const {
|
|
6
7
|
scanChange,
|
|
@@ -18,6 +19,11 @@ const listChanges = require('./list-changes.cjs');
|
|
|
18
19
|
const sddConfig = require('./sdd-config.cjs');
|
|
19
20
|
const activeChanges = require('./active-changes.cjs');
|
|
20
21
|
const { resolveProjectArg } = require('./resolve-spec-root.cjs');
|
|
22
|
+
const {
|
|
23
|
+
materializeChangeSpecOntology,
|
|
24
|
+
validateSpecOntology,
|
|
25
|
+
approveSpecOntologyFile,
|
|
26
|
+
} = require('./spec-ontology.cjs');
|
|
21
27
|
|
|
22
28
|
function parseArgs(argv) {
|
|
23
29
|
const result = { _: [] };
|
|
@@ -77,6 +83,9 @@ function showHelp() {
|
|
|
77
83
|
node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
|
|
78
84
|
node skywalk-sdd/ontology/cli.cjs observe --project=. --change=<name> [--interval=1500]
|
|
79
85
|
node skywalk-sdd/ontology/cli.cjs snapshot --project=. --change=<name> --archive=<path>
|
|
86
|
+
node skywalk-sdd/ontology/cli.cjs materialize-ontology --project=. --change=<name> [--output=<path>]
|
|
87
|
+
node skywalk-sdd/ontology/cli.cjs validate-spec-ontology --file=<spec-ontology.json>
|
|
88
|
+
node skywalk-sdd/ontology/cli.cjs approve-spec-ontology --file=<spec-ontology.json>
|
|
80
89
|
|
|
81
90
|
observe 只负责采集和同步;check/archive 前仍必须执行 reconcile。`);
|
|
82
91
|
}
|
|
@@ -254,11 +263,55 @@ function main(argv = process.argv.slice(2)) {
|
|
|
254
263
|
if (!result.ok) process.exitCode = 1;
|
|
255
264
|
return;
|
|
256
265
|
}
|
|
266
|
+
if (command === 'validate-spec-ontology') {
|
|
267
|
+
const filePath = path.resolve(args.file || args._[1] || 'spec-ontology.json');
|
|
268
|
+
const payload = JSON.parse(fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, ''));
|
|
269
|
+
const result = validateSpecOntology(payload);
|
|
270
|
+
console.log(JSON.stringify({ file: filePath, ...result }, null, 2));
|
|
271
|
+
if (!result.valid) process.exitCode = 1;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (command === 'approve-spec-ontology') {
|
|
275
|
+
const filePath = path.resolve(args.file || args._[1] || 'spec-ontology.json');
|
|
276
|
+
const result = approveSpecOntologyFile(filePath, {
|
|
277
|
+
allowCollisions: args['allow-collisions'] === true,
|
|
278
|
+
});
|
|
279
|
+
console.log(JSON.stringify({
|
|
280
|
+
file: result.file,
|
|
281
|
+
compatibility_mode: result.payload.compatibility_mode,
|
|
282
|
+
review_status: result.payload.review_status,
|
|
283
|
+
publication_eligible: result.payload.publication_eligible,
|
|
284
|
+
validation: result.validation,
|
|
285
|
+
warnings: result.payload.warnings,
|
|
286
|
+
}, null, 2));
|
|
287
|
+
if (!result.validation.valid) process.exitCode = 1;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
257
290
|
const projectRoot = resolveCliProject(args);
|
|
258
291
|
const changeName = args.change;
|
|
259
292
|
if (!changeName) throw new Error('缺少 --change 参数');
|
|
260
293
|
const profile = args.profile || 'auto';
|
|
261
294
|
|
|
295
|
+
if (command === 'materialize-ontology' || command === 'materialize') {
|
|
296
|
+
const result = materializeChangeSpecOntology(projectRoot, changeName, {
|
|
297
|
+
outputPath: args.output,
|
|
298
|
+
mappingPath: args.mapping,
|
|
299
|
+
reviewStatus: args['review-status'],
|
|
300
|
+
});
|
|
301
|
+
console.log(JSON.stringify({
|
|
302
|
+
change: changeName,
|
|
303
|
+
compatibility_mode: result.payload.compatibility_mode,
|
|
304
|
+
review_status: result.payload.review_status,
|
|
305
|
+
publication_eligible: result.payload.publication_eligible,
|
|
306
|
+
output_path: result.output_path,
|
|
307
|
+
mapping_path: result.mapping_path,
|
|
308
|
+
validation: result.validation,
|
|
309
|
+
warnings: result.payload.warnings,
|
|
310
|
+
}, null, 2));
|
|
311
|
+
if (!result.validation.valid) process.exitCode = 1;
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
262
315
|
if (command === 'scan') {
|
|
263
316
|
const result = scanChange(projectRoot, changeName, { profile });
|
|
264
317
|
console.log(JSON.stringify(summary({ ...result, changed: false, paths: undefined }), null, 2));
|
|
@@ -42,7 +42,7 @@ function nextEntityId(prefix, scope, existingIds = []) {
|
|
|
42
42
|
|
|
43
43
|
function isValidEntityId(entityId, expectedType = '') {
|
|
44
44
|
const value = String(entityId || '').trim().toUpperCase();
|
|
45
|
-
if (!/^(?:CHG|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value)) {
|
|
45
|
+
if (!/^(?:CHG|OBJ|CAP|STMT|AC|CON|DES|TASK|ART|SEC|SNAP)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/.test(value)) {
|
|
46
46
|
return false;
|
|
47
47
|
}
|
|
48
48
|
if (expectedType && entityTypeForId(value) !== expectedType) {
|
|
@@ -236,7 +236,11 @@ function buildIdentityCatalog(projectRoot, options = {}) {
|
|
|
236
236
|
|
|
237
237
|
for (const candidate of identityRoots(root)) {
|
|
238
238
|
if (excluded && path.resolve(candidate.changeDir) === excluded) continue;
|
|
239
|
-
const
|
|
239
|
+
const snapshot = candidate.scope === 'archive'
|
|
240
|
+
? readJson(path.join(candidate.changeDir, 'archive-ontology.json')) : null;
|
|
241
|
+
// The archive directory gains a date prefix, but facts were frozen with the logical change name.
|
|
242
|
+
const logicalChange = snapshot && snapshot.change ? snapshot.change : candidate.change;
|
|
243
|
+
const facts = normalizeFacts(parseChangeArtifacts(root, logicalChange, {
|
|
240
244
|
changeDir: candidate.changeDir,
|
|
241
245
|
persistStructuralIdentities: false,
|
|
242
246
|
}));
|