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.
Files changed (120) hide show
  1. package/README.md +7 -4
  2. package/dist/cli.js +19 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/analyze.d.ts.map +1 -1
  5. package/dist/commands/analyze.js +24 -8
  6. package/dist/commands/analyze.js.map +1 -1
  7. package/dist/commands/config.d.ts +3 -1
  8. package/dist/commands/config.d.ts.map +1 -1
  9. package/dist/commands/config.js +206 -90
  10. package/dist/commands/config.js.map +1 -1
  11. package/dist/commands/context-output.d.ts.map +1 -1
  12. package/dist/commands/context-output.js +0 -10
  13. package/dist/commands/context-output.js.map +1 -1
  14. package/dist/commands/dev.js +6 -7
  15. package/dist/commands/dev.js.map +1 -1
  16. package/dist/commands/doctor.d.ts.map +1 -1
  17. package/dist/commands/doctor.js +140 -0
  18. package/dist/commands/doctor.js.map +1 -1
  19. package/dist/commands/done.js +51 -20
  20. package/dist/commands/done.js.map +1 -1
  21. package/dist/commands/execute.d.ts.map +1 -1
  22. package/dist/commands/execute.js +147 -45
  23. package/dist/commands/execute.js.map +1 -1
  24. package/dist/commands/help.js +5 -0
  25. package/dist/commands/help.js.map +1 -1
  26. package/dist/commands/init.d.ts +0 -2
  27. package/dist/commands/init.d.ts.map +1 -1
  28. package/dist/commands/init.js +9 -51
  29. package/dist/commands/init.js.map +1 -1
  30. package/dist/commands/iteration/split.js +29 -17
  31. package/dist/commands/iteration/split.js.map +1 -1
  32. package/dist/commands/status-panel.d.ts.map +1 -1
  33. package/dist/commands/status-panel.js +13 -48
  34. package/dist/commands/status-panel.js.map +1 -1
  35. package/dist/commands/status.d.ts.map +1 -1
  36. package/dist/commands/status.js +32 -0
  37. package/dist/commands/status.js.map +1 -1
  38. package/dist/commands/update.d.ts.map +1 -1
  39. package/dist/commands/update.js +206 -8
  40. package/dist/commands/update.js.map +1 -1
  41. package/dist/commands/verdict.d.ts +16 -0
  42. package/dist/commands/verdict.d.ts.map +1 -0
  43. package/dist/commands/verdict.js +189 -0
  44. package/dist/commands/verdict.js.map +1 -0
  45. package/dist/commands/verify.d.ts.map +1 -1
  46. package/dist/commands/verify.js +3 -2
  47. package/dist/commands/verify.js.map +1 -1
  48. package/dist/core/analyze-engine.d.ts.map +1 -1
  49. package/dist/core/analyze-engine.js +3 -4
  50. package/dist/core/analyze-engine.js.map +1 -1
  51. package/dist/core/arbitration/arbitration-engine.d.ts +36 -0
  52. package/dist/core/arbitration/arbitration-engine.d.ts.map +1 -0
  53. package/dist/core/arbitration/arbitration-engine.js +142 -0
  54. package/dist/core/arbitration/arbitration-engine.js.map +1 -0
  55. package/dist/core/arbitration/conflict-detector.d.ts +15 -0
  56. package/dist/core/arbitration/conflict-detector.d.ts.map +1 -0
  57. package/dist/core/arbitration/conflict-detector.js +78 -0
  58. package/dist/core/arbitration/conflict-detector.js.map +1 -0
  59. package/dist/core/arbitration/conflict-types.d.ts +34 -0
  60. package/dist/core/arbitration/conflict-types.d.ts.map +1 -0
  61. package/dist/core/arbitration/conflict-types.js +8 -0
  62. package/dist/core/arbitration/conflict-types.js.map +1 -0
  63. package/dist/core/arbitration/contract-registry.d.ts +15 -0
  64. package/dist/core/arbitration/contract-registry.d.ts.map +1 -0
  65. package/dist/core/arbitration/contract-registry.js +185 -0
  66. package/dist/core/arbitration/contract-registry.js.map +1 -0
  67. package/dist/core/arbitration/contract-types.d.ts +33 -0
  68. package/dist/core/arbitration/contract-types.d.ts.map +1 -0
  69. package/dist/core/arbitration/contract-types.js +8 -0
  70. package/dist/core/arbitration/contract-types.js.map +1 -0
  71. package/dist/core/arbitration/index.d.ts +16 -0
  72. package/dist/core/arbitration/index.d.ts.map +1 -0
  73. package/dist/core/arbitration/index.js +32 -0
  74. package/dist/core/arbitration/index.js.map +1 -0
  75. package/dist/core/arbitration/l1-auto-arbiter.d.ts +19 -0
  76. package/dist/core/arbitration/l1-auto-arbiter.d.ts.map +1 -0
  77. package/dist/core/arbitration/l1-auto-arbiter.js +41 -0
  78. package/dist/core/arbitration/l1-auto-arbiter.js.map +1 -0
  79. package/dist/core/arbitration/l2-ai-arbiter.d.ts +19 -0
  80. package/dist/core/arbitration/l2-ai-arbiter.d.ts.map +1 -0
  81. package/dist/core/arbitration/l2-ai-arbiter.js +57 -0
  82. package/dist/core/arbitration/l2-ai-arbiter.js.map +1 -0
  83. package/dist/core/arbitration/l3-human-arbiter.d.ts +24 -0
  84. package/dist/core/arbitration/l3-human-arbiter.d.ts.map +1 -0
  85. package/dist/core/arbitration/l3-human-arbiter.js +65 -0
  86. package/dist/core/arbitration/l3-human-arbiter.js.map +1 -0
  87. package/dist/core/arbitration/verdict-generator.d.ts +16 -0
  88. package/dist/core/arbitration/verdict-generator.d.ts.map +1 -0
  89. package/dist/core/arbitration/verdict-generator.js +166 -0
  90. package/dist/core/arbitration/verdict-generator.js.map +1 -0
  91. package/dist/core/arbitration/verdict-types.d.ts +49 -0
  92. package/dist/core/arbitration/verdict-types.d.ts.map +1 -0
  93. package/dist/core/arbitration/verdict-types.js +8 -0
  94. package/dist/core/arbitration/verdict-types.js.map +1 -0
  95. package/dist/core/ask-config.d.ts +4 -3
  96. package/dist/core/ask-config.d.ts.map +1 -1
  97. package/dist/core/ask-config.js +79 -1
  98. package/dist/core/ask-config.js.map +1 -1
  99. package/dist/core/ask-engine.d.ts.map +1 -1
  100. package/dist/core/ask-engine.js +7 -0
  101. package/dist/core/ask-engine.js.map +1 -1
  102. package/dist/core/intent-recognition.d.ts +1 -1
  103. package/dist/core/intent-recognition.d.ts.map +1 -1
  104. package/dist/core/intent-recognition.js +10 -0
  105. package/dist/core/intent-recognition.js.map +1 -1
  106. package/dist/core/pattern-detector.d.ts +24 -0
  107. package/dist/core/pattern-detector.d.ts.map +1 -1
  108. package/dist/core/pattern-detector.js +172 -3
  109. package/dist/core/pattern-detector.js.map +1 -1
  110. package/dist/core/quality-audit.d.ts.map +1 -1
  111. package/dist/core/quality-audit.js +23 -15
  112. package/dist/core/quality-audit.js.map +1 -1
  113. package/dist/core/spec-skeleton.d.ts.map +1 -1
  114. package/dist/core/spec-skeleton.js +35 -12
  115. package/dist/core/spec-skeleton.js.map +1 -1
  116. package/dist/core/unified-config.d.ts +161 -27
  117. package/dist/core/unified-config.d.ts.map +1 -1
  118. package/dist/core/unified-config.js +856 -82
  119. package/dist/core/unified-config.js.map +1 -1
  120. 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
- * 合并 SETTINGS.md / CONSTITUTION.md / TEAM.md 的配置到单一 YAML
10
- * 向后兼容:现有项目继续用旧文件,新项目自动用 .speccore.yml
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
- const DEFAULT_CONFIG = {
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
- if (await (0, fs_extra_1.pathExists)(CONFIG_PATH)) {
28
- try {
29
- const raw = await (0, fs_extra_1.readFile)(CONFIG_PATH, 'utf-8');
30
- return { ...DEFAULT_CONFIG, ...parseYaml(raw) };
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
- catch (e) {
33
- logger_1.logger.warn(`⚠️ .speccore.yml 解析失败,使用默认配置`);
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 yaml = toYaml(config);
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(projectName) {
46
- const config = { ...DEFAULT_CONFIG };
47
- if (projectName)
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
- * 简易 YAML 解析器(只处理简单 key-value + list)
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 currentSection = null;
59
- let currentList = null;
697
+ let stack = [{ obj: result, indent: -1 }];
60
698
  for (const line of lines) {
61
- const trimmed = line.trim();
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
- // List item
73
- if (trimmed.startsWith('- ') && currentSection) {
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(result[currentSection])) {
76
- result[currentSection] = [];
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
- // Sub-section
83
- if (!trimmed.startsWith('-') && trimmed.endsWith(':') && currentSection) {
84
- const subSection = trimmed.slice(0, -1).trim();
85
- if (typeof result[currentSection] === 'object' && !Array.isArray(result[currentSection])) {
86
- result[currentSection][subSection] = result[currentSection][subSection] || {};
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
- // Key: value
92
- if (trimmed.includes(':') && !trimmed.endsWith(':')) {
93
- const [key, ...rest] = trimmed.split(':');
94
- const value = rest.join(':').trim();
95
- if (currentSection && typeof result[currentSection] === 'object') {
96
- result[currentSection][key.trim()] = value;
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
- result[key.trim()] = value;
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 += `project:\n name: ${config.project.name}\n`;
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\n`;
111
- else
112
- yaml += '\n';
113
- yaml += 'tech_stack:\n';
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 += '\n';
129
- yaml += `git:\n default_base: ${config.git.default_base}\n branch_prefix: ${config.git.branch_prefix}\n\n`;
130
- yaml += 'code_scope:\n';
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