speccore 8.3.246 → 8.3.248
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/dist/commands/analyze.js
CHANGED
|
@@ -67,6 +67,7 @@ const feature_source_1 = require("../core/feature-source");
|
|
|
67
67
|
const analyze_engine_1 = require("../core/analyze-engine");
|
|
68
68
|
const fs_extra_2 = require("fs-extra");
|
|
69
69
|
const prompt_builder_1 = require("../core/prompt-builder");
|
|
70
|
+
const spec_paths_1 = require("../core/spec-paths");
|
|
70
71
|
// v8.3.160+: 子 Agent 适配层(analyze 自定义 Prompt 中手动注入)
|
|
71
72
|
// v8.3.169+: 新增 dispatchSubagent 支持 Qoder SDK 直接调度
|
|
72
73
|
const agent_adapter_1 = require("../core/agent-adapter");
|
|
@@ -74,7 +75,7 @@ const questions_1 = require("../core/questions");
|
|
|
74
75
|
const platform_registry_1 = require("../core/platform-registry");
|
|
75
76
|
const index_guard_1 = require("../core/index-guard");
|
|
76
77
|
const unified_config_1 = require("../core/unified-config");
|
|
77
|
-
const
|
|
78
|
+
const spec_paths_2 = require("../core/spec-paths");
|
|
78
79
|
const spec_skeleton_1 = require("../core/spec-skeleton");
|
|
79
80
|
const unified_retrieval_1 = require("../core/unified-retrieval");
|
|
80
81
|
const unit_context_assembler_1 = require("../core/unit-context-assembler");
|
|
@@ -260,7 +261,7 @@ async function applyDocContent(content, iter) {
|
|
|
260
261
|
let applied = 0;
|
|
261
262
|
// v8.3.237+: 与 --apply 主通道一致——有功能模块的项目才拦截(内容类归 {feature}/overview/,
|
|
262
263
|
// 全局 overview/REQUIREMENT.md 由 split 聚合),无功能模块的老项目放行
|
|
263
|
-
const featureCount = (await (0,
|
|
264
|
+
const featureCount = (await (0, spec_paths_2.parseFeatureList)(iterDir)).length;
|
|
264
265
|
for (const [filename, docContent] of Object.entries(docs)) {
|
|
265
266
|
if (typeof docContent !== 'string' || !docContent.trim())
|
|
266
267
|
continue;
|
|
@@ -814,7 +815,7 @@ async function analyzeCommand(options) {
|
|
|
814
815
|
}
|
|
815
816
|
// v8.3.16+: 模糊匹配端名,支持多段指定(逗号分隔)
|
|
816
817
|
if (options.platform) {
|
|
817
|
-
const allPlatforms = await (0,
|
|
818
|
+
const allPlatforms = await (0, spec_paths_2.parsePlatformList)();
|
|
818
819
|
const inputPlatforms = options.platform.split(',').map(p => p.trim()).filter(Boolean);
|
|
819
820
|
const resolvedPlatforms = [];
|
|
820
821
|
for (const input of inputPlatforms) {
|
|
@@ -923,7 +924,7 @@ async function analyzeCommand(options) {
|
|
|
923
924
|
}
|
|
924
925
|
// 新路径两级澄清产物({feature}/overview/CLARIFY.md)与切分目录一一对应,优先级最高
|
|
925
926
|
const clarifyUnitDocs = await collectUnitClarifyDocs(specDir);
|
|
926
|
-
const featureUnits = await (0,
|
|
927
|
+
const featureUnits = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
927
928
|
if (featureUnits.length > 0 && (clarifyUnitDocs.length > 0 || unitLevel.length > 0)) {
|
|
928
929
|
// 有切分锚点:按单元优先读入一一对应的澄清文档,跳过冗余的文档级
|
|
929
930
|
const legacyUnitName = (f) => (0, path_1.basename)(f).replace(/^unit-/, '').replace(/\.md$/, '');
|
|
@@ -953,7 +954,7 @@ async function analyzeCommand(options) {
|
|
|
953
954
|
// features/ 单元名决定 020-specs 骨架,黄金需求文件名决定内容来源,
|
|
954
955
|
// 两者不对齐时 taskify 无法把澄清内容挂接到对应单元
|
|
955
956
|
try {
|
|
956
|
-
const featureUnits = await (0,
|
|
957
|
+
const featureUnits = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
957
958
|
if (featureUnits.length > 0) {
|
|
958
959
|
const goldenBases = (await (0, fs_extra_2.readdir)(goldenReqDir))
|
|
959
960
|
.filter((f) => f.endsWith('.md') && !(0, task_utils_1.isTimestampBackup)(f))
|
|
@@ -1116,7 +1117,7 @@ async function analyzeCommand(options) {
|
|
|
1116
1117
|
// v8.0.0+: 骨架优先架构 — CLI 预创建所有文件,AI 只覆盖内容
|
|
1117
1118
|
const iterDirForSkeleton = await (0, context_1.getIterationDir)(iterForDirs);
|
|
1118
1119
|
const specDir = (0, path_1.join)(iterDirForSkeleton, '020-specs');
|
|
1119
|
-
let platforms = await (0,
|
|
1120
|
+
let platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
1120
1121
|
const iterName = iterForDirs;
|
|
1121
1122
|
// v8.3.16+: 如果指定了 --platform,只创建指定端的骨架
|
|
1122
1123
|
if (options.platform) {
|
|
@@ -1133,7 +1134,7 @@ async function analyzeCommand(options) {
|
|
|
1133
1134
|
// v8.3.171+: 删除 sanitize 自动调用(保留为 speccore migrate 工具)
|
|
1134
1135
|
// v8.3.21+: 按需求文档名组织(020-specs/{需求名}/)
|
|
1135
1136
|
const phase = options.phase;
|
|
1136
|
-
const features = await (0,
|
|
1137
|
+
const features = await (0, spec_paths_2.parseFeatureList)(iterDirForSkeleton);
|
|
1137
1138
|
let manifest;
|
|
1138
1139
|
if (features.length > 0) {
|
|
1139
1140
|
manifest = (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, phase, iterName);
|
|
@@ -1526,10 +1527,10 @@ ${singlePrompt}`);
|
|
|
1526
1527
|
if (stepId === 'phase1-prompt') {
|
|
1527
1528
|
// 有功能模块的项目只生成全局索引;老项目生成全部全局文档。
|
|
1528
1529
|
// CLARIFY.md 由 clarify 阶段产出,不在本步骤检查范围(skip clarify 时不会卡住)。
|
|
1529
|
-
const featureCount = (await (0,
|
|
1530
|
+
const featureCount = (await (0, spec_paths_2.parseFeatureList)(iterDirForCheck)).length;
|
|
1530
1531
|
checkDocs = (featureCount > 0 ? spec_skeleton_1.PHASE1_INDEX_DOC_NAMES : spec_skeleton_1.PHASE1_DOC_NAMES)
|
|
1531
1532
|
.filter((n) => n !== 'CLARIFY.md');
|
|
1532
|
-
baseRel =
|
|
1533
|
+
baseRel = spec_paths_2.GLOBAL_SPECS_DIR;
|
|
1533
1534
|
}
|
|
1534
1535
|
else {
|
|
1535
1536
|
const m = stepId.match(/^feature-(.+)-prompt$/);
|
|
@@ -1803,7 +1804,7 @@ ${singlePrompt}`);
|
|
|
1803
1804
|
// v8.3.237+: 拦截条件化为「有功能模块的项目」——内容类归 {feature}/overview/,
|
|
1804
1805
|
// 全局 overview/REQUIREMENT.md 由 split 阶段从功能模块聚合生成,AI 不应直接写。
|
|
1805
1806
|
// 无功能模块的老项目放行(内容类唯一容器就是 overview/)。
|
|
1806
|
-
const featureCount = iterDir ? (await (0,
|
|
1807
|
+
const featureCount = iterDir ? (await (0, spec_paths_2.parseFeatureList)(iterDir)).length : 0;
|
|
1807
1808
|
if (featureCount > 0) {
|
|
1808
1809
|
for (const filename of Object.keys(docs)) {
|
|
1809
1810
|
if (filename === 'overview/REQUIREMENT.md' || filename === 'REQUIREMENT.md') {
|
|
@@ -1843,7 +1844,7 @@ ${singlePrompt}`);
|
|
|
1843
1844
|
// 全局级:写入 .speccore/GLOBAL/(与 platforms/ 同级,非迭代目录)
|
|
1844
1845
|
const globalBaseDir = (0, path_1.join)(_projectRoot, '.speccore', 'GLOBAL');
|
|
1845
1846
|
await (0, fs_extra_1.ensureDir)(globalBaseDir);
|
|
1846
|
-
const globalSet = new Set(
|
|
1847
|
+
const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
|
|
1847
1848
|
for (const [filename, content] of Object.entries(docs)) {
|
|
1848
1849
|
// PATTERNS/ 文件特殊处理 → 写入 .speccore/PATTERNS/
|
|
1849
1850
|
if (filename.startsWith('PATTERNS/')) {
|
|
@@ -1943,10 +1944,10 @@ ${singlePrompt}`);
|
|
|
1943
1944
|
// v6.69.2+: 增加端名白名单校验,防止 AI 创建非法目录
|
|
1944
1945
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
1945
1946
|
await (0, fs_extra_1.ensureDir)(specDir);
|
|
1946
|
-
const globalSet = new Set(
|
|
1947
|
-
const validPlatforms = new Set([
|
|
1947
|
+
const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
|
|
1948
|
+
const validPlatforms = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_2.parsePlatformList)())]);
|
|
1948
1949
|
// v8.3.21+: 按需求文档名组织
|
|
1949
|
-
const features = await (0,
|
|
1950
|
+
const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
1950
1951
|
if (features.length > 0) {
|
|
1951
1952
|
logger_1.logger.info(` 📂 检测到 ${features.length} 个功能模块,按需求文档名路由文档`);
|
|
1952
1953
|
}
|
|
@@ -2002,16 +2003,16 @@ ${singlePrompt}`);
|
|
|
2002
2003
|
}
|
|
2003
2004
|
// v7.4.0+: 文件名归一化——剥离 AI 可能携带的 overview/ 前缀,防止嵌套目录
|
|
2004
2005
|
let cleanFilename = filename;
|
|
2005
|
-
if (cleanFilename.startsWith(`${
|
|
2006
|
-
cleanFilename = cleanFilename.slice(`${
|
|
2006
|
+
if (cleanFilename.startsWith(`${spec_paths_2.GLOBAL_SPECS_DIR}/`)) {
|
|
2007
|
+
cleanFilename = cleanFilename.slice(`${spec_paths_2.GLOBAL_SPECS_DIR}/`.length);
|
|
2007
2008
|
}
|
|
2008
2009
|
// v8.3.21+: 按需求文档名组织的目录结构路由
|
|
2009
2010
|
if (cleanFilename.includes('/')) {
|
|
2010
2011
|
const parts = cleanFilename.split('/');
|
|
2011
2012
|
const firstPart = parts[0];
|
|
2012
2013
|
// 全局文档:overview/xxx → 写入 020-specs/overview/
|
|
2013
|
-
if (firstPart ===
|
|
2014
|
-
const targetDir = (0, path_1.join)(specDir,
|
|
2014
|
+
if (firstPart === spec_paths_2.GLOBAL_SPECS_DIR) {
|
|
2015
|
+
const targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2015
2016
|
await (0, fs_extra_1.ensureDir)(targetDir);
|
|
2016
2017
|
const fp = (0, path_1.join)(targetDir, parts.slice(1).join('/'));
|
|
2017
2018
|
if (!options.merge && !(await (0, task_utils_1.shouldOverwrite)(fp, !!options.interactive))) {
|
|
@@ -2075,7 +2076,7 @@ ${singlePrompt}`);
|
|
|
2075
2076
|
// 如果 AI 输出 {端}/{文件},尝试回退到功能模块路由
|
|
2076
2077
|
let targetDir;
|
|
2077
2078
|
if (globalSet.has(cleanFilename)) {
|
|
2078
|
-
targetDir = (0, path_1.join)(specDir,
|
|
2079
|
+
targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2079
2080
|
}
|
|
2080
2081
|
else if (platformDir && validPlatforms.has(platformDir)) {
|
|
2081
2082
|
// 检查是否应归入功能模块(骨架文件已按功能模块创建)
|
|
@@ -2083,7 +2084,7 @@ ${singlePrompt}`);
|
|
|
2083
2084
|
try {
|
|
2084
2085
|
const specEntries = await (0, fs_extra_2.readdir)(specDir, { withFileTypes: true });
|
|
2085
2086
|
for (const entry of specEntries) {
|
|
2086
|
-
if (!entry.isDirectory() || entry.name ===
|
|
2087
|
+
if (!entry.isDirectory() || entry.name === spec_paths_2.GLOBAL_SPECS_DIR || validPlatforms.has(entry.name))
|
|
2087
2088
|
continue;
|
|
2088
2089
|
const platformInFeature = (0, path_1.join)(specDir, entry.name, platformDir);
|
|
2089
2090
|
if (await (0, fs_extra_1.pathExists)(platformInFeature)) {
|
|
@@ -2291,7 +2292,7 @@ ${singlePrompt}`);
|
|
|
2291
2292
|
}
|
|
2292
2293
|
if (!isGlobalScope) {
|
|
2293
2294
|
// v8.3.237+: 与 JSON 通道一致——有功能模块的项目拦截,无功能模块的老项目放行
|
|
2294
|
-
const featureCount = iterDir ? (await (0,
|
|
2295
|
+
const featureCount = iterDir ? (await (0, spec_paths_2.parseFeatureList)(iterDir)).length : 0;
|
|
2295
2296
|
if (featureCount > 0) {
|
|
2296
2297
|
for (const docName of docBlocks.keys()) {
|
|
2297
2298
|
if (docName === 'overview/REQUIREMENT.md' || docName === 'REQUIREMENT.md') {
|
|
@@ -2342,7 +2343,7 @@ ${singlePrompt}`);
|
|
|
2342
2343
|
else if (isGlobalScope) {
|
|
2343
2344
|
const globalBaseDir = (0, path_1.join)(_projectRoot, '.speccore', 'GLOBAL');
|
|
2344
2345
|
await (0, fs_extra_1.ensureDir)(globalBaseDir);
|
|
2345
|
-
const globalSet = new Set(
|
|
2346
|
+
const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
|
|
2346
2347
|
for (const [filename, content] of Object.entries(docs)) {
|
|
2347
2348
|
if (filename.startsWith('PATTERNS/')) {
|
|
2348
2349
|
const patternsDir = (0, path_1.join)(_projectRoot, '.speccore', 'PATTERNS');
|
|
@@ -2403,8 +2404,8 @@ ${singlePrompt}`);
|
|
|
2403
2404
|
else {
|
|
2404
2405
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
2405
2406
|
await (0, fs_extra_1.ensureDir)(specDir);
|
|
2406
|
-
const globalSet = new Set(
|
|
2407
|
-
const validPlatforms = new Set([
|
|
2407
|
+
const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
|
|
2408
|
+
const validPlatforms = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...(await (0, spec_paths_2.parsePlatformList)())]);
|
|
2408
2409
|
let skippedCount = 0;
|
|
2409
2410
|
for (const [filename, content] of Object.entries(docs)) {
|
|
2410
2411
|
if (filename.startsWith('020-specs/requirements/')) {
|
|
@@ -2453,8 +2454,8 @@ ${singlePrompt}`);
|
|
|
2453
2454
|
continue;
|
|
2454
2455
|
}
|
|
2455
2456
|
let cleanFilename = filename;
|
|
2456
|
-
if (cleanFilename.startsWith(`${
|
|
2457
|
-
cleanFilename = cleanFilename.slice(`${
|
|
2457
|
+
if (cleanFilename.startsWith(`${spec_paths_2.GLOBAL_SPECS_DIR}/`)) {
|
|
2458
|
+
cleanFilename = cleanFilename.slice(`${spec_paths_2.GLOBAL_SPECS_DIR}/`.length);
|
|
2458
2459
|
}
|
|
2459
2460
|
if (cleanFilename.includes('/')) {
|
|
2460
2461
|
const prefix = cleanFilename.split('/')[0];
|
|
@@ -2473,7 +2474,7 @@ ${singlePrompt}`);
|
|
|
2473
2474
|
// 若 cleanFilename 含合法端名前缀(如 api/TECH.md),写入对应端目录
|
|
2474
2475
|
let targetDir;
|
|
2475
2476
|
if (globalSet.has(cleanFilename)) {
|
|
2476
|
-
targetDir = (0, path_1.join)(specDir,
|
|
2477
|
+
targetDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2477
2478
|
}
|
|
2478
2479
|
else if (platformDir && validPlatforms.has(platformDir)) {
|
|
2479
2480
|
targetDir = (0, path_1.join)(specDir, platformDir);
|
|
@@ -2588,7 +2589,7 @@ ${singlePrompt}`);
|
|
|
2588
2589
|
else if (!hasClarifyMarkers) {
|
|
2589
2590
|
// 迭代级:写 020-specs/overview/(迭代综合文档,v6.78.0+)
|
|
2590
2591
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
2591
|
-
const globalDir = (0, path_1.join)(specDir,
|
|
2592
|
+
const globalDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2592
2593
|
await (0, fs_extra_1.ensureDir)(globalDir);
|
|
2593
2594
|
const iterAnalysisPath = (0, path_1.join)(globalDir, 'ANALYSIS.md');
|
|
2594
2595
|
if (await (0, task_utils_1.shouldOverwrite)(iterAnalysisPath, !!options.interactive)) {
|
|
@@ -2633,7 +2634,7 @@ ${singlePrompt}`);
|
|
|
2633
2634
|
// ── v6.64.0+: Phase 1 完成后自动触发 Phase 2(仅多端项目,非 Pipeline 模式)──
|
|
2634
2635
|
if (!options.phase && !isTaskLevel && options.iteration) {
|
|
2635
2636
|
// 检查是否有多个端
|
|
2636
|
-
const platforms = await (0,
|
|
2637
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
2637
2638
|
// 只有多端项目(≥2 个端)才需要分两阶段:
|
|
2638
2639
|
// - Phase 1: 生成综合文档(overview/REQUIREMENT.md、ANALYSIS.md、DEPS.md)
|
|
2639
2640
|
// - Phase 2: 生成各端专属文档({端}/TECH.md、TEST.md、UI_SPEC.md)
|
|
@@ -2726,9 +2727,9 @@ async function sanitizeSpecDirectories(iterDir) {
|
|
|
2726
2727
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
2727
2728
|
if (!await (0, fs_extra_1.pathExists)(specDir))
|
|
2728
2729
|
return;
|
|
2729
|
-
const platforms = await (0,
|
|
2730
|
-
const validDirs = new Set([
|
|
2731
|
-
const globalSet = new Set(
|
|
2730
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
2731
|
+
const validDirs = new Set([spec_paths_2.GLOBAL_SPECS_DIR, ...platforms]);
|
|
2732
|
+
const globalSet = new Set(spec_paths_2.GLOBAL_SPEC_FILES);
|
|
2732
2733
|
const entries = await (0, fs_extra_2.readdir)(specDir);
|
|
2733
2734
|
let illegalDirCount = 0;
|
|
2734
2735
|
let orphanFileCount = 0;
|
|
@@ -2739,17 +2740,17 @@ async function sanitizeSpecDirectories(iterDir) {
|
|
|
2739
2740
|
if (entryStat.isDirectory()) {
|
|
2740
2741
|
// 检测遗留的 global/ 目录(v6.78.0+ 已改名为 overview/)
|
|
2741
2742
|
if (entry === 'global') {
|
|
2742
|
-
logger_1.logger.warn(`⚠️ 检测到遗留 global/ 目录(v6.78.0+ 已改名为 ${
|
|
2743
|
+
logger_1.logger.warn(`⚠️ 检测到遗留 global/ 目录(v6.78.0+ 已改名为 ${spec_paths_2.GLOBAL_SPECS_DIR}/)`);
|
|
2743
2744
|
// 将 global/ 中的文件迁移到 overview/
|
|
2744
2745
|
const globalFiles = await (0, fs_extra_2.readdir)(entryPath);
|
|
2745
|
-
const overviewDir = (0, path_1.join)(specDir,
|
|
2746
|
+
const overviewDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2746
2747
|
await (0, fs_extra_1.ensureDir)(overviewDir);
|
|
2747
2748
|
for (const f of globalFiles) {
|
|
2748
2749
|
const src = (0, path_1.join)(entryPath, f);
|
|
2749
2750
|
const dest = (0, path_1.join)(overviewDir, f);
|
|
2750
2751
|
if (!await (0, fs_extra_1.pathExists)(dest)) {
|
|
2751
2752
|
await (0, fs_extra_1.rename)(src, dest);
|
|
2752
|
-
logger_1.logger.info(` 📦 迁移: global/${f} → ${
|
|
2753
|
+
logger_1.logger.info(` 📦 迁移: global/${f} → ${spec_paths_2.GLOBAL_SPECS_DIR}/${f}`);
|
|
2753
2754
|
}
|
|
2754
2755
|
}
|
|
2755
2756
|
// 删除空的 global/ 目录
|
|
@@ -2781,12 +2782,12 @@ async function sanitizeSpecDirectories(iterDir) {
|
|
|
2781
2782
|
else if (entryStat.isFile()) {
|
|
2782
2783
|
// v8.3.11+: 根目录散落的文件 → 检查是否应归入 overview/(不限于 .md)
|
|
2783
2784
|
if (globalSet.has(entry)) {
|
|
2784
|
-
const overviewDir = (0, path_1.join)(specDir,
|
|
2785
|
+
const overviewDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2785
2786
|
await (0, fs_extra_1.ensureDir)(overviewDir);
|
|
2786
2787
|
const dest = (0, path_1.join)(overviewDir, entry);
|
|
2787
2788
|
if (!await (0, fs_extra_1.pathExists)(dest)) {
|
|
2788
2789
|
await (0, fs_extra_1.rename)(entryPath, dest);
|
|
2789
|
-
logger_1.logger.warn(`⚠️ 散落文件已归位: 020-specs/${entry} → ${
|
|
2790
|
+
logger_1.logger.warn(`⚠️ 散落文件已归位: 020-specs/${entry} → ${spec_paths_2.GLOBAL_SPECS_DIR}/${entry}`);
|
|
2790
2791
|
orphanFileCount++;
|
|
2791
2792
|
}
|
|
2792
2793
|
else {
|
|
@@ -2806,9 +2807,9 @@ async function sanitizeSpecDirectories(iterDir) {
|
|
|
2806
2807
|
if (illegalDirCount > 0)
|
|
2807
2808
|
logger_1.logger.warn(` ⚠️ ${illegalDirCount} 个非法目录已归档(AI 绕过 --apply 创建)`);
|
|
2808
2809
|
if (orphanFileCount > 0)
|
|
2809
|
-
logger_1.logger.warn(` ⚠️ ${orphanFileCount} 个散落文件已归位到 ${
|
|
2810
|
+
logger_1.logger.warn(` ⚠️ ${orphanFileCount} 个散落文件已归位到 ${spec_paths_2.GLOBAL_SPECS_DIR}/`);
|
|
2810
2811
|
if (legacyGlobalMigrated)
|
|
2811
|
-
logger_1.logger.info(` 📦 遗留 global/ 已迁移到 ${
|
|
2812
|
+
logger_1.logger.info(` 📦 遗留 global/ 已迁移到 ${spec_paths_2.GLOBAL_SPECS_DIR}/`);
|
|
2812
2813
|
logger_1.logger.info(` ✅ 当前合法目录: ${Array.from(validDirs).join(', ')}`);
|
|
2813
2814
|
}
|
|
2814
2815
|
}
|
|
@@ -2818,7 +2819,7 @@ async function sanitizeSpecDirectories(iterDir) {
|
|
|
2818
2819
|
async function generateIterationSpecDocs(iteration) {
|
|
2819
2820
|
const iterDir = await (0, context_1.getIterationDir)(iteration);
|
|
2820
2821
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
2821
|
-
const globalDir = (0, path_1.join)(specDir,
|
|
2822
|
+
const globalDir = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR);
|
|
2822
2823
|
await (0, fs_extra_1.ensureDir)(globalDir);
|
|
2823
2824
|
const now = new Date().toISOString().split('T')[0];
|
|
2824
2825
|
// 综合文档模板 → 写入 global/ 子目录(v6.41.0+)
|
|
@@ -3049,6 +3050,9 @@ function parseArgv(options) {
|
|
|
3049
3050
|
[['--output', '-o'], (v) => { options.output = v; }],
|
|
3050
3051
|
[['--depth'], (v) => { options.depth = v; }],
|
|
3051
3052
|
[['--feature'], (v) => { options.feature = v; }],
|
|
3053
|
+
// v8.3.248+: --unit 指定功能单元(支持逗号分隔多个),此前多单元 prompt 指示的
|
|
3054
|
+
// 「analyze --unit {id} --apply」提交命令因未注册该选项而静默失效
|
|
3055
|
+
[['--unit'], (v) => { options.unit = v; }],
|
|
3052
3056
|
[['--phase'], (v) => { options.phase = v; }],
|
|
3053
3057
|
[['--out-dir'], (v) => { options.outDir = v; }],
|
|
3054
3058
|
];
|
|
@@ -4410,10 +4414,10 @@ async function buildMultiDocPrompt(command, ctx, options, clarifyCtx) {
|
|
|
4410
4414
|
};
|
|
4411
4415
|
// 4a: 追加各前端端的需求文档
|
|
4412
4416
|
if (subLayerTarget === '4a') {
|
|
4413
|
-
const allPlatforms = await (0,
|
|
4417
|
+
const allPlatforms = await (0, spec_paths_2.parsePlatformList)();
|
|
4414
4418
|
const frontendPlatforms = allPlatforms.filter(p => {
|
|
4415
4419
|
const pt = (async () => { try {
|
|
4416
|
-
const m = await (0,
|
|
4420
|
+
const m = await (0, spec_paths_2.parsePlatformTypes)();
|
|
4417
4421
|
return m.get(p);
|
|
4418
4422
|
}
|
|
4419
4423
|
catch {
|
|
@@ -5029,11 +5033,11 @@ sequenceDiagram
|
|
|
5029
5033
|
const iterDir = await (0, context_1.getIterationDir)(ctx.iteration);
|
|
5030
5034
|
if (iterDir) {
|
|
5031
5035
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
5032
|
-
const platforms = await (0,
|
|
5036
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
5033
5037
|
// v8.3.237+: 有功能模块的项目用功能模块 manifest(内容类骨架在 {feature}/overview/,
|
|
5034
5038
|
// 全局 overview/ 只含索引类),避免旧 manifest 把 overview/REQUIREMENT.md 当
|
|
5035
5039
|
// 「下一个待填充」但 --apply 又按路由表拦截 → 单文档模式死锁。
|
|
5036
|
-
const features = await (0,
|
|
5040
|
+
const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
5037
5041
|
const manifest = features.length > 0
|
|
5038
5042
|
? (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, undefined, ctx.iteration)
|
|
5039
5043
|
: (0, spec_skeleton_1.computeAnalyzeManifest)(platforms, undefined, ctx.iteration);
|
|
@@ -5082,14 +5086,14 @@ sequenceDiagram
|
|
|
5082
5086
|
// v8.1.1+: DOC_MATRIX 交叉校验 — 检查骨架清单之外是否有遗漏的文档
|
|
5083
5087
|
// v8.3.237+: 有功能模块的项目全局 overview/ 只应有索引类文档,
|
|
5084
5088
|
// 内容类(REQUIREMENT.md 等)归 {feature}/overview/,不得在此补建骨架
|
|
5085
|
-
const hasFeatures = (await (0,
|
|
5089
|
+
const hasFeatures = (await (0, spec_paths_2.parseFeatureList)(iterDir)).length > 0;
|
|
5086
5090
|
const expectedGlobalDocs = (hasFeatures
|
|
5087
5091
|
? spec_skeleton_1.PHASE1_INDEX_DOC_NAMES
|
|
5088
5092
|
: GLOBAL_DOCS)
|
|
5089
5093
|
.filter(d => includeDocs.includes(d));
|
|
5090
5094
|
const missingDocs = [];
|
|
5091
5095
|
for (const doc of expectedGlobalDocs) {
|
|
5092
|
-
const docPath = (0, path_1.join)(specDir,
|
|
5096
|
+
const docPath = (0, path_1.join)(specDir, spec_paths_2.GLOBAL_SPECS_DIR, doc);
|
|
5093
5097
|
if (!await (0, fs_extra_1.pathExists)(docPath)) {
|
|
5094
5098
|
missingDocs.push(doc);
|
|
5095
5099
|
}
|
|
@@ -5340,7 +5344,7 @@ status: "clarified"
|
|
|
5340
5344
|
}
|
|
5341
5345
|
// ── v6.67.0+: 在 prompt 开头就明确完整工作流程 ──
|
|
5342
5346
|
if (!ctx.phase && !isTask) {
|
|
5343
|
-
const platforms = await (0,
|
|
5347
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
5344
5348
|
if (platforms.length >= 2) {
|
|
5345
5349
|
prompt += `## 📋 完整工作流程(两阶段分析)\n\n`;
|
|
5346
5350
|
prompt += `本项目检测到有 ${platforms.length} 个端(${platforms.join('、')}),需要分两阶段执行:\n\n`;
|
|
@@ -5362,10 +5366,10 @@ status: "clarified"
|
|
|
5362
5366
|
try {
|
|
5363
5367
|
const iterDir = await (0, context_1.getIterationDir)(iter);
|
|
5364
5368
|
if (iterDir) {
|
|
5365
|
-
const platforms = await (0,
|
|
5369
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
5366
5370
|
const phase = options?.phase;
|
|
5367
5371
|
// v8.3.34: 按需求文档名组织,prompt 中的文件清单也要用功能模块结构
|
|
5368
|
-
const features = await (0,
|
|
5372
|
+
const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
5369
5373
|
const manifest = (0, spec_skeleton_1.computeFeatureBasedAnalyzeManifest)(features, platforms, phase, iter);
|
|
5370
5374
|
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
5371
5375
|
prompt += (0, spec_skeleton_1.buildSkeletonFileList)(manifest, specDir);
|
|
@@ -5636,8 +5640,19 @@ status: "clarified"
|
|
|
5636
5640
|
// v8.3.160+: 大项目按端分段读取,避免一次性加载全量结构化数据
|
|
5637
5641
|
prompt += ` - 先 Read \`.speccore/cache/structured-data.json\` 的 stats 部分(了解各端规模)\n`;
|
|
5638
5642
|
prompt += ` - **按需读取各端分段文件**:\`.speccore/cache/structured-data.{端名}.json\`(只读取本功能模块涉及的端)\n`;
|
|
5639
|
-
|
|
5640
|
-
|
|
5643
|
+
// v8.3.248+: 直接注入各端工程路径表(此前仅提示 AI「从 CONSTITUTION.md 获取源码路径」,
|
|
5644
|
+
// AI 需自行翻表定位;现在直接给出,减少一步间接查找、杜绝瞎猜)
|
|
5645
|
+
try {
|
|
5646
|
+
const projectInfoMap = await (0, spec_paths_1.parseProjectInfo)();
|
|
5647
|
+
if (projectInfoMap.size > 0) {
|
|
5648
|
+
prompt += ` - **各端工程路径**(结合源码分析时按此定位,CONSTITUTION.md 工程表权威):\n`;
|
|
5649
|
+
for (const [identifier, info] of projectInfoMap) {
|
|
5650
|
+
prompt += ` - \`${identifier}\`(${info.projectType || '-'})→ \`${info.srcPath}\`\n`;
|
|
5651
|
+
}
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
catch { /* 路径解析失败不阻断分析 */ }
|
|
5655
|
+
prompt += ` - 再按需 Read 各端源码中与本功能模块相关的关键文件,重点关注:接口定义、数据模型、业务逻辑实现、状态管理\n\n`;
|
|
5641
5656
|
}
|
|
5642
5657
|
prompt += `3. 读取迭代需求文档(按功能模块组织):\n`;
|
|
5643
5658
|
prompt += ` a. 先读 010-requirements/INDEX.md — 获取功能模块清单\n`;
|
|
@@ -5729,7 +5744,7 @@ status: "clarified"
|
|
|
5729
5744
|
prompt += ` - INTERACTION_MAP.md 是前后端开发者的共同参考,补全产品文档中隐含的技术交互\n`;
|
|
5730
5745
|
// 注入工程类型信息(v6.49.0+)
|
|
5731
5746
|
// v8.3.160+: 精简为路径引用,具体维度让 AI 从 CONSTITUTION.md 读取
|
|
5732
|
-
const platformTypes = await (0,
|
|
5747
|
+
const platformTypes = await (0, spec_paths_2.parsePlatformTypes)();
|
|
5733
5748
|
if (platformTypes.size > 0) {
|
|
5734
5749
|
prompt += `8. **工程类型识别**:CONSTITUTION.md 已配置各端的工程类型,请 Read 后据此生成针对性内容\n`;
|
|
5735
5750
|
prompt += ` | 工程标识 | 工程类型 |\n`;
|
|
@@ -5915,7 +5930,7 @@ status: "clarified"
|
|
|
5915
5930
|
}
|
|
5916
5931
|
else {
|
|
5917
5932
|
prompt += `> 🎉 这是最后一个骨架文件!填充后所有迭代级分析文档将完整。\n`;
|
|
5918
|
-
const platforms = await (0,
|
|
5933
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
5919
5934
|
const hasPlatformUnfilled = perDocStatus.unfilled.some(p => p.includes('/') && !p.startsWith('overview/'));
|
|
5920
5935
|
if (platforms.length >= 2 && hasPlatformUnfilled) {
|
|
5921
5936
|
prompt += `> 下一步: 生成各端专属文档 → \`speccore analyze --prompt -I ${iter} --phase 2\`\n`;
|
|
@@ -5990,8 +6005,8 @@ status: "clarified"
|
|
|
5990
6005
|
// ── v6.74.0+: 流式全局分析 Prompt 生成 ──
|
|
5991
6006
|
async function buildStreamingGlobalPrompt(command, ctx, options) {
|
|
5992
6007
|
const iter = ctx.iteration || '当前迭代';
|
|
5993
|
-
const platforms = await (0,
|
|
5994
|
-
const platformTypes = await (0,
|
|
6008
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
6009
|
+
const platformTypes = await (0, spec_paths_2.parsePlatformTypes)();
|
|
5995
6010
|
// 确定当前 Phase
|
|
5996
6011
|
let currentPhase = 'phase0-scan';
|
|
5997
6012
|
if (options?.streamingPhase) {
|
|
@@ -6094,7 +6109,7 @@ async function buildStreamingGlobalPrompt(command, ctx, options) {
|
|
|
6094
6109
|
// ── v6.69.0+: 契约先行 Prompt 生成(增强策略一)──
|
|
6095
6110
|
async function buildContractFirstPrompt(iteration) {
|
|
6096
6111
|
const iterDir = await (0, context_1.getIterationDir)(iteration);
|
|
6097
|
-
const globalDir = (0, path_1.join)(iterDir, '020-specs',
|
|
6112
|
+
const globalDir = (0, path_1.join)(iterDir, '020-specs', spec_paths_2.GLOBAL_SPECS_DIR);
|
|
6098
6113
|
let prompt = `\n# 任务: 跨端 API 契约定义(契约先行阶段)\n\n`;
|
|
6099
6114
|
prompt += `## 背景\n\n`;
|
|
6100
6115
|
prompt += `Phase 1 迭代分析已完成。现在需要在各端开始专属技术方案分析之前,**先定义跨端 API 契约**。\n\n`;
|
|
@@ -6174,7 +6189,7 @@ async function buildContractFirstPrompt(iteration) {
|
|
|
6174
6189
|
// ── v6.49.9+: 扫描平铺的端目录,返回所有子任务目录路径 ──
|
|
6175
6190
|
async function getSubtaskDirs(taskDir) {
|
|
6176
6191
|
const result = [];
|
|
6177
|
-
const platformList = await (0,
|
|
6192
|
+
const platformList = await (0, spec_paths_2.parsePlatformList)();
|
|
6178
6193
|
for (const platform of platformList) {
|
|
6179
6194
|
const platDir = (0, path_1.join)(taskDir, platform);
|
|
6180
6195
|
if (!(await (0, fs_extra_1.pathExists)(platDir)))
|