speccore 8.3.264 → 8.3.267
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.d.ts.map +1 -1
- package/dist/commands/analyze.js +209 -11
- package/dist/commands/analyze.js.map +1 -1
- package/dist/commands/doc2spec.d.ts.map +1 -1
- package/dist/commands/doc2spec.js +28 -5
- package/dist/commands/doc2spec.js.map +1 -1
- package/dist/commands/execute.js +14 -4
- package/dist/commands/execute.js.map +1 -1
- package/dist/commands/iteration/taskify.js +7 -0
- package/dist/commands/iteration/taskify.js.map +1 -1
- package/dist/core/agent-adapter.d.ts +8 -0
- package/dist/core/agent-adapter.d.ts.map +1 -1
- package/dist/core/agent-adapter.js +49 -5
- package/dist/core/agent-adapter.js.map +1 -1
- package/dist/core/ask-host-ai.d.ts.map +1 -1
- package/dist/core/ask-host-ai.js +4 -2
- package/dist/core/ask-host-ai.js.map +1 -1
- package/dist/core/prompt-builder.d.ts.map +1 -1
- package/dist/core/prompt-builder.js +3 -2
- package/dist/core/prompt-builder.js.map +1 -1
- package/dist/core/requirement-clarifier.d.ts.map +1 -1
- package/dist/core/requirement-clarifier.js +4 -0
- package/dist/core/requirement-clarifier.js.map +1 -1
- package/dist/core/spec-paths.d.ts.map +1 -1
- package/dist/core/spec-paths.js +5 -0
- package/dist/core/spec-paths.js.map +1 -1
- package/dist/core/unit-context-assembler.d.ts.map +1 -1
- package/dist/core/unit-context-assembler.js +107 -4
- package/dist/core/unit-context-assembler.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiPD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/commands/analyze.ts"],"names":[],"mappings":"AA2FA,MAAM,WAAW,cAAc;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,UAAU,CAAC;IAC5C,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiPD,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA81E3E;AA+zBD,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,UAAU,CAAC,EAAE;IAAE,YAAY,EAAE,OAAO,CAAC;IAAC,cAAc,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAsuEzU;AAopBD,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,eAAO,MAAM,eAAe,kCAAkC,CAAC;AAE/D,oBAAoB;AACpB,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAkGrF;AAED,kBAAkB;AAClB,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAK/F;AAED,iBAAiB;AACjB,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAgBjF;AAED,iBAAiB;AACjB,wBAAsB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAiB/G;AAcD,oCAAoC;AACpC,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,aAAa,EAAE,EACtB,QAAQ,EAAE,YAAY,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,CAmFjB;AAkBD,2BAA2B;AAC3B,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,aAAa,EACnB,QAAQ,EAAE,aAAa,EAAE,GACxB,OAAO,CAAC,MAAM,CAAC,CA0EjB;AAoYD;;;GAGG;AACH,wBAAsB,yBAAyB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8D7H"}
|
package/dist/commands/analyze.js
CHANGED
|
@@ -714,6 +714,25 @@ async function analyzeCommand(options) {
|
|
|
714
714
|
return;
|
|
715
715
|
}
|
|
716
716
|
}
|
|
717
|
+
else if (options.autoUnits || options.resumeUnits) {
|
|
718
|
+
// v8.3.266+: 缓存与磁盘 features/ 权威源对照——旧缓存可能含 unitize 重切前的假单元
|
|
719
|
+
// (实测会议项目 23 个缓存单元中 6 个在 features/ 不存在),调度循环会给假单元生成
|
|
720
|
+
// Prompt 写到 features 锚点之外的目录,白写且 taskify/Phase 2 均不读。
|
|
721
|
+
const diskUnits = await extractIterationUnits(iterDir);
|
|
722
|
+
const cacheNames = new Set(units.map((u) => u.name));
|
|
723
|
+
const diskNames = new Set(diskUnits.map((u) => u.name));
|
|
724
|
+
const mismatched = diskUnits.length > 0
|
|
725
|
+
&& (diskUnits.length !== units.length || [...diskNames].some((n) => !cacheNames.has(n)));
|
|
726
|
+
if (mismatched) {
|
|
727
|
+
units = diskUnits;
|
|
728
|
+
await (0, fs_extra_1.ensureDir)((0, path_1.dirname)(unitsCachePath));
|
|
729
|
+
await (0, fs_extra_1.writeFile)(unitsCachePath, JSON.stringify(units, null, 2));
|
|
730
|
+
logger_1.logger.warn(`⚠️ 单元清单与磁盘 features/ 目录不一致,已刷新: ${units.length} 个`);
|
|
731
|
+
for (const u of units) {
|
|
732
|
+
logger_1.logger.info(` ${u.id}: ${u.name} (${u.source}#L${u.sourceRange.startLine})`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
717
736
|
// Step 2: 汇总模式(--consolidate)
|
|
718
737
|
if (options.consolidate) {
|
|
719
738
|
const analyses = await loadUnitAnalyses(iter);
|
|
@@ -771,14 +790,33 @@ async function analyzeCommand(options) {
|
|
|
771
790
|
// CLI 每轮推进状态机:校验产出 → 已就绪自动标记并推进,未就绪输出该单元 prompt(exitCode 10)
|
|
772
791
|
// → 宿主 AI 落盘 → 再次调用本命令 → CLI 校验通过自动推进下一个单元,直到全部完成
|
|
773
792
|
if (options.autoUnits || options.resumeUnits) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
793
|
+
// v8.3.266: Qoder 环境非 TTY 下优先 SDK 派单——CLI 进程内直接调起子 Agent
|
|
794
|
+
// (独立上下文、acceptEdits 直接写文件),校验通过后自动推进下一单元,宿主全程
|
|
795
|
+
// 不参与;SDK 不可用/失败时回退标记模式(exitCode 10 宿主接力)。
|
|
796
|
+
// 收敛性:每轮单元状态必然前进(done/failed)或 SDK 失败退出,不会死循环。
|
|
797
|
+
const sdkSupported = !process.stdout.isTTY && (0, agent_adapter_1.isSdkDispatchSupported)();
|
|
798
|
+
const maxDispatchRounds = units.length * 3 + 5; // 防御性上限(每单元最多 3 次尝试 + 余量)
|
|
799
|
+
let dispatchCount = 0;
|
|
800
|
+
while (true) {
|
|
801
|
+
const loopResult = await runUnitDispatchLoop(iter, iterDir, units);
|
|
802
|
+
if (loopResult.finished) {
|
|
803
|
+
await printUnitLoopSummary(iter, iterDir, units, loopResult);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const fallbackPrompt = `${loopResult.prompt}\n\n## 完成后继续\n\n落盘后运行以下命令,CLI 将校验产出并自动推进到下一个单元:\n\n\`\`\`bash\nspeccore analyze --resume-units -I ${iter}\n\`\`\`\n`;
|
|
807
|
+
if (!sdkSupported || !(await trySdkDispatchUnit(loopResult.prompt, iter, loopResult.step.name))) {
|
|
808
|
+
// 回退标记模式(非 Qoder 宿主 / TTY / SDK 失败均走此路径)
|
|
809
|
+
await emitAiPrompt(iter, fallbackPrompt, { mode: 'resume-units-loop' });
|
|
810
|
+
process.exitCode = 10;
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
if (++dispatchCount >= maxDispatchRounds) {
|
|
814
|
+
logger_1.logger.warn(`⚠️ SDK 派单轮次超限(${maxDispatchRounds}),退出调度循环`);
|
|
815
|
+
process.exitCode = 10;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
778
818
|
}
|
|
779
|
-
|
|
780
|
-
process.exitCode = 10;
|
|
781
|
-
return;
|
|
819
|
+
// 理论不可达:while(true) 内所有路径均 return
|
|
782
820
|
}
|
|
783
821
|
// 3c. --unit: 指定单元(支持逗号分隔多个,如 M-01,M-03)
|
|
784
822
|
if (options.unit) {
|
|
@@ -1765,6 +1803,8 @@ async function analyzeCommand(options) {
|
|
|
1765
1803
|
const clarifyBlocks = parseClarifyMarkers(options.apply);
|
|
1766
1804
|
if (clarifyBlocks.size > 0 && !isGlobalScope) {
|
|
1767
1805
|
logger_1.logger.info(`📋 检测到 ${clarifyBlocks.size} 个澄清文档,先写入...`);
|
|
1806
|
+
// v8.3.267: 提前取真实功能单元名集合(只取一次),供幽灵路由判定
|
|
1807
|
+
const validFeatures = new Set(iterDir ? await (0, spec_paths_2.parseFeatureList)(iterDir) : []);
|
|
1768
1808
|
for (const [filename, content] of clarifyBlocks) {
|
|
1769
1809
|
const { parseClarifiedRequirement, buildClarifiedHeader } = await Promise.resolve().then(() => __importStar(require('../core/requirement-clarifier')));
|
|
1770
1810
|
const { content: cleaned } = parseClarifiedRequirement(content);
|
|
@@ -1773,7 +1813,25 @@ async function analyzeCommand(options) {
|
|
|
1773
1813
|
// 解析写入路径:支持新格式(overview/、{feature}/overview/)和旧格式(requirements/)
|
|
1774
1814
|
let fp;
|
|
1775
1815
|
if (filename.includes('/') || filename.includes('\\')) {
|
|
1776
|
-
|
|
1816
|
+
// v8.3.267: 幽灵路由归一化——AI 常把「文档 slug」当 feature-name 生成
|
|
1817
|
+
// [CLARIFY:overview/overview/CLARIFY.md]、[CLARIFY:admin-web/overview/CLARIFY.md]
|
|
1818
|
+
// 等标记,原样落地会产出 overview/overview/、admin-web/ 等幽灵目录。
|
|
1819
|
+
// 首段不是真实功能单元时:
|
|
1820
|
+
// 「overview」→ 全局澄清 020-specs/overview/CLARIFY.md
|
|
1821
|
+
// 其余文档 slug → 文档级黄金需求 020-specs/requirements/{slug}-clarified.md
|
|
1822
|
+
const seg = filename.split(/[\\/]/)[0];
|
|
1823
|
+
const isUnitOverview = /^[^\\/]+\/overview\/CLARIFY\.md$/i.test(filename);
|
|
1824
|
+
if (isUnitOverview && seg === 'overview') {
|
|
1825
|
+
fp = (0, path_1.join)(iterDir, '020-specs', 'overview', 'CLARIFY.md');
|
|
1826
|
+
logger_1.logger.warn(` ↪ ${filename} 首段「overview」为全局澄清,已归一化到 020-specs/overview/CLARIFY.md`);
|
|
1827
|
+
}
|
|
1828
|
+
else if (isUnitOverview && !validFeatures.has(seg)) {
|
|
1829
|
+
fp = (0, path_1.join)(iterDir, '020-specs', 'requirements', `${seg}-clarified.md`);
|
|
1830
|
+
logger_1.logger.warn(` ↪ ${filename} 首段「${seg}」非功能单元,已路由到 requirements/${seg}-clarified.md(文档级黄金需求)`);
|
|
1831
|
+
}
|
|
1832
|
+
else {
|
|
1833
|
+
fp = (0, path_1.join)(iterDir, '020-specs', filename);
|
|
1834
|
+
}
|
|
1777
1835
|
}
|
|
1778
1836
|
else {
|
|
1779
1837
|
// 兼容旧格式:无路径前缀的文件名写入 requirements/
|
|
@@ -6359,12 +6417,26 @@ async function extractIterationUnits(iterDir) {
|
|
|
6359
6417
|
const featuresDir = (0, path_1.join)(reqDir, 'features');
|
|
6360
6418
|
if (await (0, fs_extra_1.pathExists)(featuresDir)) {
|
|
6361
6419
|
const entries = await (0, fs_extra_2.readdir)(featuresDir, { withFileTypes: true });
|
|
6362
|
-
|
|
6420
|
+
// v8.3.267: 过滤 overview- 前缀的全局章节目录(unitize 误切产物,
|
|
6421
|
+
// 如「核心业务规则跨端通用」),防止假单元混入调度循环
|
|
6422
|
+
const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !e.name.startsWith('_') && !e.name.startsWith('overview-'));
|
|
6363
6423
|
for (const dir of dirs) {
|
|
6364
6424
|
const readmePath = await (0, feature_source_1.resolveUnitSourcePath)((0, path_1.join)(featuresDir, dir.name));
|
|
6365
6425
|
let content = '';
|
|
6366
6426
|
if (await (0, fs_extra_1.pathExists)(readmePath)) {
|
|
6367
|
-
|
|
6427
|
+
// v8.3.267: 图片引用可能落在 500 字符截断之外,显式保留引用行,
|
|
6428
|
+
// 保证单元上下文的图片清单完整(AI 可据此 Read 原型图)
|
|
6429
|
+
const full = await (0, fs_extra_2.readFile)(readmePath, 'utf-8');
|
|
6430
|
+
content = full.slice(0, 500);
|
|
6431
|
+
const imgRefs = new Set();
|
|
6432
|
+
const imgRe = /!\[[^\]]*\]\(([^)\s]+)\)/g;
|
|
6433
|
+
let m;
|
|
6434
|
+
while ((m = imgRe.exec(full)) !== null)
|
|
6435
|
+
imgRefs.add(m[1]);
|
|
6436
|
+
const outside = [...imgRefs].filter(r => !content.includes(r));
|
|
6437
|
+
if (outside.length > 0) {
|
|
6438
|
+
content += `\n\n<!-- 原文图片引用(截断外保留)-->\n${outside.map(r => ``).join('\n')}`;
|
|
6439
|
+
}
|
|
6368
6440
|
}
|
|
6369
6441
|
units.push({
|
|
6370
6442
|
id: `M-${String(idCounter++).padStart(2, '0')}`,
|
|
@@ -6470,7 +6542,9 @@ async function loadUnitAnalyses(iteration) {
|
|
|
6470
6542
|
async function detectPendingUnits(iteration, allUnits) {
|
|
6471
6543
|
const completed = await loadUnitAnalyses(iteration);
|
|
6472
6544
|
const completedIds = new Set(completed.map(a => a.id));
|
|
6473
|
-
|
|
6545
|
+
// v8.3.266+: 用绝对路径迭代目录替代相对路径拼接——cwd 不在迭代父目录时旧写法会指向错误路径,
|
|
6546
|
+
// 导致 unitHasSkeletonOutput 兜底检查全部误判为骨架(路径不存在)
|
|
6547
|
+
const iterDir = await (0, context_1.getIterationDir)(iteration);
|
|
6474
6548
|
const pending = [];
|
|
6475
6549
|
for (const u of allUnits) {
|
|
6476
6550
|
if (!completedIds.has(u.id)) {
|
|
@@ -6864,6 +6938,7 @@ async function buildUnitLoopPrompt(iterDir, iter, unit, allUnits, attempt, lastE
|
|
|
6864
6938
|
约束:
|
|
6865
6939
|
- 每个文件内容不少于 200 字符,禁止输出空模板或骨架占位
|
|
6866
6940
|
- 写入前先创建目录;写完后回复一行:\`DONE ${unit.name}\`
|
|
6941
|
+
- 若上下文中含「需求图片/原型」或 \`\` 图片引用,请先用 Read 工具打开图片文件查看,再结合图片信息撰写规格;不要忽略图片提示
|
|
6867
6942
|
`;
|
|
6868
6943
|
if (attempt > 1 && lastError) {
|
|
6869
6944
|
extra += `
|
|
@@ -6876,6 +6951,89 @@ async function buildUnitLoopPrompt(iterDir, iter, unit, allUnits, attempt, lastE
|
|
|
6876
6951
|
}
|
|
6877
6952
|
return base + extra;
|
|
6878
6953
|
}
|
|
6954
|
+
/**
|
|
6955
|
+
* v8.3.266: 调度循环 SDK 派单——Qoder 环境非 TTY 下由 CLI 直接调起子 Agent
|
|
6956
|
+
* (独立上下文、acceptEdits 直接写文件),返回 true 表示派单成功(含自动落盘)。
|
|
6957
|
+
*/
|
|
6958
|
+
async function trySdkDispatchUnit(prompt, iter, unitName) {
|
|
6959
|
+
try {
|
|
6960
|
+
const agentCtx = {
|
|
6961
|
+
subagent: 'requirement-analyzer',
|
|
6962
|
+
iteration: iter,
|
|
6963
|
+
contextBudget: 6000,
|
|
6964
|
+
contextType: 'incremental',
|
|
6965
|
+
cwd: (0, task_utils_1.findProjectRoot)() || process.cwd(),
|
|
6966
|
+
};
|
|
6967
|
+
const dispatchResult = await (0, agent_adapter_1.dispatchSubagent)(agentCtx, prompt);
|
|
6968
|
+
if (!dispatchResult || !dispatchResult.success) {
|
|
6969
|
+
logger_1.logger.debug('[analyze] SDK 派单未成功,回退标记模式');
|
|
6970
|
+
return false;
|
|
6971
|
+
}
|
|
6972
|
+
// v8.3.266: 防御空转——success 但既没写文件也没返回可落盘内容,视为无效派单,
|
|
6973
|
+
// 立即回退标记模式(否则循环会白白耗尽该单元 3 次尝试并误标 failed)
|
|
6974
|
+
if (!dispatchResult.content && (!dispatchResult.filesChanged || dispatchResult.filesChanged.length === 0)) {
|
|
6975
|
+
logger_1.logger.warn('[analyze] SDK 派单无产出(未写文件且无回传内容),回退标记模式');
|
|
6976
|
+
return false;
|
|
6977
|
+
}
|
|
6978
|
+
logger_1.logger.info(`[analyze] 调度循环子 Agent SDK 派单成功: ${unitName}`);
|
|
6979
|
+
// v8.3.196+ 自动落盘:子 Agent 未直接写文件时,解析其返回的 JSON/[DOC:] 内容
|
|
6980
|
+
// 并路由到该单元的 overview/ 目录(applyDocContent 无单元路由,此处专用落盘)
|
|
6981
|
+
if (dispatchResult.content) {
|
|
6982
|
+
const autoApplied = await applyUnitLoopDocs(dispatchResult.content, iter, unitName);
|
|
6983
|
+
if (autoApplied > 0) {
|
|
6984
|
+
process.stdout.write(`✅ 已自动写入 ${autoApplied} 个文档到 020-specs/${sanitizeUnitDirName(unitName)}/overview/\n`);
|
|
6985
|
+
}
|
|
6986
|
+
}
|
|
6987
|
+
if (dispatchResult.filesChanged && dispatchResult.filesChanged.length > 0) {
|
|
6988
|
+
process.stdout.write(`📄 文件变更: ${dispatchResult.filesChanged.join(', ')}\n`);
|
|
6989
|
+
}
|
|
6990
|
+
return true;
|
|
6991
|
+
}
|
|
6992
|
+
catch (e) {
|
|
6993
|
+
logger_1.logger.debug(`[analyze] SDK 派单异常: ${e.message},回退标记模式`);
|
|
6994
|
+
return false;
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
/**
|
|
6998
|
+
* v8.3.266: 把子 Agent 回传的 JSON/[DOC:] 内容写入指定单元的 overview/ 目录。
|
|
6999
|
+
* 文档名归一化:analyze prompt 用 [DOC:REQ.md],落盘用 REQUIREMENT.md(validateUnitSpec 校验名单)。
|
|
7000
|
+
*/
|
|
7001
|
+
async function applyUnitLoopDocs(content, iter, unitName) {
|
|
7002
|
+
const trimmed = content.trim();
|
|
7003
|
+
if (!trimmed)
|
|
7004
|
+
return 0;
|
|
7005
|
+
const iterDir = await (0, context_1.getIterationDir)(iter);
|
|
7006
|
+
const unitDir = (0, path_1.join)(iterDir, '020-specs', sanitizeUnitDirName(unitName), 'overview');
|
|
7007
|
+
let docs = {};
|
|
7008
|
+
if (trimmed.startsWith('{')) {
|
|
7009
|
+
try {
|
|
7010
|
+
const parsed = JSON.parse(trimmed);
|
|
7011
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
7012
|
+
docs = parsed;
|
|
7013
|
+
}
|
|
7014
|
+
catch { /* 回退 [DOC:] 标记 */ }
|
|
7015
|
+
}
|
|
7016
|
+
if (Object.keys(docs).length === 0) {
|
|
7017
|
+
for (const [name, c] of parseDocMarkers(trimmed))
|
|
7018
|
+
docs[name] = c;
|
|
7019
|
+
}
|
|
7020
|
+
if (Object.keys(docs).length === 0)
|
|
7021
|
+
return 0;
|
|
7022
|
+
const NAME_MAP = { 'REQ.md': 'REQUIREMENT.md' };
|
|
7023
|
+
let applied = 0;
|
|
7024
|
+
for (const [filename, docContent] of Object.entries(docs)) {
|
|
7025
|
+
if (typeof docContent !== 'string' || !docContent.trim())
|
|
7026
|
+
continue;
|
|
7027
|
+
const baseName = (filename.split('/').pop() || filename).trim();
|
|
7028
|
+
const normalized = NAME_MAP[baseName] || baseName;
|
|
7029
|
+
if (!normalized.endsWith('.md') || normalized.startsWith('.'))
|
|
7030
|
+
continue;
|
|
7031
|
+
await (0, fs_extra_1.ensureDir)(unitDir);
|
|
7032
|
+
await (0, fs_extra_1.writeFile)((0, path_1.join)(unitDir, normalized), docContent);
|
|
7033
|
+
applied++;
|
|
7034
|
+
}
|
|
7035
|
+
return applied;
|
|
7036
|
+
}
|
|
6879
7037
|
/** 单元分析调度循环:构建步骤清单并推进一轮 */
|
|
6880
7038
|
async function runUnitDispatchLoop(iter, iterDir, units) {
|
|
6881
7039
|
const steps = units.map((u) => ({
|
|
@@ -6905,9 +7063,49 @@ async function printUnitLoopSummary(iter, iterDir, units, result) {
|
|
|
6905
7063
|
logger_1.logger.success(`🎉 所有 ${s.done.length} 个功能单元分析完成!`);
|
|
6906
7064
|
}
|
|
6907
7065
|
logger_1.logger.info(` 下一步: 汇总统一报告 speccore analyze --prompt -I ${iter} --consolidate`);
|
|
7066
|
+
// v8.3.266+: 循环只保证 overview/ 3 份,端级文档({feature}/{端}/)由 Phase 2 负责——
|
|
7067
|
+
// 此前完成提示不提 Phase 2,用户以为分析已闭环,taskify 时端级内容仍是骨架
|
|
7068
|
+
const platformSk = await countPlatformSkeletons(iterDir);
|
|
7069
|
+
if (platformSk.total > 0 && platformSk.skeleton > 0) {
|
|
7070
|
+
logger_1.logger.warn(`⚠️ 端级文档仍有 ${platformSk.skeleton}/${platformSk.total} 份为骨架(overview/ 之外)`);
|
|
7071
|
+
logger_1.logger.info(` Phase 2 生成各端专属文档: speccore analyze --prompt -I ${iter} --phase 2`);
|
|
7072
|
+
}
|
|
6908
7073
|
// v8.3.234+: 执行完成质量报告(成功/失败清单 + 产出物评分)落盘 QUALITY_AUDIT.md
|
|
6909
7074
|
await buildAndSaveQualityReport(iterDir, iter, units);
|
|
6910
7075
|
}
|
|
7076
|
+
/**
|
|
7077
|
+
* v8.3.266+: 统计 020-specs/{feature}/{端}/ 下仍为骨架的 .md 文件数。
|
|
7078
|
+
* 用于调度循环完成后引导 Phase 2(调度循环只保证 overview/ 3 份落盘)。
|
|
7079
|
+
*/
|
|
7080
|
+
async function countPlatformSkeletons(iterDir) {
|
|
7081
|
+
let skeleton = 0;
|
|
7082
|
+
let total = 0;
|
|
7083
|
+
try {
|
|
7084
|
+
const specDir = (0, path_1.join)(iterDir, '020-specs');
|
|
7085
|
+
if (!(await (0, fs_extra_1.pathExists)(specDir)))
|
|
7086
|
+
return { skeleton, total };
|
|
7087
|
+
const features = await (0, spec_paths_2.parseFeatureList)(iterDir);
|
|
7088
|
+
const platforms = await (0, spec_paths_2.parsePlatformList)();
|
|
7089
|
+
for (const feature of features) {
|
|
7090
|
+
for (const platform of platforms) {
|
|
7091
|
+
const pDir = (0, path_1.join)(specDir, feature, platform);
|
|
7092
|
+
if (!(await (0, fs_extra_1.pathExists)(pDir)))
|
|
7093
|
+
continue;
|
|
7094
|
+
const entries = await (0, fs_extra_2.readdir)(pDir, { withFileTypes: true }).catch(() => []);
|
|
7095
|
+
for (const entry of entries) {
|
|
7096
|
+
if (!entry.name.endsWith('.md') || !entry.isFile())
|
|
7097
|
+
continue;
|
|
7098
|
+
total++;
|
|
7099
|
+
const content = (await (0, fs_extra_2.readFile)((0, path_1.join)(pDir, entry.name), 'utf-8').catch(() => '')).trim();
|
|
7100
|
+
if (!content || content.includes('SPEC-SKELETON') || content.length < 200)
|
|
7101
|
+
skeleton++;
|
|
7102
|
+
}
|
|
7103
|
+
}
|
|
7104
|
+
}
|
|
7105
|
+
}
|
|
7106
|
+
catch { /* 统计失败不阻断主流程 */ }
|
|
7107
|
+
return { skeleton, total };
|
|
7108
|
+
}
|
|
6911
7109
|
/**
|
|
6912
7110
|
* v8.3.234+: 生成并落盘「功能单元分析质量报告」——成功/失败清单 + 每单元产出物评分。
|
|
6913
7111
|
* 落盘到 020-specs/QUALITY_AUDIT.md;无可评单元时返回 null。
|