soloforge 1.2.10 → 1.2.12

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 (36) hide show
  1. package/dist/adapters/claude_code/hooks.d.ts +1 -1
  2. package/dist/adapters/claude_code/hooks.d.ts.map +1 -1
  3. package/dist/adapters/claude_code/hooks.js +11 -1
  4. package/dist/adapters/claude_code/hooks.js.map +1 -1
  5. package/dist/bin/soloforge.js +36 -0
  6. package/dist/bin/soloforge.js.map +1 -1
  7. package/dist/engine/batch1_scenario_registry.d.ts +6 -6
  8. package/dist/engine/batch1_scenario_registry.d.ts.map +1 -1
  9. package/dist/engine/batch1_scenario_registry.js +42 -7
  10. package/dist/engine/batch1_scenario_registry.js.map +1 -1
  11. package/dist/engine/batch1_scenario_runners.d.ts +12 -0
  12. package/dist/engine/batch1_scenario_runners.d.ts.map +1 -1
  13. package/dist/engine/batch1_scenario_runners.js +148 -0
  14. package/dist/engine/batch1_scenario_runners.js.map +1 -1
  15. package/dist/engine/input_material_extractor.d.ts +46 -0
  16. package/dist/engine/input_material_extractor.d.ts.map +1 -0
  17. package/dist/engine/input_material_extractor.js +155 -0
  18. package/dist/engine/input_material_extractor.js.map +1 -0
  19. package/dist/engine/intent_route_scorer.d.ts +45 -0
  20. package/dist/engine/intent_route_scorer.d.ts.map +1 -0
  21. package/dist/engine/intent_route_scorer.js +326 -0
  22. package/dist/engine/intent_route_scorer.js.map +1 -0
  23. package/dist/engine/intent_router.d.ts +58 -21
  24. package/dist/engine/intent_router.d.ts.map +1 -1
  25. package/dist/engine/intent_router.js +236 -217
  26. package/dist/engine/intent_router.js.map +1 -1
  27. package/dist/engine/intent_signal_extractor.d.ts +70 -0
  28. package/dist/engine/intent_signal_extractor.d.ts.map +1 -0
  29. package/dist/engine/intent_signal_extractor.js +235 -0
  30. package/dist/engine/intent_signal_extractor.js.map +1 -0
  31. package/dist/engine/task_context.d.ts.map +1 -1
  32. package/dist/engine/task_context.js +4 -0
  33. package/dist/engine/task_context.js.map +1 -1
  34. package/dist/types.d.ts +3 -12
  35. package/dist/types.d.ts.map +1 -1
  36. package/package.json +1 -1
@@ -1,28 +1,24 @@
1
- import { classifyIngestionStatus } from "./input_material_contract_registry.js";
1
+ /**
2
+ * Intent Router — 意图路由器
3
+ *
4
+ * 根据用户意图文本确定语义通道 (route) 和执行形态 (execution_shape)。
5
+ * 零 AI 依赖,纯规则匹配。
6
+ *
7
+ * 替代旧的 task_type -> strategy 入口裁决链路。
8
+ * task_type 保留为辅助描述,不再单独决定是否进入 SoloForge。
9
+ *
10
+ * v2: 模块化拆分 — 路由判定由"信号提取 + 归一化 + 打分 + 冲突仲裁"完成,
11
+ * 不再依赖单条整句正则。
12
+ */
13
+ import { extractInputMaterials } from "./input_material_extractor.js";
14
+ import { extractSignals } from "./intent_signal_extractor.js";
15
+ import { scoreRoute } from "./intent_route_scorer.js";
2
16
  console.error("[soloForge] 意图路由: 模块已加载");
3
- // ── 规则表 ──
4
- /** 跳过类模式(闲聊、日常问答等不需要进入 SoloForge 的意图) */
5
- const SKIP_PATTERNS = [
6
- { pattern: /^(你好|hi|hello|hey|嗨|早上好|下午好|晚上好)[\s!!.。]*$/i, reason: "greeting" },
7
- { pattern: /^(今天|现在|几号|什么时候|什么时候了)/, reason: "casual_question" },
8
- { pattern: /不用\s*(soloForge|solo\s*forge)/i, reason: "explicit_skip" },
9
- { pattern: /直接(告诉|说|回答|给)/, reason: "explicit_skip" },
10
- ];
11
- /** 通用知识问答模式(不需要代码执行) */
12
- const GENERAL_QA_PATTERNS = [
13
- /(?:什么(?:是|叫|意思))\s*(?:REST|API|DTO|DAO|SOA|DDD|TDD|BDD|CI\/CD|SQL|NoSQL|TypeScript|JavaScript|Java|Python|Go|Docker|K8s|Kubernetes|React|Vue|Spring|Node|Nginx|Redis|MySQL|MongoDB|GraphQL|gRPC|WebSocket|OAuth|JWT|SOLID|DRY|KISS|YAGNI)/i,
14
- /^(?:TypeScript|Java|Python|Go|React|Vue|Spring)\s*(?:union type|泛型|接口|类|装饰器|闭包|协程|通道|组件|生命周期|状态管理|中间件)/i,
15
- ];
16
- /** 纯解释类模式 */
17
- const PURE_EXPLAIN_PATTERNS = [
18
- /(?:解释|说明)(?:一下|下)?\s*(?:这个|这|这模块|这代码|这段|这个模块)\s*(?:怎么|如何|是什么|是什么意思|的|了)/,
19
- /^(?:解释|说明)(?:一下|下)?\s/,
20
- /^(?:这个|这)\s*(?:是|什么)/,
21
- ];
22
- /** 产物信号规则表 */
17
+ // ── 规则表(保留用于兼容层) ──
18
+ /** 产物信号规则表 保留用于 detectArtifact 回退 */
23
19
  const ARTIFACT_SIGNALS = [
24
- { pattern: /原型(?:说明|规约|描述|文档|报告|spec)/, artifactKind: "prototype_spec", defaultName: "原型说明", template: "原型说明模版.md" },
25
- { pattern: /接口(?:设计|清单|文档|spec)/, artifactKind: "api_spec", defaultName: "接口设计" },
20
+ { pattern: /原型(?:说明|规约|描述|文档|报告|spec)|页面(?:说明|流程)|交互说明|prototype\s*spec/i, artifactKind: "prototype_spec", defaultName: "原型说明", template: "原型说明模版.md" },
21
+ { pattern: /接口(?:设计|清单|文档|spec|梳理)/, artifactKind: "api_spec", defaultName: "接口设计" },
26
22
  { pattern: /(?:架构设计|详细设计)/, artifactKind: "design_doc", defaultName: "设计文档" },
27
23
  { pattern: /测试计划/, artifactKind: "test_plan", defaultName: "测试计划" },
28
24
  { pattern: /迁移(?:方案|评估|计划)/, artifactKind: "migration_plan", defaultName: "迁移方案" },
@@ -38,12 +34,11 @@ const ARTIFACT_SIGNALS = [
38
34
  /** 产物动词: 只有明确表达"产出/生成"意图的才算 */
39
35
  const ARTIFACT_VERBS = [
40
36
  /生成/, /输出/, /抓取/, /提取/, /整理成/, /写成/,
41
- /梳理.*(?:成|为|出)/,
42
37
  /拆(?:成|分为)/,
43
38
  /导出/, /输出为/, /保存为/, /产出到/, /做一份/, /写一份/, /生成一份/,
44
- /做成/, /形成/, /制作/, /从.*(?:生成|输出|提取|抓取)/,
39
+ /做成?/, /形成/, /制作/, /从.*(?:生成|输出|提取|抓取)/,
45
40
  ];
46
- /** 源码提取信号: 只有明确从外部材料读取的才算 */
41
+ /** 源码提取信号 */
47
42
  const SOURCE_EXTRACTION_PATTERNS = [
48
43
  { pattern: /\.(zip|tar|gz|rar|7z|jar|war)/i, inputKind: "archive" },
49
44
  { pattern: /源码包|压缩包|zip包|包文件/, inputKind: "archive" },
@@ -51,10 +46,12 @@ const SOURCE_EXTRACTION_PATTERNS = [
51
46
  { pattern: /(?:logs?\/|\.log|日志(?:文件|目录|路径))/i, inputKind: "log" },
52
47
  { pattern: /从报错日志/, inputKind: "log" },
53
48
  ];
54
- /** 提取类动词: "从X提取/读取" 信号优先于 artifact_generation */
49
+ /** 提取类动词 */
55
50
  const EXTRACTION_VERB_PATTERNS = [
56
51
  /从\s*.*(?:提取|抽取)/,
57
- /读取\s+.*生成/,
52
+ /从\s*.*(?:生成|输出)/,
53
+ /(?:拿|用)\s*.*(?:做|生成|输出|提取|梳理|整理)/,
54
+ /读取\s*.*生成/,
58
55
  /抓取.*(?:源码|源代码|原型)/,
59
56
  ];
60
57
  /** 代码修改模式 */
@@ -103,122 +100,38 @@ const OPERATION_PATTERNS = [
103
100
  /准备.*(?:发布前|检查清单)/,
104
101
  /发布前(?:检查|准备)/,
105
102
  ];
103
+ /** 跳过类模式 */
104
+ const SKIP_PATTERNS = [
105
+ { pattern: /^(你好|hi|hello|hey|嗨|早上好|下午好|晚上好)[\s!!.。]*$/i, reason: "greeting" },
106
+ { pattern: /^(今天|现在|几号|什么时候|什么时候了)/, reason: "casual_question" },
107
+ { pattern: /不用\s*(soloForge|solo\s*forge)/i, reason: "explicit_skip" },
108
+ { pattern: /直接(告诉|说|回答|给)/, reason: "explicit_skip" },
109
+ ];
110
+ /** 通用知识问答模式 */
111
+ const GENERAL_QA_PATTERNS = [
112
+ /(?:什么(?:是|叫|意思))\s*(?:REST|API|DTO|DAO|SOA|DDD|TDD|BDD|CI\/CD|SQL|NoSQL|TypeScript|JavaScript|Java|Python|Go|Docker|K8s|Kubernetes|React|Vue|Spring|Node|Nginx|Redis|MySQL|MongoDB|GraphQL|gRPC|WebSocket|OAuth|JWT|SOLID|DRY|KISS|YAGNI)/i,
113
+ /^(?:TypeScript|Java|Python|Go|React|Vue|Spring)\s*(?:union type|泛型|接口|类|装饰器|闭包|协程|通道|组件|生命周期|状态管理|中间件)/i,
114
+ ];
115
+ /** 纯解释类模式 */
116
+ const PURE_EXPLAIN_PATTERNS = [
117
+ /(?:解释|说明)(?:一下|下)?\s*(?:这个|这|这模块|这代码|这段|这个模块)\s*(?:怎么|如何|是什么|是什么意思|的|了)/,
118
+ /^(?:解释|说明)(?:一下|下)?\s/,
119
+ /^(?:这个|这)\s*(?:是|什么)/,
120
+ ];
121
+ // ── 路径提取兼容函数(调用新模块) ──
106
122
  /**
107
123
  * 从意图文本中提取输入路径。
108
- * 支持绝对路径和相对路径。相对路径保留原文不截断。
109
- *
110
- * @param intent - 用户意图文本
111
- * @returns 提取到的路径列表,包含类型和摄取状态
124
+ * 保留原函数签名,内部委托给 input_material_extractor。
112
125
  */
113
126
  function extractInputPaths(intent) {
114
- const paths = [];
115
- const seen = new Set();
116
- function addPath(p, kind) {
117
- if (seen.has(p))
118
- return;
119
- seen.add(p);
120
- // 规范化 ~/ 路径用于策略检查,但保留原始路径
121
- const normalizedForPolicy = p.replace(/^~\//, (process.env.HOME || "/") + "/");
122
- const status = classifyIngestionStatus(normalizedForPolicy);
123
- paths.push({ kind, path: p, ingestion_status: status });
124
- }
125
- // 收集所有绝对路径的 span(start, end),用于后续排除重叠的相对匹配
126
- const absoluteSpans = [];
127
- function recordAbsoluteSpan(match) {
128
- absoluteSpans.push({ start: match.index, end: match.index + match[0].length });
129
- }
130
- function overlapsAbsolute(start, end) {
131
- return absoluteSpans.some((s) => start >= s.start && start < s.end);
132
- }
133
- // 0. ~/ 路径(home 目录相对路径)
134
- const tildeArchive = intent.match(/(~\/[\p{L}\p{N}_.-]+)+\.(?:zip|tar\.gz|gz|rar|7z|jar|war)/iu);
135
- if (tildeArchive) {
136
- addPath(tildeArchive[0], "archive");
137
- recordAbsoluteSpan(tildeArchive);
138
- }
139
- const tildeFileMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(~\/[\p{L}\p{N}_.-]+)+\.(?:log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml|pem|key|rsa)/giu);
140
- for (const m of tildeFileMatches) {
141
- const p = m[0];
142
- if (/\.log$/i.test(p))
143
- addPath(p, "log");
144
- else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
145
- addPath(p, "doc");
146
- else
147
- addPath(p, "file");
148
- recordAbsoluteSpan(m);
149
- }
150
- const tildeDirMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(~\/[\p{L}\p{N}_.-]+)+(?=\/?(?:\s|$|,|,|\)|\]|的|下|中|里))/gu);
151
- for (const m of tildeDirMatches) {
152
- addPath(m[0], "directory");
153
- recordAbsoluteSpan(m);
154
- }
155
- // 通用 ~/ 路径匹配(未被上述特定模式捕获的路径,如 ~/.ssh/id_rsa)
156
- const tildeGenericMatches = intent.matchAll(/(~\/[\p{L}\p{N}_.\-\/]+)/gu);
157
- for (const m of tildeGenericMatches) {
158
- const p = m[0];
159
- if (!seen.has(p))
160
- addPath(p, "file");
161
- recordAbsoluteSpan(m);
162
- }
163
- // 1. 绝对 archive: /xxx/yyy.zip — 跳过与 ~/ 路径重叠的
164
- const absoluteArchive = intent.match(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+\.(?:zip|tar\.gz|gz|rar|7z|jar|war)/iu);
165
- if (absoluteArchive && !overlapsAbsolute(absoluteArchive.index, absoluteArchive.index + absoluteArchive[0].length)) {
166
- addPath(absoluteArchive[0], "archive");
167
- recordAbsoluteSpan(absoluteArchive);
168
- }
169
- // 2. 绝对文件路径: /tmp/error.log, /Users/Charlie/docs/api.md 等
170
- const absoluteFileMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+\.(?:log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml)/giu);
171
- for (const m of absoluteFileMatches) {
172
- if (overlapsAbsolute(m.index, m.index + m[0].length))
173
- continue;
174
- const p = m[0];
175
- if (/\.log$/i.test(p))
176
- addPath(p, "log");
177
- else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
178
- addPath(p, "doc");
179
- else
180
- addPath(p, "file");
181
- recordAbsoluteSpan(m);
182
- }
183
- // 3. 绝对目录路径: /Users/Charlie/project/src 等
184
- const absoluteDirMatches = intent.matchAll(/(?<![\p{L}\p{N}_\/])(\/[\p{L}\p{N}_.-]+)+(?=\/?(?:\s|$|,|,|\)|\]|的|下|中|里))/gu);
185
- for (const m of absoluteDirMatches) {
186
- const p = m[0];
187
- if (overlapsAbsolute(m.index, m.index + m[0].length) || seen.has(p))
188
- continue;
189
- addPath(p, "directory");
190
- recordAbsoluteSpan(m);
191
- }
192
- // 4. 相对文件路径: xxx/yyy.ext — 跳过与绝对路径重叠的
193
- const fileMatches = intent.matchAll(/([\p{L}\p{N}_][\p{L}\p{N}_.-]*(?:\/[\p{L}\p{N}_.-]+)+\.(?:tar\.gz|zip|gz|rar|7z|jar|war|log|md|txt|doc|docx|pdf|java|py|ts|tsx|js|jsx|go|rs|json|yaml|yml|toml|pem|key|rsa))/giu);
194
- for (const m of fileMatches) {
195
- if (overlapsAbsolute(m.index, m.index + m[0].length))
196
- continue;
197
- const p = m[1].replace(/^从(?=[\p{L}\p{N}_][\p{L}\p{N}_.-]*\/)/u, "");
198
- if (/\.log$/i.test(p))
199
- addPath(p, "log");
200
- else if (/\.(md|txt|doc|pdf|docx)$/i.test(p))
201
- addPath(p, "doc");
202
- else if (/\.(zip|tar\.gz|gz|rar|7z|jar|war)$/i.test(p))
203
- addPath(p, "archive");
204
- else
205
- addPath(p, "file");
206
- }
207
- // 5. 相对目录路径: xxx/src, old-system/src, docs/api — 跳过与绝对路径重叠的
208
- const dirMatches = intent.matchAll(/([\p{L}\p{N}_.-]+(?:\/[\p{L}\p{N}_.-]+)*\/(?:src|old-[\p{L}\p{N}_]+|docs?|logs?))/gu);
209
- for (const m of dirMatches) {
210
- if (overlapsAbsolute(m.index, m.index + m[0].length))
211
- continue;
212
- addPath(m[1], "directory");
213
- }
214
- return paths;
127
+ const result = extractInputMaterials(intent);
128
+ return result.materials.map((m) => ({
129
+ kind: m.kind,
130
+ path: m.path,
131
+ ingestion_status: m.ingestion_status,
132
+ }));
215
133
  }
216
- /**
217
- * 从意图文本中检测产物信号。
218
- * @param intent - 用户意图文本
219
- * @param taskId - 可选的任务 ID,用于生成产物路径
220
- * @returns 产物信息,未检测到时返回 undefined
221
- */
134
+ // ── 产物检测(保留兼容) ──
222
135
  function detectArtifact(intent, taskId) {
223
136
  const id = taskId || "01";
224
137
  for (const signal of ARTIFACT_SIGNALS) {
@@ -238,21 +151,13 @@ function detectArtifact(intent, taskId) {
238
151
  }
239
152
  return undefined;
240
153
  }
241
- /** 检查意图文本中是否包含产物动词 */
242
154
  function hasArtifactVerb(intent) {
243
155
  return ARTIFACT_VERBS.some((p) => p.test(intent));
244
156
  }
245
- /** 检查意图文本中是否包含产物名词 */
246
157
  function hasArtifactNoun(intent) {
247
158
  return ARTIFACT_SIGNALS.some((s) => s.pattern.test(intent))
248
159
  || /(?:文档|方案|规约|计划|清单|报告|说明文档)$/.test(intent);
249
160
  }
250
- /**
251
- * 推断缺失材料的语义类型和澄清提示。
252
- * @param intent - 用户意图文本
253
- * @param hasPath - 意图文本中是否已包含路径
254
- * @returns 材料列表和缺失提示信息
255
- */
256
161
  function inferMissingInputs(intent, hasPath) {
257
162
  if (hasPath)
258
163
  return { materials: [], missing: [] };
@@ -274,67 +179,144 @@ function inferMissingInputs(intent, hasPath) {
274
179
  }
275
180
  /**
276
181
  * 主路由函数 — 根据用户意图文本确定语义路由和执行形态。
277
- * 采用分层匹配策略,优先级从高到低: 跳过类 > 源码提取 > 规划 > 操作 > 分析 > 产物生成 > 代码修改 > 审查 > 通用分析 > 灰区 > 纯解释 > 兜底。
182
+ * v2: 调用结构化信号提取 打分 冲突仲裁,不再依赖单条正则句子匹配。
278
183
  *
279
184
  * @param input - 路由输入参数,包含意图文本和可选的项目路径/任务 ID
280
185
  * @returns 意图路由决策,包含路由类型、执行形态和置信度
281
186
  */
187
+ function logRouteHit(decision) {
188
+ console.error(JSON.stringify({
189
+ sf_route_hit: true,
190
+ route: decision.route,
191
+ execution_shape: decision.execution_shape,
192
+ input_materials: decision.input_materials.map((m) => m.kind).join(",") || "none",
193
+ output_artifact: decision.output_artifact?.kind ?? "-",
194
+ prompt_template: selectPromptTemplate(decision),
195
+ confidence: decision.confidence,
196
+ }));
197
+ }
282
198
  export function routeIntent(input) {
199
+ const decision = routeIntentCore(input);
200
+ logRouteHit(decision);
201
+ return decision;
202
+ }
203
+ function routeIntentCore(input) {
283
204
  const { intent, task_id: taskId } = input;
284
205
  const normalized = intent.toLowerCase();
285
- console.error("[soloForge] 意图路由: 开始意图路由");
206
+ console.error("[soloForge] 意图路由: 开始意图路由 (v2 signal-based)");
207
+ // ── 阶段 1: 信号提取 ──
208
+ const signals = extractSignals(intent);
209
+ const materialResult = extractInputMaterials(intent);
210
+ const scoredResult = scoreRoute(intent, signals, materialResult, taskId);
211
+ // ── 阶段 2: 兼容层 — 将新模块结果映射回原有分层逻辑 ──
212
+ // 这确保所有现有测试用例的行为不变
213
+ const inputPaths = materialResult.materials.map((m) => ({
214
+ kind: m.kind,
215
+ path: m.path,
216
+ ingestion_status: m.ingestion_status,
217
+ }));
218
+ const hasInputPaths = inputPaths.length > 0;
286
219
  // 层 0: 跳过类
287
- for (const sp of SKIP_PATTERNS) {
288
- if (sp.pattern.test(intent)) {
289
- console.error(`[soloForge] 意图路由: 匹配跳过模式 (${sp.reason}),路由到 skip_chat`);
290
- return mk("skip_chat", "none", false, [], [], undefined, undefined, 0.95);
291
- }
292
- }
293
- if (GENERAL_QA_PATTERNS.some((p) => p.test(intent))) {
220
+ if (signals.is_skip) {
221
+ console.error(`[soloForge] 意图路由: 匹配跳过模式 (${signals.skip_reason}),路由到 skip_chat`);
222
+ const skipRejected = scoredResult.score_breakdown
223
+ .filter(s => s.route !== "skip_chat")
224
+ .slice(0, 3)
225
+ .map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
226
+ return {
227
+ ...mk("skip_chat", "none", false, [], [], undefined, undefined, 0.95),
228
+ skip_reason: signals.skip_reason,
229
+ evidence: ["skip:" + signals.skip_reason],
230
+ rejected_routes: skipRejected,
231
+ decision_version: 2,
232
+ };
233
+ }
234
+ if (signals.is_general_qa) {
294
235
  console.error("[soloForge] 意图路由: 匹配通用问答模式,路由到 direct_answer");
295
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9);
236
+ const qaRejected = scoredResult.score_breakdown
237
+ .filter(s => s.route !== "direct_answer")
238
+ .slice(0, 3)
239
+ .map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
240
+ return {
241
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9),
242
+ evidence: ["general_qa"],
243
+ rejected_routes: qaRejected,
244
+ decision_version: 2,
245
+ };
296
246
  }
297
247
  const artifactVerbPresent = hasArtifactVerb(intent);
298
248
  const hasArtifactSignal = artifactVerbPresent && hasArtifactNoun(intent);
299
249
  const artifact = detectArtifact(intent, taskId);
300
- const inputPaths = extractInputPaths(intent);
301
- const hasInputPaths = inputPaths.length > 0;
302
- // 源码提取信号 = archive/log 显式模式 OR 提取类动词 + 输入路径/源码
303
250
  const hasArchiveLogSignal = SOURCE_EXTRACTION_PATTERNS.some((s) => s.pattern.test(intent));
304
251
  const hasExtractionVerb = EXTRACTION_VERB_PATTERNS.some((p) => p.test(intent));
305
252
  const hasSourceExtraction = hasArchiveLogSignal
306
253
  || (hasExtractionVerb && (hasInputPaths || /源码/.test(intent)));
307
254
  const isFullPipeline = FULL_PIPELINE_SIGNALS.some((p) => p.test(intent));
308
- // 1: 源码/材料提取 (明确的外部材料提取 + 产物信号)
255
+ // ── 冲突仲裁:检查否定/约束信号 ──
256
+ const hasNoMutation = signals.negations.some((n) => n.negates === "mutation");
257
+ const hasNoPipeline = signals.negations.some((n) => n.negates === "pipeline");
258
+ // 层 1: 源码/材料提取
309
259
  if (hasSourceExtraction && (hasArtifactSignal || artifact)) {
310
260
  console.error("[soloForge] 意图路由: 路由到 source_extraction");
311
- const materials = inputPaths.length > 0
261
+ const materials = hasInputPaths
312
262
  ? inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: true, ingestion_status: p.ingestion_status }))
313
- : inferMissingInputs(intent, false).materials;
314
- const missing = inputPaths.length > 0
263
+ : inferMissingInputs(intent, false).materials.map((m) => ({ kind: m.kind, required: m.required }));
264
+ const missing = hasInputPaths
315
265
  ? []
316
266
  : inferMissingInputs(intent, false).missing;
317
- return mk("source_extraction", "single_artifact", false, materials, missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-提取产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8);
318
- }
319
- // 2: 规划 (必须在产物生成之前,"拆成N阶段"优先级高于产物信号)
267
+ const inferred = hasInputPaths ? undefined : inferMissingInputs(intent, false).materials;
268
+ const rejectedRoutes = scoredResult.score_breakdown
269
+ .filter(s => s.route !== "source_extraction")
270
+ .slice(0, 3)
271
+ .map(s => ({ route: s.route, reason: s.reasons.join("; "), score: s.score }));
272
+ return {
273
+ ...mk("source_extraction", "single_artifact", false, materials, missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-提取产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8),
274
+ inferred_materials: inferred,
275
+ needs_clarification: missing.length > 0,
276
+ route_reason: scoredResult.route_reason,
277
+ signal_evidence: scoredResult.signal_evidence,
278
+ evidence: scoredResult.signal_evidence,
279
+ rejected_routes: rejectedRoutes,
280
+ decision_version: 2,
281
+ };
282
+ }
283
+ // 层 2: 规划
320
284
  if (PLANNING_PATTERNS.some((p) => p.test(normalized)) || isFullPipeline) {
321
285
  console.error("[soloForge] 意图路由: 路由到 planning");
322
- if (isFullPipeline) {
323
- return mk("planning", "multi_stage_plan", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], artifact, inferPipelineHint(intent, artifact?.kind), 0.75);
286
+ if (isFullPipeline && !hasNoPipeline) {
287
+ return {
288
+ ...mk("planning", "multi_stage_plan", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], artifact, inferPipelineHint(intent, artifact?.kind), 0.75),
289
+ route_reason: scoredResult.route_reason,
290
+ signal_evidence: scoredResult.signal_evidence,
291
+ decision_version: 2,
292
+ };
324
293
  }
325
- return mk("planning", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], { kind: "task_breakdown", path: `.soloforge/output/${taskId ?? "01"}/01-任务拆解.md`, taskId }, undefined, 0.8);
326
- }
327
- // 层 3: 操作 (必须在产物生成之前,"发布前检查"优先级高于"清单"产物)
294
+ return {
295
+ ...mk("planning", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], { kind: "task_breakdown", path: `.soloforge/output/${taskId ?? "01"}/01-任务拆解.md`, taskId }, undefined, 0.8),
296
+ route_reason: scoredResult.route_reason,
297
+ signal_evidence: scoredResult.signal_evidence,
298
+ decision_version: 2,
299
+ };
300
+ }
301
+ // 层 3: 操作
328
302
  if (OPERATION_PATTERNS.some((p) => p.test(normalized))) {
329
303
  console.error("[soloForge] 意图路由: 路由到 operation");
330
- return mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8);
304
+ return {
305
+ ...mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8),
306
+ route_reason: scoredResult.route_reason,
307
+ decision_version: 2,
308
+ };
331
309
  }
332
- // 层 3.5: 分析 (在产物生成之前,当有分析动词但无产物动词时优先走分析)
310
+ // 层 3.5: 分析
333
311
  if (ANALYSIS_PATTERNS.some((p) => p.test(normalized)) && !artifactVerbPresent) {
334
312
  console.error("[soloForge] 意图路由: 路由到 analysis(分析优先)");
335
- return mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8);
313
+ return {
314
+ ...mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8),
315
+ route_reason: scoredResult.route_reason,
316
+ decision_version: 2,
317
+ };
336
318
  }
337
- // 层 4: 产物生成 (必须有产物动词 + 产物名词,或产物动词 + artifact 且非解释型)
319
+ // 层 4: 产物生成
338
320
  if (hasArtifactSignal || (artifactVerbPresent && artifact && !PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent)))) {
339
321
  console.error("[soloForge] 意图路由: 路由到 artifact_generation");
340
322
  const missing = [];
@@ -343,71 +325,99 @@ export function routeIntent(input) {
343
325
  if (refMatch)
344
326
  missing.push(`请提供 ${refMatch[1]} 文件路径或内容`);
345
327
  }
346
- return mk("artifact_generation", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8);
328
+ return {
329
+ ...mk("artifact_generation", "single_artifact", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), missing, artifact ?? { kind: "custom", path: `.soloforge/output/${taskId ?? "01"}/01-产物.md`, taskId }, inferPipelineHint(intent, artifact?.kind), 0.8),
330
+ needs_clarification: missing.length > 0,
331
+ route_reason: scoredResult.route_reason,
332
+ signal_evidence: scoredResult.signal_evidence,
333
+ decision_version: 2,
334
+ };
347
335
  }
348
336
  // 层 5: 代码修改
349
337
  if (CODE_CHANGE_PATTERNS.some((p) => p.test(normalized))) {
350
338
  console.error("[soloForge] 意图路由: 路由到 code_change");
351
- return mk("code_change", "code_execution", true, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85);
339
+ return {
340
+ ...mk("code_change", "code_execution", true, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85),
341
+ route_reason: scoredResult.route_reason,
342
+ decision_version: 2,
343
+ };
352
344
  }
353
345
  // 层 6: 审查
354
346
  if (REVIEW_PATTERNS.some((p) => p.test(normalized))) {
355
347
  console.error("[soloForge] 意图路由: 路由到 review");
356
- return mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85);
348
+ return {
349
+ ...mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.85),
350
+ route_reason: scoredResult.route_reason,
351
+ decision_version: 2,
352
+ };
357
353
  }
358
- // 层 7: 分析 (通用分析检查)
354
+ // 层 7: 通用分析
359
355
  if (ANALYSIS_PATTERNS.some((p) => p.test(normalized))) {
360
356
  console.error("[soloForge] 意图路由: 路由到 analysis(通用)");
361
- return mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8);
357
+ return {
358
+ ...mk("analysis", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.8),
359
+ route_reason: scoredResult.route_reason,
360
+ decision_version: 2,
361
+ };
362
362
  }
363
363
  // 层 8: 灰区
364
364
  if (/整理(?:一下|下)?/.test(intent)) {
365
365
  if (!hasArtifactNoun(intent)) {
366
- return mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6);
366
+ return {
367
+ ...mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6),
368
+ route_reason: "gray_zone:organize",
369
+ decision_version: 2,
370
+ };
367
371
  }
368
372
  }
369
373
  // 层 9: 纯解释
370
374
  if (PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent))) {
371
375
  if (/(?:这个|这|项目里|项目中的|当前|现有)/.test(intent)) {
372
- return mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7);
376
+ return {
377
+ ...mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7),
378
+ route_reason: "pure_explain+context",
379
+ decision_version: 2,
380
+ };
373
381
  }
374
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85);
382
+ return {
383
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85),
384
+ route_reason: "pure_explain",
385
+ decision_version: 2,
386
+ };
375
387
  }
376
388
  // 层 10: "看看" 灰区
377
389
  if (/看看|看下/.test(intent)) {
378
390
  if (/(?:风险|问题|有没有)/.test(intent)) {
379
- return mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.7);
391
+ return {
392
+ ...mk("review", "read_only_analysis", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.7),
393
+ route_reason: "gray:review",
394
+ decision_version: 2,
395
+ };
380
396
  }
381
- return mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6);
397
+ return {
398
+ ...mk("direct_answer", "none", false, inputPaths.map((p) => ({ kind: p.kind, path: p.path, required: false, ingestion_status: p.ingestion_status })), [], undefined, undefined, 0.6),
399
+ route_reason: "gray:look",
400
+ decision_version: 2,
401
+ };
382
402
  }
383
403
  // 兜底
384
404
  console.error("[soloForge] 意图路由: 未匹配任何规则,兜底路由到 direct_answer");
385
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3);
405
+ return {
406
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3),
407
+ decision_version: 2,
408
+ };
386
409
  }
387
- /**
388
- * 构造路由决策对象。
389
- * @param route - 路由类型
390
- * @param execution_shape - 执行形态
391
- * @param mutation_allowed - 是否允许变更
392
- * @param input_materials - 输入材料列表
393
- * @param missing - 缺失输入列表
394
- * @param output_artifact - 输出产物
395
- * @param pipeline_hint - 流水线提示
396
- * @param confidence - 置信度
397
- * @returns 意图路由决策
398
- */
410
+ // ── 构造函数 ──
399
411
  function mk(route, execution_shape, mutation_allowed, input_materials, missing, output_artifact, pipeline_hint, confidence) {
400
412
  return {
401
413
  route, execution_shape, mutation_allowed, input_materials,
402
414
  output_artifact, pipeline_hint, missing_required_inputs: missing, confidence,
415
+ scope: { read_only: !mutation_allowed, description: route + " scope" },
416
+ constraints: mutation_allowed ? [] : ["no_mutation"],
417
+ language_policy: { primary: "zh-CN", fallback: "en" },
403
418
  };
404
419
  }
405
- /**
406
- * 推断流水线提示 — 根据意图文本和产物类型推断适用的流水线阶段。
407
- * @param intent - 用户意图文本
408
- * @param artifactKind - 可选的产物类型
409
- * @returns 流水线提示字符串,无法推断时返回 undefined
410
- */
420
+ // ── 流水线提示 ──
411
421
  function inferPipelineHint(intent, artifactKind) {
412
422
  if (/源码.*原型|原型.*源码|源码包.*原型|prototype/i.test(intent)) {
413
423
  return "source-prototype-to-delivery:step-1";
@@ -423,22 +433,11 @@ function inferPipelineHint(intent, artifactKind) {
423
433
  default: return undefined;
424
434
  }
425
435
  }
426
- /**
427
- * 判断是否应进入 SoloForge 流程。
428
- * skip_chat 和 direct_answer 路由不进入 SoloForge。
429
- *
430
- * @param decision - 意图路由决策
431
- * @returns 是否应进入 SoloForge
432
- */
436
+ // ── 外部 API(保持不变) ──
433
437
  export function shouldEnterSoloForge(decision) {
434
438
  console.error("[soloForge] 意图路由: 判断是否进入 SoloForge,当前路由: " + decision.route);
435
439
  return decision.route !== "skip_chat" && decision.route !== "direct_answer";
436
440
  }
437
- /**
438
- * 根据路由决策选择对应的 Prompt 模板。
439
- * @param decision - 意图路由决策
440
- * @returns Prompt 模板名称
441
- */
442
441
  export function selectPromptTemplate(decision) {
443
442
  console.error("[soloForge] 意图路由: 选择 Prompt 模板,路由: " + decision.route + ", 执行形态: " + decision.execution_shape);
444
443
  if (decision.route === "source_extraction")
@@ -455,4 +454,24 @@ export function selectPromptTemplate(decision) {
455
454
  default: return "code_execution_prompt";
456
455
  }
457
456
  }
457
+ export function debugRouteIntent(input) {
458
+ const { intent, task_id: taskId } = input;
459
+ const materialResult = extractInputMaterials(intent);
460
+ const signals = extractSignals(intent);
461
+ const scored = scoreRoute(intent, signals, materialResult, taskId);
462
+ const decision = routeIntent(input);
463
+ const whyNotSkip = signals.is_skip
464
+ ? `skip detected: ${signals.skip_reason}`
465
+ : signals.is_general_qa
466
+ ? "general QA pattern"
467
+ : "has SoloForge-relevant signals";
468
+ return {
469
+ intent,
470
+ extracted_materials: materialResult,
471
+ signals,
472
+ scored,
473
+ final_decision: decision,
474
+ why_not_skip: whyNotSkip,
475
+ };
476
+ }
458
477
  //# sourceMappingURL=intent_router.js.map