kld-sdd 2.7.8-2 → 2.7.8-4
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 +2 -0
- package/USABILITY.md +52 -0
- package/bin/kld-sdd-init.js +700 -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 +450 -711
- package/lib/managed-hook-hashes.json +24 -0
- package/lib/settings-merge.js +102 -25
- package/lib/skills-bundle.js +2 -4
- package/lib/spec-choice.js +85 -0
- package/lib/tool-profiles.js +2 -2
- package/lib/uninstall.js +764 -0
- package/package.json +3 -3
- package/skywalk-sdd/index.cjs +1 -1
- package/skywalk-sdd/kb-upload.cjs +64 -49
- package/skywalk-sdd/lib/shared.cjs +62 -41
- package/skywalk-sdd/lib/test-execution.cjs +20 -0
- package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
- package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
- package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
- package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
- package/skywalk-sdd/ontology/workspace-layout.cjs +46 -16
- package/templates/git-hooks/commit-msg +17 -99
- 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 +61 -240
- 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-consistency-check.cjs +13 -31
- package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
- package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
- package/templates/openspec/proposal.md +1 -1
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +24 -22
- 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 +157 -11
- package/index.js +0 -13
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kld-sdd",
|
|
3
|
-
"version": "2.7.8-
|
|
3
|
+
"version": "2.7.8-4",
|
|
4
4
|
"description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "lib/init.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kld-sdd": "bin/kld-sdd-init.js",
|
|
8
8
|
"kld-sdd-init": "bin/kld-sdd-init.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"bench:check": "node test/benchmark-check.cjs",
|
|
12
|
-
"test": "node --test test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs
|
|
12
|
+
"test": "node --test test/delivery-hooks.test.cjs test/execution-friction.test.cjs test/kb-fact-lifecycle.test.cjs test/workflow-usability.test.cjs test/check-efficiency.test.cjs && node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/prompt-spec-path.cjs && node test/spec-detect.cjs && node test/workspace-layout.cjs && node test/sync-repos-spec-choice.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node --test test/evidence-integrity-oracle.cjs test/evidence-integrity-wiring.cjs test/change-report-correctness.cjs test/report-metrics-correctness.cjs test/change-report-model-ui.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/deploy-strategy.cjs && node test/uninstall.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs && node test/init-report-runtime.cjs && node test/init-output-verbosity.cjs && node --test test/usage-contract.test.cjs test/user-config.test.cjs test/usage-runtime-removal.test.cjs test/runtime/usage-reporter.test.cjs test/runtime/usage-reporting.test.cjs test/runtime/git-identity.test.cjs test/runtime/kb-state-location.test.cjs && node test/runtime/metrics-v3.test.cjs && node test/runtime/fixback.test.cjs && node test/progress-package-artifact.test.cjs && node --test test/progress-package-install.e2e.cjs && node test/consistency-check-core.cjs"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"kld",
|
package/skywalk-sdd/index.cjs
CHANGED
|
@@ -8610,7 +8610,7 @@ SDD Telemetry CLI - 流程度量采集工具
|
|
|
8610
8610
|
check-task 扫描变更目录 tasks.md 并勾选指定 task_id
|
|
8611
8611
|
archive-docs 将 Simple/Full spec 变更真实移动到 openspec/changes/archive/,并可结束 archive 阶段生成报告
|
|
8612
8612
|
kb-upload 知识库上传(文件夹自动打包 / zip 直传),支持随时入库
|
|
8613
|
-
semantic-identity 生成或复用实体/版本 ID
|
|
8613
|
+
semantic-identity 生成或复用实体/版本 ID(完整 UUID,界面可显示短标识);无需网络和中央 ID 服务
|
|
8614
8614
|
semantic-scan 只读解析并校验当前 Change
|
|
8615
8615
|
semantic-reconcile 全量对账文件与本地工作态本体实例
|
|
8616
8616
|
semantic-check 全量对账并在通过时标记 pending;--include-tasks 同时返回任务状态、评审输入清单和程序耗时
|
|
@@ -28,7 +28,8 @@ const {
|
|
|
28
28
|
ensureDir,
|
|
29
29
|
copyDirRecursive,
|
|
30
30
|
resolveSpecRootFast,
|
|
31
|
-
|
|
31
|
+
KB_STATE_FILENAME,
|
|
32
|
+
isKbStateHostDir,
|
|
32
33
|
uploadFile,
|
|
33
34
|
httpGetJson,
|
|
34
35
|
} = require('./lib/shared.cjs');
|
|
@@ -61,6 +62,18 @@ function assertIngestionResult(result) {
|
|
|
61
62
|
return job;
|
|
62
63
|
}
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 解析 kb-state.json 的权威路径:`<specRoot>/kb-state.json`。
|
|
67
|
+
*
|
|
68
|
+
* 位置校验已由 `resolveSpecRoot` 在入口完成(本函数只做路径拼接)。
|
|
69
|
+
*
|
|
70
|
+
* @param {string} specRoot spec 包裹包根目录
|
|
71
|
+
* @returns {string} kb-state.json 绝对路径
|
|
72
|
+
*/
|
|
73
|
+
function resolveKbStatePath(specRoot) {
|
|
74
|
+
return path.join(path.resolve(specRoot), KB_STATE_FILENAME);
|
|
75
|
+
}
|
|
76
|
+
|
|
64
77
|
/**
|
|
65
78
|
* 读取 kb-state.json 中指定 change 的 archive 缓存。
|
|
66
79
|
* @param {string} specRoot
|
|
@@ -68,9 +81,7 @@ function assertIngestionResult(result) {
|
|
|
68
81
|
* @returns {object|null} { archive_id, content_hash, uploaded_at }
|
|
69
82
|
*/
|
|
70
83
|
function readArchiveCache(specRoot, changeName, context) {
|
|
71
|
-
const
|
|
72
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
73
|
-
const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
|
|
84
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
74
85
|
const state = readJsonSafe(statePath);
|
|
75
86
|
if (!state || !state.archiveCache) return null;
|
|
76
87
|
const key = cacheKey(changeName, context);
|
|
@@ -85,9 +96,7 @@ function readArchiveCache(specRoot, changeName, context) {
|
|
|
85
96
|
* @param {string} contentHash
|
|
86
97
|
*/
|
|
87
98
|
function updateKbStateCache(specRoot, changeName, archiveId, contentHash, context) {
|
|
88
|
-
const
|
|
89
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
90
|
-
const statePath = fs.existsSync(directPath) ? directPath : legacyPath;
|
|
99
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
91
100
|
const state = readJsonSafe(statePath) || {};
|
|
92
101
|
if (!state.archiveCache) state.archiveCache = {};
|
|
93
102
|
const key = cacheKey(changeName, context);
|
|
@@ -116,8 +125,18 @@ const DEFAULT_LEVEL = 'propose-spec';
|
|
|
116
125
|
// ── 工具函数 ──────────────────────────────────────────
|
|
117
126
|
|
|
118
127
|
function resolveSpecRoot(explicit) {
|
|
119
|
-
|
|
120
|
-
|
|
128
|
+
const root = explicit ? path.resolve(explicit) : resolveSpecRootFast(__dirname);
|
|
129
|
+
// 位置校验前移:specRoot 必须是 spec 包裹包根,后续所有 kb-state.json /
|
|
130
|
+
// project-identity.json 读取都建立在它之上。在此 fail-fast,避免流程走到
|
|
131
|
+
// 中途才因「文件不存在」报出误导性错误(真正原因是路径指错了地方)。
|
|
132
|
+
if (!isKbStateHostDir(root)) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`kb-state.json 位置校验失败:${root} 不是 spec 包裹包根目录。\n` +
|
|
135
|
+
'kb-state.json 只允许位于 spec 包裹包根(由 /opsx-kb-config 写入)。\n' +
|
|
136
|
+
'请确认 --project 指向 *-sdd-specs 包裹包,或运行 /opsx-kb-config 重新配置。'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return root;
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
/**
|
|
@@ -139,36 +158,28 @@ function resolveProjectId(specRoot) {
|
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
/**
|
|
142
|
-
* 读取 KB 配置(api, apiKey, tenantKey, targets
|
|
143
|
-
*
|
|
161
|
+
* 读取 KB 配置(api, apiKey, tenantKey, targets)。
|
|
162
|
+
*
|
|
163
|
+
* 位置校验:只读 `<specRoot>/kb-state.json`(specRoot 的 spec 根身份已由
|
|
164
|
+
* resolveSpecRoot 在入口校验)。
|
|
144
165
|
*/
|
|
145
166
|
function readKbState(specRoot) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
// 兼容旧路径:specRoot 同级 .shared/kb-state.json
|
|
149
|
-
const legacyPath = path.join(specRoot, '..', '.shared', 'kb-state.json');
|
|
150
|
-
if (fs.existsSync(directPath)) {
|
|
151
|
-
try {
|
|
152
|
-
return readJson(directPath);
|
|
153
|
-
} catch (e) {
|
|
154
|
-
throw new Error(
|
|
155
|
-
`KB 配置解析失败 (${directPath}): ${e.message}\n` +
|
|
156
|
-
'请运行 /opsx-kb-config 重新配置知识库。'
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
// 退回到多路径搜索(从 specRoot 向上查找)
|
|
161
|
-
const { config, path: foundPath, warnings } = loadKbState(specRoot);
|
|
162
|
-
if (warnings.length > 0) {
|
|
163
|
-
for (const w of warnings) console.warn(`⚠️ ${w}`);
|
|
164
|
-
}
|
|
165
|
-
if (!config) {
|
|
167
|
+
const statePath = resolveKbStatePath(specRoot);
|
|
168
|
+
if (!fs.existsSync(statePath)) {
|
|
166
169
|
throw new Error(
|
|
167
170
|
'KB 配置不存在(kb-state.json 缺失)。\n' +
|
|
171
|
+
`期望位置: ${statePath}\n` +
|
|
168
172
|
'请运行 /opsx-kb-config 配置知识库。'
|
|
169
173
|
);
|
|
170
174
|
}
|
|
171
|
-
|
|
175
|
+
try {
|
|
176
|
+
return readJson(statePath);
|
|
177
|
+
} catch (e) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`KB 配置解析失败 (${statePath}): ${e.message}\n` +
|
|
180
|
+
'请运行 /opsx-kb-config 重新配置知识库。'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
172
183
|
}
|
|
173
184
|
|
|
174
185
|
/**
|
|
@@ -332,6 +343,7 @@ async function main() {
|
|
|
332
343
|
try {
|
|
333
344
|
// ── Zip 模式 ──
|
|
334
345
|
if (args.package) {
|
|
346
|
+
if (args['validate-only']) throw new Error('--validate-only 仅支持 --folder/--change 本地语义校验;未执行归档上传');
|
|
335
347
|
const zipPath = path.resolve(args.package);
|
|
336
348
|
if (!fs.existsSync(zipPath)) {
|
|
337
349
|
throw new Error(`zip 文件不存在: ${zipPath}`);
|
|
@@ -370,9 +382,12 @@ async function main() {
|
|
|
370
382
|
changeName = path.basename(folderPath);
|
|
371
383
|
}
|
|
372
384
|
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
385
|
+
const changesRoot = fs.realpathSync(path.resolve(specRoot, 'openspec', 'changes'));
|
|
386
|
+
const expectedPath = path.resolve(changesRoot, changeName);
|
|
387
|
+
const expectedFolder = fs.existsSync(expectedPath) ? fs.realpathSync(expectedPath) : expectedPath;
|
|
388
|
+
const suppliedFolder = fs.existsSync(folderPath) ? fs.realpathSync(folderPath) : path.resolve(folderPath);
|
|
389
|
+
if (suppliedFolder !== expectedFolder || !expectedFolder.startsWith(changesRoot + path.sep)) {
|
|
390
|
+
throw new Error('文件夹上传仅接受当前 Spec 仓 openspec/changes/<change>;已归档内容请用 --package 指定归档包');
|
|
376
391
|
}
|
|
377
392
|
|
|
378
393
|
if (!fs.existsSync(folderPath)) {
|
|
@@ -389,19 +404,6 @@ async function main() {
|
|
|
389
404
|
// 1. 校验必需文件
|
|
390
405
|
validateRequiredFiles(folderPath, minimumLevel);
|
|
391
406
|
|
|
392
|
-
// 2. 解析 project_id
|
|
393
|
-
const projectId = resolveProjectId(specRoot);
|
|
394
|
-
console.log(`✓ project_id: ${projectId}`);
|
|
395
|
-
|
|
396
|
-
// 3. 读取 KB 配置 + 预检
|
|
397
|
-
const state = readKbState(specRoot);
|
|
398
|
-
if (!state.apiKey) {
|
|
399
|
-
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
400
|
-
}
|
|
401
|
-
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
402
|
-
preCheckProjectId(projectId, state, target);
|
|
403
|
-
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
404
|
-
|
|
405
407
|
// 4. 仅校验模式(含语义校验)
|
|
406
408
|
if (args['validate-only']) {
|
|
407
409
|
const runtime = require('./ontology/runtime.cjs');
|
|
@@ -417,10 +419,23 @@ async function main() {
|
|
|
417
419
|
throw new Error(`语义校验失败:\n${errors.join('\n')}`);
|
|
418
420
|
}
|
|
419
421
|
console.log(`✓ 语义校验通过 (profile: ${semanticResult.profile}, revision: ${semanticResult.revision})`);
|
|
420
|
-
console.log('\n✓
|
|
422
|
+
console.log('\n✓ 本地语义校验完成(--validate-only,未联网;不代表权限、发布基线或入库已验证)');
|
|
421
423
|
return;
|
|
422
424
|
}
|
|
423
425
|
|
|
426
|
+
// 2. 解析 project_id
|
|
427
|
+
const projectId = resolveProjectId(specRoot);
|
|
428
|
+
console.log(`✓ project_id: ${projectId}`);
|
|
429
|
+
|
|
430
|
+
// 3. 读取 KB 配置 + 预检
|
|
431
|
+
const state = readKbState(specRoot);
|
|
432
|
+
if (!state.apiKey) {
|
|
433
|
+
throw new Error('KB 配置中无 apiKey,请运行 /opsx-kb-config 配置知识库。');
|
|
434
|
+
}
|
|
435
|
+
const target = resolveUploadTarget(state, { spaceId: args['space-id'], kbId: args['kb-id'] });
|
|
436
|
+
preCheckProjectId(projectId, state, target);
|
|
437
|
+
const cacheContext = { api: state.api || 'http://localhost:8090/api', spaceId: target.spaceId, kbId: target.kbId };
|
|
438
|
+
|
|
424
439
|
// 5. 构建 Archive Package
|
|
425
440
|
console.log('');
|
|
426
441
|
const { zipPath, manifest, stagingDir } = buildPackageFromFolder(specRoot, changeName, {
|
|
@@ -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,
|
|
@@ -17,6 +17,26 @@ function parseCounts(output) {
|
|
|
17
17
|
&& total === passed + failed + (skipped || 0) + (todo || 0)) {
|
|
18
18
|
return { counts_known: true, passed, failed, skipped: (skipped || 0) + (todo || 0) };
|
|
19
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
|
+
}
|
|
20
40
|
return { counts_known: false };
|
|
21
41
|
}
|
|
22
42
|
|
|
@@ -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,8 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const sddConfig = require('./sdd-config.cjs');
|
|
6
6
|
const workspaceLayout = require('./workspace-layout.cjs');
|
|
7
|
+
// spec 判据的唯一真相源(spec-detect 对本模块无依赖,不构成循环)
|
|
8
|
+
const specDetect = require('./spec-detect.cjs');
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* 从 anchorRoot 向上遍历目录树,查找 .sdd-spec-root 配置文件。
|
|
@@ -28,6 +30,34 @@ function resolveHint(anchorRoot) {
|
|
|
28
30
|
return null;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 从 `.sdd-workspace.yaml` 的 `spec_path` 解析 spec 根(向上逐级查找)。
|
|
35
|
+
*
|
|
36
|
+
* 委托 `spec-detect.resolveWorkspaceYamlUpward` —— 该解析在 spec-detect 中
|
|
37
|
+
* 同时用于「候选枚举的 tier 0 来源」,两处共用一份实现,避免漂移。
|
|
38
|
+
*/
|
|
39
|
+
function resolveWorkspaceYamlSpecPath(anchorRoot) {
|
|
40
|
+
return specDetect.resolveWorkspaceYamlUpward(anchorRoot);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 解析 spec 包裹包根目录。
|
|
45
|
+
*
|
|
46
|
+
* 优先级(权威声明 → 结构判据 → 回落):
|
|
47
|
+
* 1. `.sdd-spec-root` 指针文件(init / 运行时写入)
|
|
48
|
+
* 2. `.sdd.yaml` layout: mono(单仓,仓内包裹包)
|
|
49
|
+
* 3. `.sdd-workspace.yaml` spec_path(多仓工作区编排)
|
|
50
|
+
* 4. 工作区探测:子仓结构判据命中的 spec 包
|
|
51
|
+
* 5. 非 Git 子目录包裹包
|
|
52
|
+
* 6. 回落为入参本身(调用方用 isSpecPackageRoot 区分「就是 spec 根」与「解析失败」)
|
|
53
|
+
*
|
|
54
|
+
* 第 3 步是**新增**的:多仓工作区根执行文档类命令时,权威链此前不读
|
|
55
|
+
* `.sdd-workspace.yaml`,只能靠第 4 步的结构判据猜;而 skills 的
|
|
56
|
+
* `opsx-consistency-check/scripts.cjs` 早已自己解析该文件 —— 两处不一致。
|
|
57
|
+
* 现由权威链直接支持,该文件成为与 `.sdd.yaml` 同级的显式声明来源。
|
|
58
|
+
*
|
|
59
|
+
* 前 3 步均**向上逐级查找**,故从工作区子目录(如某个代码仓内)调用也能命中。
|
|
60
|
+
*/
|
|
31
61
|
function resolveSpecProjectRoot(projectRoot = process.cwd()) {
|
|
32
62
|
const root = path.resolve(projectRoot);
|
|
33
63
|
const hinted = resolveHint(root);
|
|
@@ -37,6 +67,9 @@ function resolveSpecProjectRoot(projectRoot = process.cwd()) {
|
|
|
37
67
|
return sddConfig.resolveMonoSpecRoot(root);
|
|
38
68
|
}
|
|
39
69
|
|
|
70
|
+
const fromWorkspaceYaml = resolveWorkspaceYamlSpecPath(root);
|
|
71
|
+
if (fromWorkspaceYaml) return fromWorkspaceYaml;
|
|
72
|
+
|
|
40
73
|
const layout = workspaceLayout.detectWorkspaceLayout(root);
|
|
41
74
|
if (layout.specRepo) return layout.specRepo.abs;
|
|
42
75
|
|
|
@@ -50,7 +83,23 @@ function resolveProjectArg(projectArg = '.', cwd = process.cwd()) {
|
|
|
50
83
|
return resolveSpecProjectRoot(path.resolve(cwd, projectArg));
|
|
51
84
|
}
|
|
52
85
|
|
|
86
|
+
/**
|
|
87
|
+
* 判断 dir 本身是否就是一个 SDD spec 包裹包根。
|
|
88
|
+
*
|
|
89
|
+
* 用于 `log spec-root` 的 resolved 判定:当 resolveSpecProjectRoot 因找不到
|
|
90
|
+
* 任何线索而回落为入参本身时,需区分「dir 就是 spec 根」与「解析失败」。
|
|
91
|
+
*
|
|
92
|
+
* ⚠️ 判据已收敛到 `spec-detect.cjs#isSpecPackageRoot`(全仓唯一判据入口)。
|
|
93
|
+
* 该判据比 `isSpecCandidate` 宽一档 —— 额外接受仅含 `openspec/` 的半成品
|
|
94
|
+
* (`--skip-openspec` 产物),因为本函数的语义是「能否作为 spec 根使用」
|
|
95
|
+
* 而非「是否是当前项目的 spec 仓」。
|
|
96
|
+
*/
|
|
97
|
+
function isSpecPackageRoot(dir) {
|
|
98
|
+
return specDetect.isSpecPackageRoot(dir);
|
|
99
|
+
}
|
|
100
|
+
|
|
53
101
|
module.exports = {
|
|
54
102
|
resolveProjectArg,
|
|
55
103
|
resolveSpecProjectRoot,
|
|
104
|
+
isSpecPackageRoot,
|
|
56
105
|
};
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execFileSync } = require('child_process');
|
|
6
|
+
// spec 判据的唯一真相源(spec-detect 对本模块的依赖是惰性,不构成循环)
|
|
7
|
+
const specDetect = require('./spec-detect.cjs');
|
|
6
8
|
|
|
7
9
|
const CODES = {
|
|
8
10
|
MISSING: 'SDD_YAML_MISSING',
|
|
@@ -156,12 +158,18 @@ function resolveMonoSpecRoot(repoRoot) {
|
|
|
156
158
|
return root;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
/**
|
|
162
|
+
* 目录内容是否"像 spec 包"(任一产物标记存在)。
|
|
163
|
+
*
|
|
164
|
+
* ⚠️ 判据已收敛到 `spec-detect.cjs#hasSpecPackageContent`(全仓唯一判据入口)。
|
|
165
|
+
* 本函数保留仅为向后兼容既有调用点。
|
|
166
|
+
*
|
|
167
|
+
* 语义:仅用于用户**显式给定**路径的校验(`validateSpecPath` 的
|
|
168
|
+
* allowNestedPackage 分支)。不可用于检测 —— 单仓代码仓根可能有 modules.yaml
|
|
169
|
+
* 而无 openspec,仅凭内容无法区分。
|
|
170
|
+
*/
|
|
159
171
|
function looksLikeSpecPackageContent(dir) {
|
|
160
|
-
return (
|
|
161
|
-
fs.existsSync(path.join(dir, 'modules.yaml'))
|
|
162
|
-
|| fs.existsSync(path.join(dir, 'sdd.config.yaml'))
|
|
163
|
-
|| fs.existsSync(path.join(dir, 'openspec', 'changes'))
|
|
164
|
-
);
|
|
172
|
+
return specDetect.hasSpecPackageContent(dir);
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
function gitConfig(cwd, key) {
|
|
@@ -176,6 +184,25 @@ function gitConfig(cwd, key) {
|
|
|
176
184
|
}
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
/**
|
|
188
|
+
* 删除 git config --local sdd.specPath(卸载用)。
|
|
189
|
+
* 配置项不存在时视为成功(幂等)。
|
|
190
|
+
*/
|
|
191
|
+
function unsetSpecPath(codeRepoRoot) {
|
|
192
|
+
if (!gitConfig(codeRepoRoot, 'sdd.specPath')) {
|
|
193
|
+
return { ok: true, removed: false };
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
execFileSync('git', ['config', '--local', '--unset', 'sdd.specPath'], {
|
|
197
|
+
cwd: codeRepoRoot,
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
});
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return { ok: false, message: `无法删除 git config sdd.specPath: ${error.message}` };
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, removed: true };
|
|
204
|
+
}
|
|
205
|
+
|
|
179
206
|
function gitRemoteUrl(cwd, remote = 'origin') {
|
|
180
207
|
try {
|
|
181
208
|
return execFileSync('git', ['remote', 'get-url', remote], {
|
|
@@ -374,6 +401,7 @@ module.exports = {
|
|
|
374
401
|
resolveSpecPath,
|
|
375
402
|
validateSpecPath,
|
|
376
403
|
setSpecPath,
|
|
404
|
+
unsetSpecPath,
|
|
377
405
|
resolveCodeRepoContext,
|
|
378
406
|
gitHeadSha,
|
|
379
407
|
gitWorkingTreeClean,
|