soloforge 1.2.10 → 1.2.11

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.
@@ -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,124 @@ 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
+ return {
223
+ ...mk("skip_chat", "none", false, [], [], undefined, undefined, 0.95),
224
+ skip_reason: signals.skip_reason,
225
+ decision_version: 2,
226
+ };
227
+ }
228
+ if (signals.is_general_qa) {
294
229
  console.error("[soloForge] 意图路由: 匹配通用问答模式,路由到 direct_answer");
295
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9);
230
+ return {
231
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.9),
232
+ decision_version: 2,
233
+ };
296
234
  }
297
235
  const artifactVerbPresent = hasArtifactVerb(intent);
298
236
  const hasArtifactSignal = artifactVerbPresent && hasArtifactNoun(intent);
299
237
  const artifact = detectArtifact(intent, taskId);
300
- const inputPaths = extractInputPaths(intent);
301
- const hasInputPaths = inputPaths.length > 0;
302
- // 源码提取信号 = archive/log 显式模式 OR 提取类动词 + 输入路径/源码
303
238
  const hasArchiveLogSignal = SOURCE_EXTRACTION_PATTERNS.some((s) => s.pattern.test(intent));
304
239
  const hasExtractionVerb = EXTRACTION_VERB_PATTERNS.some((p) => p.test(intent));
305
240
  const hasSourceExtraction = hasArchiveLogSignal
306
241
  || (hasExtractionVerb && (hasInputPaths || /源码/.test(intent)));
307
242
  const isFullPipeline = FULL_PIPELINE_SIGNALS.some((p) => p.test(intent));
308
- // 1: 源码/材料提取 (明确的外部材料提取 + 产物信号)
243
+ // ── 冲突仲裁:检查否定/约束信号 ──
244
+ const hasNoMutation = signals.negations.some((n) => n.negates === "mutation");
245
+ const hasNoPipeline = signals.negations.some((n) => n.negates === "pipeline");
246
+ // 层 1: 源码/材料提取
309
247
  if (hasSourceExtraction && (hasArtifactSignal || artifact)) {
310
248
  console.error("[soloForge] 意图路由: 路由到 source_extraction");
311
- const materials = inputPaths.length > 0
249
+ const materials = hasInputPaths
312
250
  ? 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
251
+ : inferMissingInputs(intent, false).materials.map((m) => ({ kind: m.kind, required: m.required }));
252
+ const missing = hasInputPaths
315
253
  ? []
316
254
  : 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阶段"优先级高于产物信号)
255
+ return {
256
+ ...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),
257
+ needs_clarification: missing.length > 0,
258
+ route_reason: scoredResult.route_reason,
259
+ signal_evidence: scoredResult.signal_evidence,
260
+ decision_version: 2,
261
+ };
262
+ }
263
+ // 层 2: 规划
320
264
  if (PLANNING_PATTERNS.some((p) => p.test(normalized)) || isFullPipeline) {
321
265
  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);
266
+ if (isFullPipeline && !hasNoPipeline) {
267
+ return {
268
+ ...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),
269
+ route_reason: scoredResult.route_reason,
270
+ signal_evidence: scoredResult.signal_evidence,
271
+ decision_version: 2,
272
+ };
324
273
  }
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: 操作 (必须在产物生成之前,"发布前检查"优先级高于"清单"产物)
274
+ return {
275
+ ...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),
276
+ route_reason: scoredResult.route_reason,
277
+ signal_evidence: scoredResult.signal_evidence,
278
+ decision_version: 2,
279
+ };
280
+ }
281
+ // 层 3: 操作
328
282
  if (OPERATION_PATTERNS.some((p) => p.test(normalized))) {
329
283
  console.error("[soloForge] 意图路由: 路由到 operation");
330
- return mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8);
284
+ return {
285
+ ...mk("operation", "verification_only", false, [], [], undefined, undefined, 0.8),
286
+ route_reason: scoredResult.route_reason,
287
+ decision_version: 2,
288
+ };
331
289
  }
332
- // 层 3.5: 分析 (在产物生成之前,当有分析动词但无产物动词时优先走分析)
290
+ // 层 3.5: 分析
333
291
  if (ANALYSIS_PATTERNS.some((p) => p.test(normalized)) && !artifactVerbPresent) {
334
292
  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);
293
+ return {
294
+ ...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),
295
+ route_reason: scoredResult.route_reason,
296
+ decision_version: 2,
297
+ };
336
298
  }
337
- // 层 4: 产物生成 (必须有产物动词 + 产物名词,或产物动词 + artifact 且非解释型)
299
+ // 层 4: 产物生成
338
300
  if (hasArtifactSignal || (artifactVerbPresent && artifact && !PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent)))) {
339
301
  console.error("[soloForge] 意图路由: 路由到 artifact_generation");
340
302
  const missing = [];
@@ -343,71 +305,96 @@ export function routeIntent(input) {
343
305
  if (refMatch)
344
306
  missing.push(`请提供 ${refMatch[1]} 文件路径或内容`);
345
307
  }
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);
308
+ return {
309
+ ...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),
310
+ needs_clarification: missing.length > 0,
311
+ route_reason: scoredResult.route_reason,
312
+ signal_evidence: scoredResult.signal_evidence,
313
+ decision_version: 2,
314
+ };
347
315
  }
348
316
  // 层 5: 代码修改
349
317
  if (CODE_CHANGE_PATTERNS.some((p) => p.test(normalized))) {
350
318
  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);
319
+ return {
320
+ ...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),
321
+ route_reason: scoredResult.route_reason,
322
+ decision_version: 2,
323
+ };
352
324
  }
353
325
  // 层 6: 审查
354
326
  if (REVIEW_PATTERNS.some((p) => p.test(normalized))) {
355
327
  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);
328
+ return {
329
+ ...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),
330
+ route_reason: scoredResult.route_reason,
331
+ decision_version: 2,
332
+ };
357
333
  }
358
- // 层 7: 分析 (通用分析检查)
334
+ // 层 7: 通用分析
359
335
  if (ANALYSIS_PATTERNS.some((p) => p.test(normalized))) {
360
336
  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);
337
+ return {
338
+ ...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),
339
+ route_reason: scoredResult.route_reason,
340
+ decision_version: 2,
341
+ };
362
342
  }
363
343
  // 层 8: 灰区
364
344
  if (/整理(?:一下|下)?/.test(intent)) {
365
345
  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);
346
+ return {
347
+ ...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),
348
+ route_reason: "gray_zone:organize",
349
+ decision_version: 2,
350
+ };
367
351
  }
368
352
  }
369
353
  // 层 9: 纯解释
370
354
  if (PURE_EXPLAIN_PATTERNS.some((p) => p.test(intent))) {
371
355
  if (/(?:这个|这|项目里|项目中的|当前|现有)/.test(intent)) {
372
- return mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7);
356
+ return {
357
+ ...mk("analysis", "read_only_analysis", false, [], [], undefined, undefined, 0.7),
358
+ route_reason: "pure_explain+context",
359
+ decision_version: 2,
360
+ };
373
361
  }
374
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85);
362
+ return {
363
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.85),
364
+ route_reason: "pure_explain",
365
+ decision_version: 2,
366
+ };
375
367
  }
376
368
  // 层 10: "看看" 灰区
377
369
  if (/看看|看下/.test(intent)) {
378
370
  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);
371
+ return {
372
+ ...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),
373
+ route_reason: "gray:review",
374
+ decision_version: 2,
375
+ };
380
376
  }
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);
377
+ return {
378
+ ...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),
379
+ route_reason: "gray:look",
380
+ decision_version: 2,
381
+ };
382
382
  }
383
383
  // 兜底
384
384
  console.error("[soloForge] 意图路由: 未匹配任何规则,兜底路由到 direct_answer");
385
- return mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3);
385
+ return {
386
+ ...mk("direct_answer", "none", false, [], [], undefined, undefined, 0.3),
387
+ decision_version: 2,
388
+ };
386
389
  }
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
- */
390
+ // ── 构造函数 ──
399
391
  function mk(route, execution_shape, mutation_allowed, input_materials, missing, output_artifact, pipeline_hint, confidence) {
400
392
  return {
401
393
  route, execution_shape, mutation_allowed, input_materials,
402
394
  output_artifact, pipeline_hint, missing_required_inputs: missing, confidence,
403
395
  };
404
396
  }
405
- /**
406
- * 推断流水线提示 — 根据意图文本和产物类型推断适用的流水线阶段。
407
- * @param intent - 用户意图文本
408
- * @param artifactKind - 可选的产物类型
409
- * @returns 流水线提示字符串,无法推断时返回 undefined
410
- */
397
+ // ── 流水线提示 ──
411
398
  function inferPipelineHint(intent, artifactKind) {
412
399
  if (/源码.*原型|原型.*源码|源码包.*原型|prototype/i.test(intent)) {
413
400
  return "source-prototype-to-delivery:step-1";
@@ -423,22 +410,11 @@ function inferPipelineHint(intent, artifactKind) {
423
410
  default: return undefined;
424
411
  }
425
412
  }
426
- /**
427
- * 判断是否应进入 SoloForge 流程。
428
- * skip_chat 和 direct_answer 路由不进入 SoloForge。
429
- *
430
- * @param decision - 意图路由决策
431
- * @returns 是否应进入 SoloForge
432
- */
413
+ // ── 外部 API(保持不变) ──
433
414
  export function shouldEnterSoloForge(decision) {
434
415
  console.error("[soloForge] 意图路由: 判断是否进入 SoloForge,当前路由: " + decision.route);
435
416
  return decision.route !== "skip_chat" && decision.route !== "direct_answer";
436
417
  }
437
- /**
438
- * 根据路由决策选择对应的 Prompt 模板。
439
- * @param decision - 意图路由决策
440
- * @returns Prompt 模板名称
441
- */
442
418
  export function selectPromptTemplate(decision) {
443
419
  console.error("[soloForge] 意图路由: 选择 Prompt 模板,路由: " + decision.route + ", 执行形态: " + decision.execution_shape);
444
420
  if (decision.route === "source_extraction")
@@ -455,4 +431,24 @@ export function selectPromptTemplate(decision) {
455
431
  default: return "code_execution_prompt";
456
432
  }
457
433
  }
434
+ export function debugRouteIntent(input) {
435
+ const { intent, task_id: taskId } = input;
436
+ const materialResult = extractInputMaterials(intent);
437
+ const signals = extractSignals(intent);
438
+ const scored = scoreRoute(intent, signals, materialResult, taskId);
439
+ const decision = routeIntent(input);
440
+ const whyNotSkip = signals.is_skip
441
+ ? `skip detected: ${signals.skip_reason}`
442
+ : signals.is_general_qa
443
+ ? "general QA pattern"
444
+ : "has SoloForge-relevant signals";
445
+ return {
446
+ intent,
447
+ extracted_materials: materialResult,
448
+ signals,
449
+ scored,
450
+ final_decision: decision,
451
+ why_not_skip: whyNotSkip,
452
+ };
453
+ }
458
454
  //# sourceMappingURL=intent_router.js.map