speccore 8.3.21 → 8.3.26
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 +7 -4
- package/dist/cli.js +19 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/analyze.d.ts.map +1 -1
- package/dist/commands/analyze.js +24 -8
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/config.d.ts +3 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +206 -90
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/context-output.d.ts.map +1 -1
- package/dist/commands/context-output.js +0 -10
- package/dist/commands/context-output.js.map +1 -1
- package/dist/commands/dev.js +6 -7
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +140 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/done.js +51 -20
- package/dist/commands/done.js.map +1 -1
- package/dist/commands/execute.d.ts.map +1 -1
- package/dist/commands/execute.js +147 -45
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/help.js +5 -0
- package/dist/commands/help.js.map +1 -1
- package/dist/commands/init.d.ts +0 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +9 -51
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/iteration/split.js +29 -17
- package/dist/commands/iteration/split.js.map +1 -1
- package/dist/commands/status-panel.d.ts.map +1 -1
- package/dist/commands/status-panel.js +13 -48
- package/dist/commands/status-panel.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +32 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +206 -8
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/verdict.d.ts +16 -0
- package/dist/commands/verdict.d.ts.map +1 -0
- package/dist/commands/verdict.js +189 -0
- package/dist/commands/verdict.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +3 -2
- package/dist/commands/verify.js.map +1 -1
- package/dist/core/analyze-engine.d.ts.map +1 -1
- package/dist/core/analyze-engine.js +3 -4
- package/dist/core/analyze-engine.js.map +1 -1
- package/dist/core/arbitration/arbitration-engine.d.ts +36 -0
- package/dist/core/arbitration/arbitration-engine.d.ts.map +1 -0
- package/dist/core/arbitration/arbitration-engine.js +142 -0
- package/dist/core/arbitration/arbitration-engine.js.map +1 -0
- package/dist/core/arbitration/conflict-detector.d.ts +15 -0
- package/dist/core/arbitration/conflict-detector.d.ts.map +1 -0
- package/dist/core/arbitration/conflict-detector.js +78 -0
- package/dist/core/arbitration/conflict-detector.js.map +1 -0
- package/dist/core/arbitration/conflict-types.d.ts +34 -0
- package/dist/core/arbitration/conflict-types.d.ts.map +1 -0
- package/dist/core/arbitration/conflict-types.js +8 -0
- package/dist/core/arbitration/conflict-types.js.map +1 -0
- package/dist/core/arbitration/contract-registry.d.ts +15 -0
- package/dist/core/arbitration/contract-registry.d.ts.map +1 -0
- package/dist/core/arbitration/contract-registry.js +185 -0
- package/dist/core/arbitration/contract-registry.js.map +1 -0
- package/dist/core/arbitration/contract-types.d.ts +33 -0
- package/dist/core/arbitration/contract-types.d.ts.map +1 -0
- package/dist/core/arbitration/contract-types.js +8 -0
- package/dist/core/arbitration/contract-types.js.map +1 -0
- package/dist/core/arbitration/index.d.ts +16 -0
- package/dist/core/arbitration/index.d.ts.map +1 -0
- package/dist/core/arbitration/index.js +32 -0
- package/dist/core/arbitration/index.js.map +1 -0
- package/dist/core/arbitration/l1-auto-arbiter.d.ts +19 -0
- package/dist/core/arbitration/l1-auto-arbiter.d.ts.map +1 -0
- package/dist/core/arbitration/l1-auto-arbiter.js +41 -0
- package/dist/core/arbitration/l1-auto-arbiter.js.map +1 -0
- package/dist/core/arbitration/l2-ai-arbiter.d.ts +19 -0
- package/dist/core/arbitration/l2-ai-arbiter.d.ts.map +1 -0
- package/dist/core/arbitration/l2-ai-arbiter.js +57 -0
- package/dist/core/arbitration/l2-ai-arbiter.js.map +1 -0
- package/dist/core/arbitration/l3-human-arbiter.d.ts +24 -0
- package/dist/core/arbitration/l3-human-arbiter.d.ts.map +1 -0
- package/dist/core/arbitration/l3-human-arbiter.js +65 -0
- package/dist/core/arbitration/l3-human-arbiter.js.map +1 -0
- package/dist/core/arbitration/verdict-generator.d.ts +16 -0
- package/dist/core/arbitration/verdict-generator.d.ts.map +1 -0
- package/dist/core/arbitration/verdict-generator.js +166 -0
- package/dist/core/arbitration/verdict-generator.js.map +1 -0
- package/dist/core/arbitration/verdict-types.d.ts +49 -0
- package/dist/core/arbitration/verdict-types.d.ts.map +1 -0
- package/dist/core/arbitration/verdict-types.js +8 -0
- package/dist/core/arbitration/verdict-types.js.map +1 -0
- package/dist/core/ask-config.d.ts +4 -3
- package/dist/core/ask-config.d.ts.map +1 -1
- package/dist/core/ask-config.js +79 -1
- package/dist/core/ask-config.js.map +1 -1
- package/dist/core/ask-engine.d.ts.map +1 -1
- package/dist/core/ask-engine.js +7 -0
- package/dist/core/ask-engine.js.map +1 -1
- package/dist/core/intent-recognition.d.ts +1 -1
- package/dist/core/intent-recognition.d.ts.map +1 -1
- package/dist/core/intent-recognition.js +10 -0
- package/dist/core/intent-recognition.js.map +1 -1
- package/dist/core/pattern-detector.d.ts +24 -0
- package/dist/core/pattern-detector.d.ts.map +1 -1
- package/dist/core/pattern-detector.js +172 -3
- package/dist/core/pattern-detector.js.map +1 -1
- package/dist/core/quality-audit.d.ts.map +1 -1
- package/dist/core/quality-audit.js +23 -15
- package/dist/core/quality-audit.js.map +1 -1
- package/dist/core/spec-skeleton.d.ts.map +1 -1
- package/dist/core/spec-skeleton.js +35 -12
- package/dist/core/spec-skeleton.js.map +1 -1
- package/dist/core/unified-config.d.ts +161 -27
- package/dist/core/unified-config.d.ts.map +1 -1
- package/dist/core/unified-config.js +856 -82
- package/dist/core/unified-config.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,141 +1,915 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_PROJECT_CONFIG = exports.DEFAULT_CONFIG = exports.PROJECT_CURRENT_SCHEMA_VERSION = exports.CURRENT_SCHEMA_VERSION = void 0;
|
|
3
4
|
exports.loadConfig = loadConfig;
|
|
5
|
+
exports.loadConfigWithMeta = loadConfigWithMeta;
|
|
6
|
+
exports.loadProjectConfig = loadProjectConfig;
|
|
7
|
+
exports.loadProjectConfigWithMeta = loadProjectConfigWithMeta;
|
|
8
|
+
exports.getConfig = getConfig;
|
|
4
9
|
exports.saveConfig = saveConfig;
|
|
10
|
+
exports.saveProjectConfig = saveProjectConfig;
|
|
5
11
|
exports.initConfig = initConfig;
|
|
12
|
+
exports.initProjectConfig = initProjectConfig;
|
|
13
|
+
exports.detectConfigDiff = detectConfigDiff;
|
|
14
|
+
exports.requiresUserConfirmation = requiresUserConfirmation;
|
|
15
|
+
exports.formatConfigDiff = formatConfigDiff;
|
|
16
|
+
exports.upgradeConfig = upgradeConfig;
|
|
17
|
+
exports.upgradeProjectConfig = upgradeProjectConfig;
|
|
18
|
+
exports.resolveEnvVars = resolveEnvVars;
|
|
6
19
|
/**
|
|
7
20
|
* unified-config — .speccore.yml 统一配置入口
|
|
8
21
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
22
|
+
* v8.3.25+: 配置版本化 + 结构校验 + 自动升级
|
|
23
|
+
* - schema_version: 配置结构版本号(CLI 升级时检测)
|
|
24
|
+
* - 运行时校验:加载时自动校验结构完整性
|
|
25
|
+
* - 自动补全:缺失字段自动填充默认值
|
|
26
|
+
* - 升级提示:schema_version < CURRENT_SCHEMA_VERSION 时警告
|
|
11
27
|
*/
|
|
12
28
|
const fs_extra_1 = require("fs-extra");
|
|
13
29
|
const path_1 = require("path");
|
|
14
30
|
const logger_1 = require("../utils/logger");
|
|
15
|
-
|
|
31
|
+
/** 当前系统配置 schema 版本号(.speccore.yml) */
|
|
32
|
+
exports.CURRENT_SCHEMA_VERSION = 1;
|
|
33
|
+
/** 当前项目配置 schema 版本号(.speccore/PROJECT.yaml) */
|
|
34
|
+
exports.PROJECT_CURRENT_SCHEMA_VERSION = 1;
|
|
35
|
+
// ─────────────────────────────────────────
|
|
36
|
+
// 系统配置默认值(.speccore.yml)
|
|
37
|
+
// ─────────────────────────────────────────
|
|
38
|
+
exports.DEFAULT_CONFIG = {
|
|
39
|
+
schema_version: exports.CURRENT_SCHEMA_VERSION,
|
|
40
|
+
quality_gates: { enforce_testing: true, enforce_review: true, require_pr: true },
|
|
41
|
+
arbitration: { enabled: true, mode: 'full' },
|
|
42
|
+
settings: {
|
|
43
|
+
assignee: { enabled: true, mode: 'loose' },
|
|
44
|
+
trace: { enabled: true, auto_annotate: true },
|
|
45
|
+
archive: { auto_cleanup: false },
|
|
46
|
+
plan: { parallel_suggest: true },
|
|
47
|
+
validation: { strict_mode: false },
|
|
48
|
+
sync: { auto_check: true },
|
|
49
|
+
patterns: { auto_save: 'smart' },
|
|
50
|
+
review: { check_assignee: false },
|
|
51
|
+
},
|
|
52
|
+
ask: {
|
|
53
|
+
routing: { mode: 'hybrid', high_threshold: 70, low_threshold: 45, auto_host_ai: true, cache_enabled: true, cache_min_hits: 3 },
|
|
54
|
+
rules: { force_host_ai: false },
|
|
55
|
+
llm_providers: [],
|
|
56
|
+
},
|
|
57
|
+
config_history: [],
|
|
58
|
+
};
|
|
59
|
+
// ─────────────────────────────────────────
|
|
60
|
+
// 项目配置默认值(.speccore/PROJECT.yaml)
|
|
61
|
+
// ─────────────────────────────────────────
|
|
62
|
+
exports.DEFAULT_PROJECT_CONFIG = {
|
|
63
|
+
schema_version: exports.CURRENT_SCHEMA_VERSION,
|
|
16
64
|
project: { name: 'my-project' },
|
|
17
|
-
tech_stack: {},
|
|
18
|
-
team: { members: [] },
|
|
19
65
|
platforms: [],
|
|
20
|
-
git: { default_base: 'main', branch_prefix: 'feature/' },
|
|
66
|
+
git: { default_base: 'main', branch_prefix: 'feature/', protected_branches: ['main', 'master'] },
|
|
21
67
|
code_scope: ['src/'],
|
|
22
|
-
conventions: {},
|
|
23
|
-
quality_gates: { enforce_testing: true, enforce_review: true, require_pr: true },
|
|
24
68
|
};
|
|
25
69
|
const CONFIG_PATH = (0, path_1.join)('.speccore.yml');
|
|
70
|
+
const PROJECT_CONFIG_PATH = (0, path_1.join)('.speccore', 'PROJECT.yaml');
|
|
26
71
|
async function loadConfig() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
72
|
+
const { config, warnings } = await loadConfigWithMeta();
|
|
73
|
+
if (warnings.length > 0) {
|
|
74
|
+
for (const w of warnings)
|
|
75
|
+
logger_1.logger.warn(w);
|
|
76
|
+
}
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
/** 获取配置 + 元信息(warnings、是否迁移过) */
|
|
80
|
+
async function loadConfigWithMeta() {
|
|
81
|
+
const warnings = [];
|
|
82
|
+
if (!(await (0, fs_extra_1.pathExists)(CONFIG_PATH))) {
|
|
83
|
+
warnings.push('⚠️ .speccore.yml 不存在,使用默认配置');
|
|
84
|
+
return { config: { ...exports.DEFAULT_CONFIG }, warnings, migrated: false };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const raw = await (0, fs_extra_1.readFile)(CONFIG_PATH, 'utf-8');
|
|
88
|
+
const parsed = parseYaml(raw);
|
|
89
|
+
// v8.3.25+: 解析环境变量引用(如 apiKey: ${SPECCORE_LLM_KEY})
|
|
90
|
+
const envResult = resolveEnvVars(parsed);
|
|
91
|
+
if (envResult.unresolved.length > 0) {
|
|
92
|
+
warnings.push(`⚠️ 以下环境变量未设置,配置项保持原样: ${envResult.unresolved.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
// 1. 结构校验 + 自动补全
|
|
95
|
+
const validation = validateConfig(parsed);
|
|
96
|
+
if (!validation.valid) {
|
|
97
|
+
warnings.push(`⚠️ .speccore.yml 结构校验失败:`);
|
|
98
|
+
for (const issue of validation.issues.slice(0, 5)) {
|
|
99
|
+
warnings.push(` - ${issue}`);
|
|
100
|
+
}
|
|
101
|
+
if (validation.issues.length > 5) {
|
|
102
|
+
warnings.push(` ... 还有 ${validation.issues.length - 5} 项`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 用默认值深度合并(补全缺失字段)
|
|
106
|
+
const merged = deepMerge(exports.DEFAULT_CONFIG, parsed);
|
|
107
|
+
const config = merged;
|
|
108
|
+
// 2. 版本检测
|
|
109
|
+
if (config.schema_version < exports.CURRENT_SCHEMA_VERSION) {
|
|
110
|
+
warnings.push(`⚠️ .speccore.yml schema_version=${config.schema_version} < ${exports.CURRENT_SCHEMA_VERSION},` +
|
|
111
|
+
`配置结构已过期。建议运行: speccore config --upgrade`);
|
|
112
|
+
}
|
|
113
|
+
else if (config.schema_version > exports.CURRENT_SCHEMA_VERSION) {
|
|
114
|
+
warnings.push(`⚠️ .speccore.yml schema_version=${config.schema_version} > ${exports.CURRENT_SCHEMA_VERSION},` +
|
|
115
|
+
`CLI 版本可能过旧,建议升级 CLI`);
|
|
116
|
+
}
|
|
117
|
+
const migrated = config.schema_version !== exports.CURRENT_SCHEMA_VERSION || !validation.valid;
|
|
118
|
+
return { config, warnings, migrated };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
warnings.push(`⚠️ .speccore.yml 解析失败: ${e.message},使用默认配置`);
|
|
122
|
+
return { config: { ...exports.DEFAULT_CONFIG }, warnings, migrated: false };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** 加载项目配置(简化版) */
|
|
126
|
+
async function loadProjectConfig() {
|
|
127
|
+
const { config, warnings } = await loadProjectConfigWithMeta();
|
|
128
|
+
if (warnings.length > 0) {
|
|
129
|
+
for (const w of warnings)
|
|
130
|
+
logger_1.logger.warn(w);
|
|
131
|
+
}
|
|
132
|
+
return config;
|
|
133
|
+
}
|
|
134
|
+
/** 获取项目配置 + 元信息 */
|
|
135
|
+
async function loadProjectConfigWithMeta() {
|
|
136
|
+
const warnings = [];
|
|
137
|
+
if (!(await (0, fs_extra_1.pathExists)(PROJECT_CONFIG_PATH))) {
|
|
138
|
+
warnings.push('⚠️ .speccore/PROJECT.yaml 不存在,使用默认项目配置');
|
|
139
|
+
return { config: { ...exports.DEFAULT_PROJECT_CONFIG }, warnings, migrated: false };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
const raw = await (0, fs_extra_1.readFile)(PROJECT_CONFIG_PATH, 'utf-8');
|
|
143
|
+
const parsed = parseYaml(raw);
|
|
144
|
+
// 解析环境变量
|
|
145
|
+
const envResult = resolveEnvVars(parsed);
|
|
146
|
+
if (envResult.unresolved.length > 0) {
|
|
147
|
+
warnings.push(`⚠️ 以下环境变量未设置,配置项保持原样: ${envResult.unresolved.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
// 1. 结构校验
|
|
150
|
+
const validation = validateProjectConfig(parsed);
|
|
151
|
+
if (!validation.valid) {
|
|
152
|
+
warnings.push(`⚠️ .speccore/PROJECT.yaml 结构校验失败:`);
|
|
153
|
+
for (const issue of validation.issues.slice(0, 5)) {
|
|
154
|
+
warnings.push(` - ${issue}`);
|
|
155
|
+
}
|
|
156
|
+
if (validation.issues.length > 5) {
|
|
157
|
+
warnings.push(` ... 还有 ${validation.issues.length - 5} 项`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// 用默认值深度合并
|
|
161
|
+
const merged = deepMerge(exports.DEFAULT_PROJECT_CONFIG, parsed);
|
|
162
|
+
const config = merged;
|
|
163
|
+
// 2. 版本检测
|
|
164
|
+
if (config.schema_version < exports.PROJECT_CURRENT_SCHEMA_VERSION) {
|
|
165
|
+
warnings.push(`⚠️ .speccore/PROJECT.yaml schema_version=${config.schema_version} < ${exports.PROJECT_CURRENT_SCHEMA_VERSION},` +
|
|
166
|
+
`配置结构已过期。建议运行: speccore config --upgrade --project`);
|
|
31
167
|
}
|
|
32
|
-
|
|
33
|
-
|
|
168
|
+
else if (config.schema_version > exports.PROJECT_CURRENT_SCHEMA_VERSION) {
|
|
169
|
+
warnings.push(`⚠️ .speccore/PROJECT.yaml schema_version=${config.schema_version} > ${exports.PROJECT_CURRENT_SCHEMA_VERSION},` +
|
|
170
|
+
`CLI 版本可能过旧,建议升级 CLI`);
|
|
34
171
|
}
|
|
172
|
+
const migrated = config.schema_version !== exports.PROJECT_CURRENT_SCHEMA_VERSION || !validation.valid;
|
|
173
|
+
return { config, warnings, migrated };
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
warnings.push(`⚠️ .speccore/PROJECT.yaml 解析失败: ${e.message},使用默认配置`);
|
|
177
|
+
return { config: { ...exports.DEFAULT_PROJECT_CONFIG }, warnings, migrated: false };
|
|
35
178
|
}
|
|
36
|
-
return DEFAULT_CONFIG;
|
|
37
179
|
}
|
|
180
|
+
/** 兼容旧代码:getConfig = loadConfig */
|
|
181
|
+
async function getConfig() {
|
|
182
|
+
return loadConfig();
|
|
183
|
+
}
|
|
184
|
+
// ─────────────────────────────────────────
|
|
185
|
+
// 配置保存(自动写入 schema_version)
|
|
186
|
+
// ─────────────────────────────────────────
|
|
38
187
|
async function saveConfig(config) {
|
|
39
|
-
const
|
|
188
|
+
const enriched = { ...config, schema_version: exports.CURRENT_SCHEMA_VERSION };
|
|
189
|
+
const yaml = toYaml(enriched);
|
|
40
190
|
await (0, fs_extra_1.writeFile)(CONFIG_PATH, yaml, 'utf-8');
|
|
41
191
|
}
|
|
192
|
+
async function saveProjectConfig(config) {
|
|
193
|
+
const enriched = { ...config, schema_version: exports.CURRENT_SCHEMA_VERSION };
|
|
194
|
+
const yaml = toProjectYaml(enriched);
|
|
195
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)('.speccore'));
|
|
196
|
+
await (0, fs_extra_1.writeFile)(PROJECT_CONFIG_PATH, yaml, 'utf-8');
|
|
197
|
+
}
|
|
42
198
|
/**
|
|
43
|
-
* 初始化时生成 .speccore.yml
|
|
199
|
+
* 初始化时生成 .speccore.yml(系统配置)
|
|
44
200
|
*/
|
|
45
|
-
async function initConfig(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
config.project.name = projectName;
|
|
49
|
-
await saveConfig(config);
|
|
50
|
-
logger_1.logger.info(` 📄 .speccore.yml 已生成`);
|
|
201
|
+
async function initConfig() {
|
|
202
|
+
await saveConfig({ ...exports.DEFAULT_CONFIG });
|
|
203
|
+
logger_1.logger.info(` 📄 .speccore.yml 已生成 (schema_version=${exports.CURRENT_SCHEMA_VERSION})`);
|
|
51
204
|
}
|
|
52
205
|
/**
|
|
53
|
-
*
|
|
206
|
+
* 初始化时生成 .speccore/PROJECT.yaml(项目配置)
|
|
207
|
+
*/
|
|
208
|
+
async function initProjectConfig(projectName) {
|
|
209
|
+
const config = {
|
|
210
|
+
...exports.DEFAULT_PROJECT_CONFIG,
|
|
211
|
+
project: { ...exports.DEFAULT_PROJECT_CONFIG.project, name: projectName || 'my-project' },
|
|
212
|
+
};
|
|
213
|
+
await saveProjectConfig(config);
|
|
214
|
+
logger_1.logger.info(` 📄 .speccore/PROJECT.yaml 已生成 (schema_version=${exports.CURRENT_SCHEMA_VERSION})`);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 系统配置版本迁移注册表(.speccore.yml)
|
|
218
|
+
* 键 = 目标 schema_version,值 = 从上一版本升级到该版本需要执行的迁移步骤。
|
|
219
|
+
*/
|
|
220
|
+
const MIGRATIONS = {
|
|
221
|
+
// schema_version 1 是初始版本,无迁移步骤
|
|
222
|
+
1: [],
|
|
223
|
+
// 未来版本示例(预留):
|
|
224
|
+
// 2: [
|
|
225
|
+
// {
|
|
226
|
+
// type: 'rename',
|
|
227
|
+
// description: 'settings.assignee.mode: strict_mode → strict',
|
|
228
|
+
// migrate: (cfg) => {
|
|
229
|
+
// const old = (cfg as any).settings?.assignee?.strict_mode;
|
|
230
|
+
// if (old !== undefined) {
|
|
231
|
+
// (cfg.settings.assignee as any).mode = old ? 'strict' : 'loose';
|
|
232
|
+
// delete (cfg.settings.assignee as any).strict_mode;
|
|
233
|
+
// return true;
|
|
234
|
+
// }
|
|
235
|
+
// return false;
|
|
236
|
+
// },
|
|
237
|
+
// },
|
|
238
|
+
// ],
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* 项目配置版本迁移注册表(.speccore/PROJECT.yaml)
|
|
54
242
|
*/
|
|
243
|
+
const PROJECT_MIGRATIONS = {
|
|
244
|
+
1: [],
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* 执行从当前 config.schema_version 到目标版本的所有迁移步骤。
|
|
248
|
+
* 返回迁移过程中产生的日志信息。
|
|
249
|
+
*/
|
|
250
|
+
function runMigrations(config, migrations, currentVersion) {
|
|
251
|
+
const logs = [];
|
|
252
|
+
const startVersion = config.schema_version;
|
|
253
|
+
for (let v = startVersion + 1; v <= currentVersion; v++) {
|
|
254
|
+
const steps = migrations[v];
|
|
255
|
+
if (!steps || steps.length === 0)
|
|
256
|
+
continue;
|
|
257
|
+
for (const step of steps) {
|
|
258
|
+
try {
|
|
259
|
+
const changed = step.migrate(config);
|
|
260
|
+
if (changed) {
|
|
261
|
+
logs.push(` [v${v}] ${step.description}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (e) {
|
|
265
|
+
logs.push(` [v${v}] ⚠️ 迁移失败: ${step.description} — ${e.message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return logs;
|
|
270
|
+
}
|
|
271
|
+
// ─────────────────────────────────────────
|
|
272
|
+
// 枚举约束定义(字段路径 → 合法值列表)
|
|
273
|
+
// ─────────────────────────────────────────
|
|
274
|
+
/** 支持通配符 * 的路径模式 */
|
|
275
|
+
const ENUM_CONSTRAINTS = {
|
|
276
|
+
'platforms.*.type': ['frontend', 'backend', 'infra'],
|
|
277
|
+
'arbitration.mode': ['full', 'l1-only', 'report-only'],
|
|
278
|
+
'settings.assignee.mode': ['strict', 'loose', 'off'],
|
|
279
|
+
'settings.patterns.auto_save': ['off', 'smart', 'aggressive'],
|
|
280
|
+
'ask.routing.mode': ['hybrid', 'cli-only', 'host-ai-only'],
|
|
281
|
+
'ask.llm_providers.*.type': ['ollama', 'openai', 'anthropic', 'custom'],
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* 按通配符路径模式遍历对象,获取所有匹配的值
|
|
285
|
+
* 示例: 'platforms.*.type' → [{path:'platforms.0.type', value:'frontend'}, ...]
|
|
286
|
+
*/
|
|
287
|
+
function getValuesAtPath(obj, pathPattern) {
|
|
288
|
+
const parts = pathPattern.split('.');
|
|
289
|
+
const results = [];
|
|
290
|
+
function traverse(current, currentPath, idx) {
|
|
291
|
+
if (idx >= parts.length) {
|
|
292
|
+
results.push({ path: currentPath.join('.'), value: current });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const part = parts[idx];
|
|
296
|
+
if (part === '*') {
|
|
297
|
+
if (Array.isArray(current)) {
|
|
298
|
+
for (let i = 0; i < current.length; i++) {
|
|
299
|
+
traverse(current[i], [...currentPath, String(i)], idx + 1);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
else if (isPlainObject(current)) {
|
|
303
|
+
for (const key of Object.keys(current)) {
|
|
304
|
+
traverse(current[key], [...currentPath, key], idx + 1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
else if (isPlainObject(current) && part in current) {
|
|
309
|
+
traverse(current[part], [...currentPath, part], idx + 1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
traverse(obj, [], 0);
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 检测配置中的枚举值是否超出合法范围
|
|
317
|
+
*/
|
|
318
|
+
function checkEnumConstraints(config) {
|
|
319
|
+
const changes = [];
|
|
320
|
+
for (const [pattern, allowedValues] of Object.entries(ENUM_CONSTRAINTS)) {
|
|
321
|
+
const matches = getValuesAtPath(config, pattern);
|
|
322
|
+
for (const { path, value } of matches) {
|
|
323
|
+
if (typeof value === 'string' && !allowedValues.includes(value)) {
|
|
324
|
+
changes.push({
|
|
325
|
+
path,
|
|
326
|
+
current: [value],
|
|
327
|
+
expected: allowedValues,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return changes;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* 递归比较当前配置与目标配置(DEFAULT_CONFIG),检测所有差异
|
|
336
|
+
*/
|
|
337
|
+
function detectConfigDiff(current, target, path = '') {
|
|
338
|
+
const diff = { added: [], removed: [], typeChanged: [], enumChanged: [], structureChanged: [] };
|
|
339
|
+
// 根节点类型不同(极少发生,如整体从对象变成数组)
|
|
340
|
+
if (typeof current !== typeof target) {
|
|
341
|
+
if (path === '') {
|
|
342
|
+
diff.structureChanged.push({ path: '(root)', description: `根节点类型从 ${typeof current} 变为 ${typeof target}` });
|
|
343
|
+
}
|
|
344
|
+
return diff;
|
|
345
|
+
}
|
|
346
|
+
// 两者都是对象(非数组)
|
|
347
|
+
if (isPlainObject(current) && isPlainObject(target)) {
|
|
348
|
+
const currentKeys = Object.keys(current);
|
|
349
|
+
const targetKeys = Object.keys(target);
|
|
350
|
+
// 检测删除的字段(当前有,目标没有)
|
|
351
|
+
for (const key of currentKeys) {
|
|
352
|
+
if (!targetKeys.includes(key)) {
|
|
353
|
+
diff.removed.push(path ? `${path}.${key}` : key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// 检测新增和变更的字段
|
|
357
|
+
for (const key of targetKeys) {
|
|
358
|
+
const currentVal = current[key];
|
|
359
|
+
const targetVal = target[key];
|
|
360
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
361
|
+
if (!(key in current)) {
|
|
362
|
+
// 新增字段
|
|
363
|
+
diff.added.push(childPath);
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
// 两者都存在,递归比较
|
|
367
|
+
const childDiff = detectConfigDiff(currentVal, targetVal, childPath);
|
|
368
|
+
diff.added.push(...childDiff.added);
|
|
369
|
+
diff.removed.push(...childDiff.removed);
|
|
370
|
+
diff.typeChanged.push(...childDiff.typeChanged);
|
|
371
|
+
diff.enumChanged.push(...childDiff.enumChanged);
|
|
372
|
+
diff.structureChanged.push(...childDiff.structureChanged);
|
|
373
|
+
}
|
|
374
|
+
return diff;
|
|
375
|
+
}
|
|
376
|
+
// 两者都是数组
|
|
377
|
+
if (Array.isArray(current) && Array.isArray(target)) {
|
|
378
|
+
// 如果数组元素类型不同(如 string[] → object[])
|
|
379
|
+
const currentElemType = current.length > 0 ? typeof current[0] : 'empty';
|
|
380
|
+
const targetElemType = target.length > 0 ? typeof target[0] : 'empty';
|
|
381
|
+
if (currentElemType !== targetElemType && currentElemType !== 'empty' && targetElemType !== 'empty') {
|
|
382
|
+
diff.structureChanged.push({
|
|
383
|
+
path: path || '(array)',
|
|
384
|
+
description: `数组元素类型从 ${currentElemType} 变为 ${targetElemType}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return diff;
|
|
388
|
+
}
|
|
389
|
+
// 一个是数组,一个不是
|
|
390
|
+
if (Array.isArray(current) !== Array.isArray(target)) {
|
|
391
|
+
diff.structureChanged.push({
|
|
392
|
+
path: path || '(value)',
|
|
393
|
+
description: `类型从 ${Array.isArray(current) ? 'array' : typeof current} 变为 ${Array.isArray(target) ? 'array' : typeof target}`,
|
|
394
|
+
});
|
|
395
|
+
return diff;
|
|
396
|
+
}
|
|
397
|
+
// 基本类型值不同(不算结构变更,只是值不同)
|
|
398
|
+
// 但可能涉及枚举值非法
|
|
399
|
+
const enumChanges = checkEnumConstraints(current);
|
|
400
|
+
diff.enumChanged.push(...enumChanges);
|
|
401
|
+
return diff;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 判断差异是否需要用户确认(新增字段除外,可自动处理)
|
|
405
|
+
*/
|
|
406
|
+
function requiresUserConfirmation(diff) {
|
|
407
|
+
return diff.removed.length > 0
|
|
408
|
+
|| diff.typeChanged.length > 0
|
|
409
|
+
|| diff.enumChanged.length > 0
|
|
410
|
+
|| diff.structureChanged.length > 0;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* 格式化差异为可读文本
|
|
414
|
+
*/
|
|
415
|
+
function formatConfigDiff(diff) {
|
|
416
|
+
const lines = [];
|
|
417
|
+
if (diff.added.length > 0) {
|
|
418
|
+
lines.push(`📌 新增字段(${diff.added.length} 项,将使用默认值):`);
|
|
419
|
+
for (const p of diff.added)
|
|
420
|
+
lines.push(` + ${p}`);
|
|
421
|
+
}
|
|
422
|
+
if (diff.removed.length > 0) {
|
|
423
|
+
lines.push(`⚠️ 删除字段(${diff.removed.length} 项,配置已过时):`);
|
|
424
|
+
for (const p of diff.removed)
|
|
425
|
+
lines.push(` - ${p}`);
|
|
426
|
+
}
|
|
427
|
+
if (diff.typeChanged.length > 0) {
|
|
428
|
+
lines.push(`🔧 类型变更(${diff.typeChanged.length} 项,需人工确认):`);
|
|
429
|
+
for (const t of diff.typeChanged)
|
|
430
|
+
lines.push(` ~ ${t.path}: ${t.current} → ${t.expected}`);
|
|
431
|
+
}
|
|
432
|
+
if (diff.enumChanged.length > 0) {
|
|
433
|
+
lines.push(`📝 枚举值变更(${diff.enumChanged.length} 项,需人工确认):`);
|
|
434
|
+
for (const e of diff.enumChanged) {
|
|
435
|
+
lines.push(` ~ ${e.path}:`);
|
|
436
|
+
lines.push(` 当前: [${e.current.join(', ')}]`);
|
|
437
|
+
lines.push(` 目标: [${e.expected.join(', ')}]`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (diff.structureChanged.length > 0) {
|
|
441
|
+
lines.push(`🏗️ 结构变更(${diff.structureChanged.length} 项,需人工确认):`);
|
|
442
|
+
for (const s of diff.structureChanged)
|
|
443
|
+
lines.push(` ~ ${s.path}: ${s.description}`);
|
|
444
|
+
}
|
|
445
|
+
return lines;
|
|
446
|
+
}
|
|
447
|
+
// ─────────────────────────────────────────
|
|
448
|
+
// 配置升级(补全缺失字段 + 执行迁移 + 更新版本号)
|
|
449
|
+
// ─────────────────────────────────────────
|
|
450
|
+
async function upgradeConfig() {
|
|
451
|
+
const { config, warnings } = await loadConfigWithMeta();
|
|
452
|
+
if (config.schema_version === exports.CURRENT_SCHEMA_VERSION && warnings.length === 0) {
|
|
453
|
+
logger_1.logger.info(`✅ .speccore.yml 已经是 schema_version=${exports.CURRENT_SCHEMA_VERSION},无需升级`);
|
|
454
|
+
return { config, warnings: [], migrated: false };
|
|
455
|
+
}
|
|
456
|
+
// 1. 检测配置差异(当前配置 vs 目标配置)
|
|
457
|
+
const diff = detectConfigDiff(config, exports.DEFAULT_CONFIG);
|
|
458
|
+
const needsConfirm = requiresUserConfirmation(diff);
|
|
459
|
+
if (needsConfirm) {
|
|
460
|
+
// 生成差异报告文件
|
|
461
|
+
const reportLines = [
|
|
462
|
+
'# .speccore.yml 升级差异报告',
|
|
463
|
+
'',
|
|
464
|
+
`生成时间: ${new Date().toLocaleString('zh-CN')}`,
|
|
465
|
+
`当前 schema_version: ${config.schema_version}`,
|
|
466
|
+
`目标 schema_version: ${exports.CURRENT_SCHEMA_VERSION}`,
|
|
467
|
+
'',
|
|
468
|
+
'## 检测到的变更',
|
|
469
|
+
'',
|
|
470
|
+
...formatConfigDiff(diff),
|
|
471
|
+
'',
|
|
472
|
+
'## 处理建议',
|
|
473
|
+
'',
|
|
474
|
+
'1. **删除字段**: 检查这些字段是否在你的项目中有自定义值,如果有,需要迁移到新字段',
|
|
475
|
+
'2. **类型变更**: 手动修改字段值类型,确保与新版 schema 一致',
|
|
476
|
+
'3. **枚举值变更**: 将字段值改为合法枚举值之一',
|
|
477
|
+
'4. **结构变更**: 重组配置对象结构,确保嵌套关系正确',
|
|
478
|
+
'',
|
|
479
|
+
'修改完成后,重新运行: speccore config --upgrade',
|
|
480
|
+
];
|
|
481
|
+
try {
|
|
482
|
+
const diffPath = (0, path_1.join)('.speccore', 'config', 'upgrade-diff.md');
|
|
483
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)('.speccore', 'config'));
|
|
484
|
+
await (0, fs_extra_1.writeFile)(diffPath, reportLines.join('\n'), 'utf-8');
|
|
485
|
+
logger_1.logger.info(`\n📝 差异报告已保存: ${diffPath}`);
|
|
486
|
+
}
|
|
487
|
+
catch { /* 静默失败 */ }
|
|
488
|
+
logger_1.logger.info('\n⚠️ 检测到配置结构性变更,需要确认:\n');
|
|
489
|
+
for (const line of formatConfigDiff(diff)) {
|
|
490
|
+
logger_1.logger.info(line);
|
|
491
|
+
}
|
|
492
|
+
logger_1.logger.info('\n💡 建议操作:');
|
|
493
|
+
logger_1.logger.info(' 1. 检查上述变更是否影响现有配置');
|
|
494
|
+
logger_1.logger.info(' 2. 手动调整 .speccore.yml 中的相关字段');
|
|
495
|
+
logger_1.logger.info(' 3. 确认无误后重新运行 speccore config --upgrade');
|
|
496
|
+
logger_1.logger.info('');
|
|
497
|
+
// 不执行升级,返回当前配置和差异信息
|
|
498
|
+
return { config, warnings: [...warnings, '配置存在结构性变更,需人工确认后重新升级'], migrated: false };
|
|
499
|
+
}
|
|
500
|
+
// 2. 用默认值深度合并(补全缺失字段)
|
|
501
|
+
const upgraded = deepMerge(exports.DEFAULT_CONFIG, config);
|
|
502
|
+
// 3. 执行跨版本迁移(rename / add / remove / transform)
|
|
503
|
+
const migrationLogs = runMigrations(upgraded, MIGRATIONS, exports.CURRENT_SCHEMA_VERSION);
|
|
504
|
+
// 4. 更新 schema_version
|
|
505
|
+
upgraded.schema_version = exports.CURRENT_SCHEMA_VERSION;
|
|
506
|
+
// 5. 记录升级历史
|
|
507
|
+
const changeParts = [`升级到 schema_version=${exports.CURRENT_SCHEMA_VERSION}`];
|
|
508
|
+
if (migrationLogs.length > 0) {
|
|
509
|
+
changeParts.push(`包含 ${migrationLogs.length} 项迁移`);
|
|
510
|
+
}
|
|
511
|
+
if (diff.added.length > 0) {
|
|
512
|
+
changeParts.push(`自动补全 ${diff.added.length} 个新字段`);
|
|
513
|
+
}
|
|
514
|
+
upgraded.config_history = [
|
|
515
|
+
...(upgraded.config_history || []),
|
|
516
|
+
{
|
|
517
|
+
date: new Date().toISOString().split('T')[0],
|
|
518
|
+
change: changeParts.join(','),
|
|
519
|
+
},
|
|
520
|
+
];
|
|
521
|
+
await saveConfig(upgraded);
|
|
522
|
+
logger_1.logger.info(`✅ .speccore.yml 已升级到 schema_version=${exports.CURRENT_SCHEMA_VERSION}`);
|
|
523
|
+
if (diff.added.length > 0) {
|
|
524
|
+
logger_1.logger.info(` 自动补全 ${diff.added.length} 个新字段(使用默认值)`);
|
|
525
|
+
}
|
|
526
|
+
if (warnings.length > 0) {
|
|
527
|
+
logger_1.logger.info(' 修复了以下问题:');
|
|
528
|
+
for (const w of warnings)
|
|
529
|
+
logger_1.logger.info(` ${w}`);
|
|
530
|
+
}
|
|
531
|
+
if (migrationLogs.length > 0) {
|
|
532
|
+
logger_1.logger.info(' 执行了以下迁移:');
|
|
533
|
+
for (const log of migrationLogs)
|
|
534
|
+
logger_1.logger.info(` ${log}`);
|
|
535
|
+
}
|
|
536
|
+
return { config: upgraded, warnings, migrated: true };
|
|
537
|
+
}
|
|
538
|
+
// ─────────────────────────────────────────
|
|
539
|
+
// 项目配置升级(.speccore/PROJECT.yaml)
|
|
540
|
+
// ─────────────────────────────────────────
|
|
541
|
+
async function upgradeProjectConfig() {
|
|
542
|
+
const { config, warnings } = await loadProjectConfigWithMeta();
|
|
543
|
+
if (config.schema_version === exports.PROJECT_CURRENT_SCHEMA_VERSION && warnings.length === 0) {
|
|
544
|
+
logger_1.logger.info(`✅ .speccore/PROJECT.yaml 已经是 schema_version=${exports.PROJECT_CURRENT_SCHEMA_VERSION},无需升级`);
|
|
545
|
+
return { config, warnings: [], migrated: false };
|
|
546
|
+
}
|
|
547
|
+
// 1. 检测配置差异(当前配置 vs 目标配置)
|
|
548
|
+
const diff = detectConfigDiff(config, exports.DEFAULT_PROJECT_CONFIG);
|
|
549
|
+
const needsConfirm = requiresUserConfirmation(diff);
|
|
550
|
+
if (needsConfirm) {
|
|
551
|
+
// 生成差异报告文件
|
|
552
|
+
const reportLines = [
|
|
553
|
+
'# .speccore/PROJECT.yaml 升级差异报告',
|
|
554
|
+
'',
|
|
555
|
+
`生成时间: ${new Date().toLocaleString('zh-CN')}`,
|
|
556
|
+
`当前 schema_version: ${config.schema_version}`,
|
|
557
|
+
`目标 schema_version: ${exports.PROJECT_CURRENT_SCHEMA_VERSION}`,
|
|
558
|
+
'',
|
|
559
|
+
'## 检测到的变更',
|
|
560
|
+
'',
|
|
561
|
+
...formatConfigDiff(diff),
|
|
562
|
+
'',
|
|
563
|
+
'## 处理建议',
|
|
564
|
+
'',
|
|
565
|
+
'1. **删除字段**: 检查这些字段是否在你的项目中有自定义值,如果有,需要迁移到新字段',
|
|
566
|
+
'2. **类型变更**: 手动修改字段值类型,确保与新版 schema 一致',
|
|
567
|
+
'3. **枚举值变更**: 将字段值改为合法枚举值之一',
|
|
568
|
+
'4. **结构变更**: 重组配置对象结构,确保嵌套关系正确',
|
|
569
|
+
'',
|
|
570
|
+
'修改完成后,重新运行: speccore config --upgrade --project',
|
|
571
|
+
];
|
|
572
|
+
try {
|
|
573
|
+
const diffPath = (0, path_1.join)('.speccore', 'config', 'upgrade-diff-project.md');
|
|
574
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.join)('.speccore', 'config'));
|
|
575
|
+
await (0, fs_extra_1.writeFile)(diffPath, reportLines.join('\n'), 'utf-8');
|
|
576
|
+
logger_1.logger.info(`\n📝 差异报告已保存: ${diffPath}`);
|
|
577
|
+
}
|
|
578
|
+
catch { /* 静默失败 */ }
|
|
579
|
+
logger_1.logger.info('\n⚠️ 检测到项目配置结构性变更,需要确认:\n');
|
|
580
|
+
for (const line of formatConfigDiff(diff)) {
|
|
581
|
+
logger_1.logger.info(line);
|
|
582
|
+
}
|
|
583
|
+
logger_1.logger.info('\n💡 建议操作:');
|
|
584
|
+
logger_1.logger.info(' 1. 检查上述变更是否影响现有配置');
|
|
585
|
+
logger_1.logger.info(' 2. 手动调整 .speccore/PROJECT.yaml 中的相关字段');
|
|
586
|
+
logger_1.logger.info(' 3. 确认无误后重新运行 speccore config --upgrade --project');
|
|
587
|
+
logger_1.logger.info('');
|
|
588
|
+
return { config, warnings: [...warnings, '项目配置存在结构性变更,需人工确认后重新升级'], migrated: false };
|
|
589
|
+
}
|
|
590
|
+
// 2. 用默认值深度合并(补全缺失字段)
|
|
591
|
+
const upgraded = deepMerge(exports.DEFAULT_PROJECT_CONFIG, config);
|
|
592
|
+
// 3. 执行跨版本迁移
|
|
593
|
+
const migrationLogs = runMigrations(upgraded, PROJECT_MIGRATIONS, exports.PROJECT_CURRENT_SCHEMA_VERSION);
|
|
594
|
+
// 4. 更新 schema_version
|
|
595
|
+
upgraded.schema_version = exports.PROJECT_CURRENT_SCHEMA_VERSION;
|
|
596
|
+
await saveProjectConfig(upgraded);
|
|
597
|
+
logger_1.logger.info(`✅ .speccore/PROJECT.yaml 已升级到 schema_version=${exports.PROJECT_CURRENT_SCHEMA_VERSION}`);
|
|
598
|
+
if (diff.added.length > 0) {
|
|
599
|
+
logger_1.logger.info(` 自动补全 ${diff.added.length} 个新字段(使用默认值)`);
|
|
600
|
+
}
|
|
601
|
+
if (warnings.length > 0) {
|
|
602
|
+
logger_1.logger.info(' 修复了以下问题:');
|
|
603
|
+
for (const w of warnings)
|
|
604
|
+
logger_1.logger.info(` ${w}`);
|
|
605
|
+
}
|
|
606
|
+
if (migrationLogs.length > 0) {
|
|
607
|
+
logger_1.logger.info(' 执行了以下迁移:');
|
|
608
|
+
for (const log of migrationLogs)
|
|
609
|
+
logger_1.logger.info(` ${log}`);
|
|
610
|
+
}
|
|
611
|
+
return { config: upgraded, warnings, migrated: true };
|
|
612
|
+
}
|
|
613
|
+
function validateConfig(obj) {
|
|
614
|
+
const issues = [];
|
|
615
|
+
// schema_version
|
|
616
|
+
if (typeof obj.schema_version !== 'number') {
|
|
617
|
+
issues.push('schema_version 必须是数字');
|
|
618
|
+
}
|
|
619
|
+
// quality_gates
|
|
620
|
+
if (isObject(obj.quality_gates)) {
|
|
621
|
+
for (const key of ['enforce_testing', 'enforce_review', 'require_pr']) {
|
|
622
|
+
if (obj.quality_gates[key] !== undefined && typeof obj.quality_gates[key] !== 'boolean') {
|
|
623
|
+
issues.push(`quality_gates.${key} 必须是布尔值`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// arbitration
|
|
628
|
+
if (isObject(obj.arbitration)) {
|
|
629
|
+
if (obj.arbitration.enabled !== undefined && typeof obj.arbitration.enabled !== 'boolean') {
|
|
630
|
+
issues.push('arbitration.enabled 必须是布尔值');
|
|
631
|
+
}
|
|
632
|
+
if (obj.arbitration.mode && !['full', 'l1-only', 'report-only'].includes(obj.arbitration.mode)) {
|
|
633
|
+
issues.push('arbitration.mode 必须是 full/l1-only/report-only 之一');
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// settings
|
|
637
|
+
if (isObject(obj.settings)) {
|
|
638
|
+
const s = obj.settings;
|
|
639
|
+
if (isObject(s.assignee) && s.assignee.mode && !['strict', 'loose', 'off'].includes(s.assignee.mode)) {
|
|
640
|
+
issues.push('settings.assignee.mode 必须是 strict/loose/off 之一');
|
|
641
|
+
}
|
|
642
|
+
if (isObject(s.patterns) && s.patterns.auto_save && !['off', 'smart', 'aggressive'].includes(s.patterns.auto_save)) {
|
|
643
|
+
issues.push('settings.patterns.auto_save 必须是 off/smart/aggressive 之一');
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { valid: issues.length === 0, issues };
|
|
647
|
+
}
|
|
648
|
+
function validateProjectConfig(obj) {
|
|
649
|
+
const issues = [];
|
|
650
|
+
// schema_version
|
|
651
|
+
if (typeof obj.schema_version !== 'number') {
|
|
652
|
+
issues.push('schema_version 必须是数字');
|
|
653
|
+
}
|
|
654
|
+
// project
|
|
655
|
+
if (!isObject(obj.project)) {
|
|
656
|
+
issues.push('project 必须是对象');
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
if (typeof obj.project.name !== 'string')
|
|
660
|
+
issues.push('project.name 必须是字符串');
|
|
661
|
+
}
|
|
662
|
+
// platforms
|
|
663
|
+
if (Array.isArray(obj.platforms)) {
|
|
664
|
+
for (let idx = 0; idx < obj.platforms.length; idx++) {
|
|
665
|
+
const p = obj.platforms[idx];
|
|
666
|
+
if (!isObject(p)) {
|
|
667
|
+
issues.push(`platforms[${idx}] 必须是对象`);
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
if (typeof p.name !== 'string')
|
|
671
|
+
issues.push(`platforms[${idx}].name 必须是字符串`);
|
|
672
|
+
if (p.type && !['frontend', 'backend', 'infra'].includes(p.type)) {
|
|
673
|
+
issues.push(`platforms[${idx}].type 必须是 frontend/backend/infra 之一`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
// git
|
|
678
|
+
if (isObject(obj.git)) {
|
|
679
|
+
if (typeof obj.git.default_base !== 'string')
|
|
680
|
+
issues.push('git.default_base 必须是字符串');
|
|
681
|
+
if (typeof obj.git.branch_prefix !== 'string')
|
|
682
|
+
issues.push('git.branch_prefix 必须是字符串');
|
|
683
|
+
if (!Array.isArray(obj.git.protected_branches))
|
|
684
|
+
issues.push('git.protected_branches 必须是数组');
|
|
685
|
+
}
|
|
686
|
+
return { valid: issues.length === 0, issues };
|
|
687
|
+
}
|
|
688
|
+
function isObject(v) {
|
|
689
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
690
|
+
}
|
|
691
|
+
// ─────────────────────────────────────────
|
|
692
|
+
// YAML 解析 / 序列化
|
|
693
|
+
// ─────────────────────────────────────────
|
|
55
694
|
function parseYaml(content) {
|
|
56
695
|
const result = {};
|
|
57
696
|
const lines = content.split('\n');
|
|
58
|
-
let
|
|
59
|
-
let currentList = null;
|
|
697
|
+
let stack = [{ obj: result, indent: -1 }];
|
|
60
698
|
for (const line of lines) {
|
|
61
|
-
|
|
62
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
63
|
-
continue;
|
|
64
|
-
// Top-level section
|
|
65
|
-
if (!trimmed.startsWith('-') && trimmed.endsWith(':')) {
|
|
66
|
-
currentSection = trimmed.slice(0, -1).trim();
|
|
67
|
-
if (!result[currentSection])
|
|
68
|
-
result[currentSection] = {};
|
|
69
|
-
currentList = null;
|
|
699
|
+
if (!line.trim() || line.trim().startsWith('#'))
|
|
70
700
|
continue;
|
|
701
|
+
const indent = line.length - line.trimStart().length;
|
|
702
|
+
// 回退到正确层级
|
|
703
|
+
while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
|
|
704
|
+
stack.pop();
|
|
71
705
|
}
|
|
72
|
-
|
|
73
|
-
|
|
706
|
+
const trimmed = line.trim();
|
|
707
|
+
const parent = stack[stack.length - 1].obj;
|
|
708
|
+
if (trimmed.startsWith('- ')) {
|
|
74
709
|
const value = trimmed.slice(2).trim();
|
|
75
|
-
if (!Array.isArray(
|
|
76
|
-
|
|
710
|
+
if (!Array.isArray(parent))
|
|
711
|
+
continue;
|
|
712
|
+
if (value.includes(':')) {
|
|
713
|
+
const obj = {};
|
|
714
|
+
const [k, ...rest] = value.split(':');
|
|
715
|
+
const v = rest.join(':').trim();
|
|
716
|
+
if (v)
|
|
717
|
+
obj[k.trim()] = autoType(v);
|
|
718
|
+
parent.push(obj);
|
|
719
|
+
stack.push({ obj, indent });
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
parent.push(autoType(value));
|
|
77
723
|
}
|
|
78
|
-
result[currentSection].push(value);
|
|
79
|
-
currentList = currentSection;
|
|
80
|
-
continue;
|
|
81
724
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
725
|
+
else if (trimmed.endsWith(':')) {
|
|
726
|
+
const key = trimmed.slice(0, -1).trim();
|
|
727
|
+
if (Array.isArray(parent)) {
|
|
728
|
+
const last = parent[parent.length - 1];
|
|
729
|
+
if (last && typeof last === 'object' && !Array.isArray(last)) {
|
|
730
|
+
last[key] = {};
|
|
731
|
+
stack.push({ obj: last[key], indent });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
if (!parent[key])
|
|
736
|
+
parent[key] = {};
|
|
737
|
+
stack.push({ obj: parent[key], indent });
|
|
87
738
|
}
|
|
88
|
-
currentList = null;
|
|
89
|
-
continue;
|
|
90
739
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
740
|
+
else if (trimmed.includes(':')) {
|
|
741
|
+
const [k, ...rest] = trimmed.split(':');
|
|
742
|
+
const v = rest.join(':').trim();
|
|
743
|
+
if (Array.isArray(parent)) {
|
|
744
|
+
const last = parent[parent.length - 1];
|
|
745
|
+
if (last && typeof last === 'object') {
|
|
746
|
+
last[k.trim()] = autoType(v);
|
|
747
|
+
}
|
|
97
748
|
}
|
|
98
749
|
else {
|
|
99
|
-
|
|
750
|
+
parent[k.trim()] = autoType(v);
|
|
100
751
|
}
|
|
101
752
|
}
|
|
102
753
|
}
|
|
103
754
|
return result;
|
|
104
755
|
}
|
|
756
|
+
/** 自动推断简单值的类型 */
|
|
757
|
+
function autoType(v) {
|
|
758
|
+
if (v === 'true')
|
|
759
|
+
return true;
|
|
760
|
+
if (v === 'false')
|
|
761
|
+
return false;
|
|
762
|
+
if (/^-?\d+$/.test(v))
|
|
763
|
+
return parseInt(v, 10);
|
|
764
|
+
if (/^-?\d+\.\d+$/.test(v))
|
|
765
|
+
return parseFloat(v);
|
|
766
|
+
return v;
|
|
767
|
+
}
|
|
105
768
|
function toYaml(config) {
|
|
106
|
-
let yaml = '# SpecCore Configuration\n';
|
|
769
|
+
let yaml = '# SpecCore System Configuration\n';
|
|
770
|
+
yaml += `# schema_version: ${config.schema_version}\n`;
|
|
107
771
|
yaml += `# ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
108
|
-
yaml +=
|
|
772
|
+
yaml += '\nquality_gates:\n';
|
|
773
|
+
yaml += ` enforce_testing: ${config.quality_gates.enforce_testing}\n`;
|
|
774
|
+
yaml += ` enforce_review: ${config.quality_gates.enforce_review}\n`;
|
|
775
|
+
yaml += ` require_pr: ${config.quality_gates.require_pr}\n`;
|
|
776
|
+
yaml += '\narbitration:\n';
|
|
777
|
+
yaml += ` enabled: ${config.arbitration.enabled}\n`;
|
|
778
|
+
yaml += ` mode: ${config.arbitration.mode}\n`;
|
|
779
|
+
yaml += '\nsettings:\n';
|
|
780
|
+
yaml += ` assignee:\n enabled: ${config.settings.assignee.enabled}\n mode: ${config.settings.assignee.mode}\n`;
|
|
781
|
+
yaml += ` trace:\n enabled: ${config.settings.trace.enabled}\n auto_annotate: ${config.settings.trace.auto_annotate}\n`;
|
|
782
|
+
yaml += ` archive:\n auto_cleanup: ${config.settings.archive.auto_cleanup}\n`;
|
|
783
|
+
yaml += ` plan:\n parallel_suggest: ${config.settings.plan.parallel_suggest}\n`;
|
|
784
|
+
yaml += ` validation:\n strict_mode: ${config.settings.validation.strict_mode}\n`;
|
|
785
|
+
yaml += ` sync:\n auto_check: ${config.settings.sync.auto_check}\n`;
|
|
786
|
+
yaml += ` patterns:\n auto_save: ${config.settings.patterns.auto_save}\n`;
|
|
787
|
+
yaml += ` review:\n check_assignee: ${config.settings.review.check_assignee}\n`;
|
|
788
|
+
yaml += '\nask:\n';
|
|
789
|
+
yaml += ` routing:\n mode: ${config.ask.routing.mode}\n`;
|
|
790
|
+
yaml += ` high_threshold: ${config.ask.routing.high_threshold}\n`;
|
|
791
|
+
yaml += ` low_threshold: ${config.ask.routing.low_threshold}\n`;
|
|
792
|
+
yaml += ` auto_host_ai: ${config.ask.routing.auto_host_ai}\n`;
|
|
793
|
+
yaml += ` cache_enabled: ${config.ask.routing.cache_enabled}\n`;
|
|
794
|
+
yaml += ` cache_min_hits: ${config.ask.routing.cache_min_hits}\n`;
|
|
795
|
+
yaml += ` rules:\n force_host_ai: ${config.ask.rules.force_host_ai}\n`;
|
|
796
|
+
if (config.ask.llm_providers.length > 0) {
|
|
797
|
+
yaml += ` llm_providers:\n`;
|
|
798
|
+
for (const p of config.ask.llm_providers) {
|
|
799
|
+
yaml += ` - name: ${p.name}\n enabled: ${p.enabled}\n type: ${p.type}\n endpoint: ${p.endpoint}\n model: ${p.model}\n`;
|
|
800
|
+
if (p.apiKey)
|
|
801
|
+
yaml += ` apiKey: ${p.apiKey}\n`;
|
|
802
|
+
yaml += ` priority: ${p.priority}\n`;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (config.config_history.length > 0) {
|
|
806
|
+
yaml += '\nconfig_history:\n';
|
|
807
|
+
for (const h of config.config_history) {
|
|
808
|
+
yaml += ` - date: ${h.date}\n change: ${h.change}\n`;
|
|
809
|
+
if (h.changed_by)
|
|
810
|
+
yaml += ` changed_by: ${h.changed_by}\n`;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return yaml;
|
|
814
|
+
}
|
|
815
|
+
function toProjectYaml(config) {
|
|
816
|
+
let yaml = '# SpecCore Project Configuration\n';
|
|
817
|
+
yaml += `# schema_version: ${config.schema_version}\n`;
|
|
818
|
+
yaml += `# ${new Date().toISOString().split('T')[0]}\n\n`;
|
|
819
|
+
yaml += `project:\n`;
|
|
820
|
+
yaml += ` name: ${config.project.name}\n`;
|
|
109
821
|
if (config.project.description)
|
|
110
|
-
yaml += ` description: ${config.project.description}\n
|
|
111
|
-
|
|
112
|
-
yaml +=
|
|
113
|
-
yaml += '
|
|
114
|
-
for (const [k, v] of Object.entries(config.tech_stack)) {
|
|
115
|
-
if (v)
|
|
116
|
-
yaml += ` ${k}: ${v}\n`;
|
|
117
|
-
}
|
|
118
|
-
yaml += '\n';
|
|
119
|
-
yaml += 'team:\n members:\n';
|
|
120
|
-
for (const m of config.team.members) {
|
|
121
|
-
yaml += ` - name: ${m.name}\n role: ${m.role}\n`;
|
|
122
|
-
}
|
|
123
|
-
yaml += '\n';
|
|
124
|
-
yaml += 'platforms:\n';
|
|
822
|
+
yaml += ` description: ${config.project.description}\n`;
|
|
823
|
+
if (config.project.version)
|
|
824
|
+
yaml += ` version: ${config.project.version}\n`;
|
|
825
|
+
yaml += '\nplatforms:\n';
|
|
125
826
|
for (const p of config.platforms) {
|
|
126
|
-
yaml += ` - ${p}\n`;
|
|
827
|
+
yaml += ` - name: ${p.name}\n type: ${p.type}\n`;
|
|
828
|
+
if (p.description)
|
|
829
|
+
yaml += ` description: ${p.description}\n`;
|
|
830
|
+
if (p.code_path)
|
|
831
|
+
yaml += ` code_path: ${p.code_path}\n`;
|
|
832
|
+
if (p.git_repo)
|
|
833
|
+
yaml += ` git_repo: ${p.git_repo}\n`;
|
|
834
|
+
if (p.default_branch)
|
|
835
|
+
yaml += ` default_branch: ${p.default_branch}\n`;
|
|
127
836
|
}
|
|
128
|
-
yaml +=
|
|
129
|
-
yaml += `
|
|
130
|
-
yaml +=
|
|
837
|
+
yaml += `\ngit:\n`;
|
|
838
|
+
yaml += ` default_base: ${config.git.default_base}\n`;
|
|
839
|
+
yaml += ` branch_prefix: ${config.git.branch_prefix}\n`;
|
|
840
|
+
yaml += ` protected_branches:\n`;
|
|
841
|
+
for (const b of config.git.protected_branches) {
|
|
842
|
+
yaml += ` - ${b}\n`;
|
|
843
|
+
}
|
|
844
|
+
yaml += '\ncode_scope:\n';
|
|
131
845
|
for (const s of config.code_scope) {
|
|
132
846
|
yaml += ` - ${s}\n`;
|
|
133
847
|
}
|
|
134
|
-
yaml += '\n';
|
|
135
|
-
yaml += 'quality_gates:\n';
|
|
136
|
-
yaml += ` enforce_testing: ${config.quality_gates.enforce_testing}\n`;
|
|
137
|
-
yaml += ` enforce_review: ${config.quality_gates.enforce_review}\n`;
|
|
138
|
-
yaml += ` require_pr: ${config.quality_gates.require_pr}\n`;
|
|
139
848
|
return yaml;
|
|
140
849
|
}
|
|
850
|
+
// ─────────────────────────────────────────
|
|
851
|
+
// 深度合并(用于自动补全缺失字段)
|
|
852
|
+
// ─────────────────────────────────────────
|
|
853
|
+
function deepMerge(target, source) {
|
|
854
|
+
const result = { ...target };
|
|
855
|
+
for (const key of Object.keys(source)) {
|
|
856
|
+
const sVal = source[key];
|
|
857
|
+
const tVal = target[key];
|
|
858
|
+
if (sVal === undefined)
|
|
859
|
+
continue;
|
|
860
|
+
if (isPlainObject(tVal) && isPlainObject(sVal)) {
|
|
861
|
+
result[key] = deepMerge(tVal, sVal);
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
result[key] = sVal;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return result;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* 递归遍历对象/数组,将所有字符串中的 ${ENV_VAR} 替换为环境变量值。
|
|
871
|
+
* 支持默认值语法: ${VAR:-default}
|
|
872
|
+
*/
|
|
873
|
+
function resolveEnvVars(obj) {
|
|
874
|
+
const resolved = [];
|
|
875
|
+
const unresolved = [];
|
|
876
|
+
function traverse(current) {
|
|
877
|
+
if (typeof current === 'string') {
|
|
878
|
+
return resolveEnvVar(current, resolved, unresolved);
|
|
879
|
+
}
|
|
880
|
+
if (Array.isArray(current)) {
|
|
881
|
+
return current.map((item) => traverse(item));
|
|
882
|
+
}
|
|
883
|
+
if (isPlainObject(current)) {
|
|
884
|
+
const result = {};
|
|
885
|
+
for (const [key, value] of Object.entries(current)) {
|
|
886
|
+
result[key] = traverse(value);
|
|
887
|
+
}
|
|
888
|
+
return result;
|
|
889
|
+
}
|
|
890
|
+
return current;
|
|
891
|
+
}
|
|
892
|
+
const value = traverse(obj);
|
|
893
|
+
return { value, resolved, unresolved };
|
|
894
|
+
}
|
|
895
|
+
/** 解析单个字符串中的 ${VAR} 和 ${VAR:-default} */
|
|
896
|
+
function resolveEnvVar(str, resolved, unresolved) {
|
|
897
|
+
return str.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, varName, defaultValue) => {
|
|
898
|
+
const envValue = process.env[varName];
|
|
899
|
+
if (envValue !== undefined && envValue !== '') {
|
|
900
|
+
if (!resolved.includes(varName))
|
|
901
|
+
resolved.push(varName);
|
|
902
|
+
return envValue;
|
|
903
|
+
}
|
|
904
|
+
if (defaultValue !== undefined) {
|
|
905
|
+
return defaultValue;
|
|
906
|
+
}
|
|
907
|
+
if (!unresolved.includes(varName))
|
|
908
|
+
unresolved.push(varName);
|
|
909
|
+
return match; // 保留原样,环境变量不存在
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
function isPlainObject(v) {
|
|
913
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
914
|
+
}
|
|
141
915
|
//# sourceMappingURL=unified-config.js.map
|