@gordon.gan/specflow 1.8.0-alpha → 1.8.1-beta
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/README.md +5 -3
- package/dist/cli/commands/document-run.d.ts +14 -1
- package/dist/cli/commands/document-run.js +518 -212
- package/dist/core/document/chapters.js +18 -3
- package/dist/core/document/digests.d.ts +0 -1
- package/dist/core/document/digests.js +18 -10
- package/dist/core/document/engine.d.ts +9 -0
- package/dist/core/document/engine.js +273 -38
- package/dist/core/document/extract.d.ts +61 -0
- package/dist/core/document/extract.js +437 -0
- package/dist/core/document/forbidden-patterns.js +7 -4
- package/dist/core/document/gates.d.ts +13 -1
- package/dist/core/document/gates.js +48 -6
- package/dist/core/document/input-digest.d.ts +8 -0
- package/dist/core/document/input-digest.js +93 -14
- package/dist/core/document/input-features.d.ts +23 -4
- package/dist/core/document/input-features.js +51 -2
- package/dist/core/document/lint.d.ts +17 -0
- package/dist/core/document/lint.js +118 -0
- package/dist/core/document/llm.d.ts +3 -10
- package/dist/core/document/llm.js +3 -8
- package/dist/core/document/map.d.ts +6 -0
- package/dist/core/document/map.js +114 -30
- package/dist/core/document/outline.d.ts +3 -0
- package/dist/core/document/outline.js +52 -9
- package/dist/core/document/paths.d.ts +5 -5
- package/dist/core/document/paths.js +11 -6
- package/dist/core/document/profile-validator.js +33 -5
- package/dist/core/document/profiles.js +5 -0
- package/dist/core/document/render.d.ts +26 -0
- package/dist/core/document/render.js +110 -14
- package/dist/core/document/review.d.ts +19 -4
- package/dist/core/document/review.js +65 -20
- package/dist/core/document/scene-detect.d.ts +10 -3
- package/dist/core/document/scene-detect.js +138 -22
- package/dist/core/document/schemas.d.ts +188 -34
- package/dist/core/document/schemas.js +39 -26
- package/dist/integrations/shared/capability-evidence.js +4 -4
- package/dist/integrations/shared/command-catalog.js +2 -1
- package/dist/integrations/shared/parity-manifest.js +4 -4
- package/package.json +2 -1
- package/prompts/document/map/acceptance.md +1 -0
- package/prompts/document/map/anti-ai.md +29 -0
- package/prompts/document/map/api-design.md +13 -4
- package/prompts/document/map/architecture.md +21 -1
- package/prompts/document/map/benchmark.md +26 -0
- package/prompts/document/map/closed-loop.md +1 -0
- package/prompts/document/map/compat-migration.md +24 -1
- package/prompts/document/map/component-design.md +30 -0
- package/prompts/document/map/config-runtime.md +1 -0
- package/prompts/document/map/core-flow.md +62 -0
- package/prompts/document/map/core-logic.md +1 -0
- package/prompts/document/map/data-model.md +1 -0
- package/prompts/document/map/deploy.md +20 -2
- package/prompts/document/map/fix.md +1 -0
- package/prompts/document/map/frontend-architecture.md +35 -0
- package/prompts/document/map/goal.md +1 -0
- package/prompts/document/map/impact.md +1 -0
- package/prompts/document/map/implementability.md +1 -0
- package/prompts/document/map/migration-guide.md +36 -0
- package/prompts/document/map/mvp-boundary.md +1 -0
- package/prompts/document/map/non-goals.md +1 -0
- package/prompts/document/map/ops.md +33 -0
- package/prompts/document/map/performance.md +32 -0
- package/prompts/document/map/poc-demo.md +25 -0
- package/prompts/document/map/regression.md +1 -0
- package/prompts/document/map/reproduce.md +1 -0
- package/prompts/document/map/requirement.md +1 -0
- package/prompts/document/map/research.md +25 -0
- package/prompts/document/map/root-cause.md +1 -0
- package/prompts/document/map/signoff.md +1 -0
- package/prompts/document/map/state-management.md +23 -0
- package/prompts/document/map/tech-selection.md +13 -1
- package/prompts/document/map/test-strategy.md +18 -1
- package/prompts/document/map/ui-design.md +10 -7
- package/prompts/document/outline/general.md +9 -0
- package/prompts/document/review/ai-review.md +2 -1
- package/prompts/document/shared/grounding.md +84 -0
- package/skills/specflow-techdoc/SKILL.md +143 -0
- package/skills/specflow-techdoc-synth/SKILL.md +99 -0
- package/templates/document/chapters/api-design.yaml +6 -1
- package/templates/document/chapters/architecture.yaml +7 -4
- package/templates/document/chapters/benchmark.yaml +20 -0
- package/templates/document/chapters/compat-migration.yaml +7 -3
- package/templates/document/chapters/component-design.yaml +22 -0
- package/templates/document/chapters/core-flow.yaml +27 -0
- package/templates/document/chapters/core-logic.yaml +1 -1
- package/templates/document/chapters/deploy.yaml +11 -7
- package/templates/document/chapters/frontend-architecture.yaml +22 -0
- package/templates/document/chapters/migration-guide.yaml +21 -0
- package/templates/document/chapters/ops.yaml +25 -0
- package/templates/document/chapters/performance.yaml +21 -0
- package/templates/document/chapters/poc-demo.yaml +22 -0
- package/templates/document/chapters/research.yaml +22 -0
- package/templates/document/chapters/state-management.yaml +22 -0
- package/templates/document/chapters/tech-selection.yaml +6 -3
- package/templates/document/chapters/test-strategy.yaml +5 -2
- package/templates/document/chapters/ui-design.yaml +7 -1
- package/templates/document/profiles/0to1.yaml +41 -9
- package/templates/document/profiles/bugfix.yaml +8 -3
- package/templates/document/profiles/feature.yaml +19 -7
- package/templates/document/profiles/frontend-0to1.yaml +47 -0
- package/templates/document/profiles/migration.yaml +42 -0
- package/templates/document/profiles/poc.yaml +46 -0
- package/dist/core/document/index.d.ts +0 -7
- package/dist/core/document/index.js +0 -7
- package/skills/specflow-document/SKILL.md +0 -124
|
@@ -6,61 +6,103 @@
|
|
|
6
6
|
* Optional HTML render reuses `marked`.
|
|
7
7
|
*/
|
|
8
8
|
import { marked } from 'marked';
|
|
9
|
+
// ============= Escaping helpers(P1-5/6:HTML 与 markdown 注入面) =============
|
|
10
|
+
/** HTML 转义:用于 <title>/<h1> 插值(防存储型 XSS)。 */
|
|
11
|
+
export function escapeHtml(s) {
|
|
12
|
+
return String(s)
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>')
|
|
16
|
+
.replace(/"/g, '"')
|
|
17
|
+
.replace(/'/g, ''');
|
|
18
|
+
}
|
|
19
|
+
/** Markdown 表格单元格转义:转义 | 与换行(防破坏列对齐/拆行)。 */
|
|
20
|
+
export function escapeTableCell(s) {
|
|
21
|
+
return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
|
|
22
|
+
}
|
|
23
|
+
/** 代码围栏内容转义:把可能提前终止 ``` 围栏的行替换为安全形式。 */
|
|
24
|
+
export function escapeFenceContent(s) {
|
|
25
|
+
return String(s).split('\n').map((line) => (line.trimStart().startsWith('```') ? `\\\`\\\`\\\`${line.slice(3)}` : line)).join('\n');
|
|
26
|
+
}
|
|
27
|
+
/** 单行内联文本:转义换行,避免注入新标题/新行。 */
|
|
28
|
+
export function escapeInline(s) {
|
|
29
|
+
return String(s).replace(/\r?\n/g, ' ');
|
|
30
|
+
}
|
|
9
31
|
// ============= Contract entity → Markdown (T9.1) =============
|
|
10
32
|
export function renderInterface(i) {
|
|
11
33
|
const rows = [
|
|
12
|
-
`##### ${i.id} · ${i.name}(${i.method} ${i.path})`,
|
|
34
|
+
`##### ${i.id} · ${escapeInline(i.name)}(${i.method} ${i.path})`,
|
|
13
35
|
'',
|
|
14
36
|
'**字段表**',
|
|
15
37
|
'',
|
|
16
38
|
'| 字段 | 类型 | 必填 | 说明 |',
|
|
17
39
|
'|------|------|------|------|',
|
|
18
|
-
...[...i.request_fields ?? [], ...i.response_fields ?? []].map((f) => `| ${f.name} | ${f.type} | ${f.required ? '是' : '否'} | ${f.desc ?? ''} |`),
|
|
40
|
+
...[...i.request_fields ?? [], ...i.response_fields ?? []].map((f) => `| ${escapeTableCell(f.name)} | ${escapeTableCell(f.type)} | ${f.required ? '是' : '否'} | ${escapeTableCell(f.desc ?? '')} |`),
|
|
19
41
|
'',
|
|
20
42
|
'**成功示例**',
|
|
21
43
|
'',
|
|
22
44
|
'```http',
|
|
23
|
-
`${i.method} ${i.path}`,
|
|
45
|
+
`${i.method} ${escapeFenceContent(i.path)}`,
|
|
24
46
|
'```',
|
|
25
47
|
'',
|
|
26
48
|
'**失败示例**',
|
|
27
49
|
'',
|
|
28
|
-
...i.failure_examples.map((ex) => `- ${ex}`),
|
|
50
|
+
...i.failure_examples.map((ex) => `- ${escapeInline(ex)}`),
|
|
29
51
|
'',
|
|
30
52
|
'**错误**',
|
|
31
53
|
'',
|
|
32
54
|
'| 条件 | 状态 | 说明 |',
|
|
33
55
|
'|------|------|------|',
|
|
34
|
-
...i.errors.map((e) => `| ${e.condition} | ${e.status} | ${e.note ?? ''} |`),
|
|
56
|
+
...i.errors.map((e) => `| ${escapeTableCell(e.condition)} | ${escapeTableCell(e.status)} | ${escapeTableCell(e.note ?? '')} |`),
|
|
35
57
|
];
|
|
36
58
|
return rows.join('\n');
|
|
37
59
|
}
|
|
38
60
|
export function renderTable(t) {
|
|
39
61
|
return [
|
|
40
|
-
`##### ${t.id} · ${t.name}`,
|
|
62
|
+
`##### ${t.id} · ${escapeInline(t.name)}`,
|
|
41
63
|
'',
|
|
42
64
|
'```sql',
|
|
43
|
-
t.ddl,
|
|
65
|
+
escapeFenceContent(t.ddl),
|
|
44
66
|
'```',
|
|
45
67
|
'',
|
|
46
68
|
...(t.migration ? ['**存量填充策略**', '', t.migration, ''] : []),
|
|
47
69
|
...(t.rollback ? ['**回滚兼容**', '', t.rollback, ''] : []),
|
|
48
70
|
].join('\n');
|
|
49
71
|
}
|
|
72
|
+
export function renderDecisions(decisions) {
|
|
73
|
+
if (decisions.length === 0)
|
|
74
|
+
return '';
|
|
75
|
+
return [
|
|
76
|
+
'**决策记录**',
|
|
77
|
+
'',
|
|
78
|
+
'| 决策 | 结论 |',
|
|
79
|
+
'|------|------|',
|
|
80
|
+
...decisions.map((d) => `| ${escapeTableCell(d.id)} | ${escapeTableCell(d.text)} |`),
|
|
81
|
+
].join('\n');
|
|
82
|
+
}
|
|
50
83
|
export function renderEntities(entities) {
|
|
51
84
|
const parts = [];
|
|
52
85
|
for (const i of entities.interfaces)
|
|
53
86
|
parts.push(renderInterface(i));
|
|
54
87
|
for (const t of entities.tables)
|
|
55
88
|
parts.push(renderTable(t));
|
|
89
|
+
const decisionsMd = renderDecisions(entities.decisions);
|
|
90
|
+
if (decisionsMd)
|
|
91
|
+
parts.push(decisionsMd);
|
|
56
92
|
return parts.join('\n\n');
|
|
57
93
|
}
|
|
94
|
+
/** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
|
|
95
|
+
* stays in `chapters/<id>.md` and `review-result.json`; the deliverable document must not show
|
|
96
|
+
* internal fix notes. */
|
|
97
|
+
export function stripReviewNotes(text) {
|
|
98
|
+
return text.replace(/<!--\s*review-fix[\s\S]*?-->/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
99
|
+
}
|
|
58
100
|
export function renderDocument(input) {
|
|
59
|
-
const { outline, entities, narratives } = input;
|
|
60
|
-
const body = [`# 方案文档:${outline.profile}`, ''];
|
|
101
|
+
const { outline, entities, narratives, profile, appendixAntiAI } = input;
|
|
102
|
+
const body = [`# 方案文档:${escapeInline(outline.profile)}`, ''];
|
|
61
103
|
for (const ch of outline.chapters) {
|
|
62
|
-
body.push(`## ${ch.title}`, '');
|
|
63
|
-
const narrative = narratives.get(ch.id) ?? '';
|
|
104
|
+
body.push(`## ${escapeInline(ch.title)}`, '');
|
|
105
|
+
const narrative = stripReviewNotes(narratives.get(ch.id) ?? '');
|
|
64
106
|
if (narrative.trim())
|
|
65
107
|
body.push(narrative.trim(), '');
|
|
66
108
|
}
|
|
@@ -75,11 +117,64 @@ export function renderDocument(input) {
|
|
|
75
117
|
body.push('| 章节 | 数据来源 | 处理方式 |');
|
|
76
118
|
body.push('|------|---------|---------|');
|
|
77
119
|
for (const ch of outline.chapters) {
|
|
78
|
-
body.push(`| ${ch.title} | 输入素材 / 项目规约 / 大纲要点 | LLM 生成 + 引擎校验 |`);
|
|
120
|
+
body.push(`| ${escapeTableCell(ch.title)} | 输入素材 / 项目规约 / 大纲要点 | LLM 生成 + 引擎校验 |`);
|
|
79
121
|
}
|
|
80
122
|
body.push('', '');
|
|
123
|
+
// 附录 B/C: rendered from profile.appendices (decision: deterministic templates, §7.3).
|
|
124
|
+
const appendices = profile?.appendices ?? [];
|
|
125
|
+
if (appendices.includes('anti-ai')) {
|
|
126
|
+
body.push('---', '', renderAppendixAntiAI(appendixAntiAI), '');
|
|
127
|
+
}
|
|
128
|
+
if (appendices.includes('handoff')) {
|
|
129
|
+
body.push('---', '', renderAppendixHandoff(), '');
|
|
130
|
+
}
|
|
81
131
|
return body.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
82
132
|
}
|
|
133
|
+
// ============= 附录 B/C(§7.3 确定性渲染) =============
|
|
134
|
+
/** 附录 B · 去AI味自检(frontend-dev-guide §十一 → README §1.6 五维表)。 */
|
|
135
|
+
export function renderAppendixAntiAI(content) {
|
|
136
|
+
const items = (content ?? '').trim();
|
|
137
|
+
const table = [
|
|
138
|
+
'## 附录 B: 去AI味自检',
|
|
139
|
+
'',
|
|
140
|
+
'> 每份交付末尾必须附「去AI味自检」,至少 3 条;缺一视为交付不完整(README §1.6)。',
|
|
141
|
+
'',
|
|
142
|
+
'| 维度 | 自检问题 | AI 味反例 |',
|
|
143
|
+
'|------|---------|----------|',
|
|
144
|
+
'| 不做决策 | 明确说了「不做什么」? | 只列功能清单 |',
|
|
145
|
+
'| 数字具体 | 指标/约束具体到数字? | 「提升体验」 |',
|
|
146
|
+
'| 竞品立场 | 与竞品差异有一句话立场? | 「借鉴头部最佳实践」 |',
|
|
147
|
+
'| 此刻合理性 | 为什么现在做?不做会怎样? | 不解释时机 |',
|
|
148
|
+
'| 优先级倾斜 | P0/P1 有倾斜逻辑? | 全功能同等权重 |',
|
|
149
|
+
'',
|
|
150
|
+
items
|
|
151
|
+
? items
|
|
152
|
+
: '> (自检条目由生成流程填充,至少 3 条;缺失将触发引擎 warning)',
|
|
153
|
+
'',
|
|
154
|
+
];
|
|
155
|
+
return table.join('\n');
|
|
156
|
+
}
|
|
157
|
+
/** 附录 C · 下游交付衔接(frontend-dev-guide §八 → README §1.5 矩阵 + 三步)。 */
|
|
158
|
+
export function renderAppendixHandoff() {
|
|
159
|
+
const matrix = [
|
|
160
|
+
'## 附录 C: 下游交付衔接',
|
|
161
|
+
'',
|
|
162
|
+
'| 下游角色 | 交付物 | 对方需要的核心信息 | 常见退货原因 |',
|
|
163
|
+
'|---------|--------|------------------|-------------|',
|
|
164
|
+
'| 后端 | 接口对接文档/契约 | 请求/响应/错误码约定、时序、分页排序约定 | 字段名与后端对不上、错误处理各写各的 |',
|
|
165
|
+
'| QA | 自测报告/测试范围 | 已测/未测项、环境地址、Mock 说明、已知问题 | 无自测记录、环境缺数据 |',
|
|
166
|
+
'| UI 设计师 | 还原实现(可访问预览) | 各断点/状态实现效果、偏差清单及原因 | 偏差无说明,被当 bug 打回 |',
|
|
167
|
+
'| DevOps | 构建产物/部署配置 | 环境变量清单、构建命令、SPA 路由回退、缓存策略 | 部署后白屏 404 |',
|
|
168
|
+
'',
|
|
169
|
+
'**交付动作三步**:',
|
|
170
|
+
'',
|
|
171
|
+
'1. **交前核对**:核心链路自测 + 契约确认 + 预览可访问。',
|
|
172
|
+
'2. **交底说明**:测什么 / 不测什么 / 已知问题。',
|
|
173
|
+
'3. **交后跟进**:Bug 24h 分级响应、首周每日巡检。',
|
|
174
|
+
'',
|
|
175
|
+
];
|
|
176
|
+
return matrix.join('\n');
|
|
177
|
+
}
|
|
83
178
|
// ============= HTML (T9.3) =============
|
|
84
179
|
export async function renderHtml(markdown, title = '方案文档') {
|
|
85
180
|
const bodyHtml = await marked.parse(markdown);
|
|
@@ -87,11 +182,12 @@ export async function renderHtml(markdown, title = '方案文档') {
|
|
|
87
182
|
const mermaidScript = hasMermaid
|
|
88
183
|
? '<script type="module">\nimport mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";\nmermaid.initialize({ startOnLoad: true });\n</script>'
|
|
89
184
|
: '';
|
|
185
|
+
const safeTitle = escapeHtml(title);
|
|
90
186
|
return `<!DOCTYPE html>
|
|
91
187
|
<html lang="zh-CN">
|
|
92
188
|
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
93
|
-
<title>${
|
|
189
|
+
<title>${safeTitle}</title>
|
|
94
190
|
<style>body{max-width:920px;margin:0 auto;padding:2rem;font-family:system-ui,sans-serif;line-height:1.65}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:.4rem}pre{background:#f6f8fa;padding:1rem;overflow-x:auto}</style>
|
|
95
191
|
</head>
|
|
96
|
-
<body><h1>${
|
|
192
|
+
<body><h1>${safeTitle}</h1><article>${bodyHtml}</article>${mermaidScript}</body></html>`;
|
|
97
193
|
}
|
|
@@ -20,10 +20,23 @@ export interface ReviewPacket {
|
|
|
20
20
|
pointId: string;
|
|
21
21
|
text: string;
|
|
22
22
|
}>;
|
|
23
|
+
/** Previous round's findings (decision 9 · closed loop): the reviewer must verify each was
|
|
24
|
+
* addressed via the `<!-- review-fix -->` note in the chapter and not re-report fixed ones. */
|
|
25
|
+
prevFindings: ReviewFinding[];
|
|
23
26
|
}
|
|
24
|
-
export declare function buildReviewPacket(workRoot: string, change: string, outline: Outline
|
|
27
|
+
export declare function buildReviewPacket(workRoot: string, change: string, outline: Outline, prevFindings?: ReviewFinding[],
|
|
28
|
+
/** 内存态章节覆盖(P2):persist=false 时磁盘无章节文件,传 narrativeMap 让评审者
|
|
29
|
+
* 看到真实内容而非空串;缺省时回退读磁盘。 */
|
|
30
|
+
narratives?: Map<string, string>): Promise<ReviewPacket>;
|
|
25
31
|
export declare function reviewWithAgent(llm: DocumentLLM, packet: ReviewPacket, prompt?: string): Promise<ReviewResult>;
|
|
26
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 应用待修复的 findings。闭环语义(decision 9):
|
|
34
|
+
* - status==='open'(默认)→ 尚未解决,需要应用修复(追加 fix note / 重填实体)。
|
|
35
|
+
* - status==='fixed' → 评审者已确认上一轮注记生效,仅作闭环核对记账,不再应用。
|
|
36
|
+
* - status==='waived' → 豁免,不应用。
|
|
37
|
+
* 同一 finding 可能跨轮被回显,按 id 去重避免注记/修复重复累积。
|
|
38
|
+
*/
|
|
39
|
+
export declare function applyFindings(workRoot: string, findings: ReviewFinding[], apply: (finding: ReviewFinding) => Promise<void>, appliedIds?: Set<string>): Promise<void>;
|
|
27
40
|
export declare function sha256(content: string): string;
|
|
28
41
|
export declare function computePartHashes(workRoot: string, chapterIds: string[]): Promise<{
|
|
29
42
|
id: string;
|
|
@@ -37,6 +50,8 @@ export interface ReviewCheckResult {
|
|
|
37
50
|
message: string;
|
|
38
51
|
}>;
|
|
39
52
|
}
|
|
40
|
-
export declare function checkReviewResult(workRoot: string, review: ReviewResult, outline: Outline
|
|
53
|
+
export declare function checkReviewResult(workRoot: string, review: ReviewResult, outline: Outline, currentHashesParam?: Array<{
|
|
54
|
+
id: string;
|
|
55
|
+
sha256: string;
|
|
56
|
+
}>): Promise<ReviewCheckResult>;
|
|
41
57
|
export declare function writeReviewResult(workRoot: string, review: ReviewResult): Promise<void>;
|
|
42
|
-
export declare function readReviewResult(workRoot: string): Promise<ReviewResult | null>;
|
|
@@ -28,9 +28,17 @@ export const AI_REVIEW_CHECKLIST = [
|
|
|
28
28
|
'R14 大纲要点覆盖',
|
|
29
29
|
];
|
|
30
30
|
// ============= Packet (T8.1) =============
|
|
31
|
-
export async function buildReviewPacket(workRoot, change, outline
|
|
31
|
+
export async function buildReviewPacket(workRoot, change, outline, prevFindings = [],
|
|
32
|
+
/** 内存态章节覆盖(P2):persist=false 时磁盘无章节文件,传 narrativeMap 让评审者
|
|
33
|
+
* 看到真实内容而非空串;缺省时回退读磁盘。 */
|
|
34
|
+
narratives) {
|
|
32
35
|
const chapters = new Map();
|
|
33
36
|
for (const ch of outline.chapters) {
|
|
37
|
+
const fromMemory = narratives?.get(ch.id);
|
|
38
|
+
if (fromMemory !== undefined) {
|
|
39
|
+
chapters.set(ch.id, fromMemory);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
34
42
|
try {
|
|
35
43
|
chapters.set(ch.id, await fs.readFile(chapterPath(workRoot, ch.id), 'utf-8'));
|
|
36
44
|
}
|
|
@@ -45,6 +53,7 @@ export async function buildReviewPacket(workRoot, change, outline) {
|
|
|
45
53
|
chapters,
|
|
46
54
|
checklist: [...AI_REVIEW_CHECKLIST],
|
|
47
55
|
semanticChecklist: semanticCoverageChecklist(outline),
|
|
56
|
+
prevFindings,
|
|
48
57
|
};
|
|
49
58
|
}
|
|
50
59
|
// ============= Review call (T8.2) =============
|
|
@@ -53,18 +62,32 @@ export async function reviewWithAgent(llm, packet, prompt) {
|
|
|
53
62
|
const semantic = packet.semanticChecklist.length > 0
|
|
54
63
|
? packet.semanticChecklist.map((s) => `- ${s.chapter} · ${s.pointId}: ${s.text}`).join('\n')
|
|
55
64
|
: '(无 narrative/mixed 要点)';
|
|
65
|
+
const prev = packet.prevFindings.length > 0
|
|
66
|
+
? packet.prevFindings
|
|
67
|
+
.map((f) => `- ${f.id} [${f.severity}] ${f.target ?? 'narrative'} ${f.chapter ?? ''}: ${f.message}`)
|
|
68
|
+
.join('\n')
|
|
69
|
+
: '(无)';
|
|
56
70
|
const content = [
|
|
57
71
|
`# Review packet`,
|
|
58
72
|
`change: ${packet.change}`,
|
|
59
73
|
`checklist:\n${checklist}`,
|
|
74
|
+
`## 上一轮 findings(闭环核对:对应章节已追加 <!-- review-fix --> 注记;已解决 → status=fixed,勿重复报告)`,
|
|
75
|
+
prev,
|
|
60
76
|
`## R14 语义覆盖核对清单(逐要点核对:对应小节存在且有实质内容)`,
|
|
61
77
|
semantic,
|
|
62
78
|
`outline: ${JSON.stringify(packet.outline)}`,
|
|
63
79
|
`chapters:\n${[...packet.chapters.entries()].map(([id, c]) => `### ${id}\n${c}`).join('\n\n')}`,
|
|
64
80
|
].join('\n\n');
|
|
81
|
+
// 超长保护(P2):大文档全文内联会超模型上下文导致审查失败;截断章节正文并标注。
|
|
82
|
+
const MAX_PACKET_CHARS = 60000;
|
|
83
|
+
const contentFinal = content.length > MAX_PACKET_CHARS
|
|
84
|
+
? content.slice(0, MAX_PACKET_CHARS) + `…(已截断,原内容 ${content.length} 字符 > ${MAX_PACKET_CHARS};请基于可见部分审查)`
|
|
85
|
+
: content;
|
|
65
86
|
const input = {
|
|
66
|
-
system: prompt ??
|
|
67
|
-
|
|
87
|
+
system: prompt ??
|
|
88
|
+
'你是独立长文档审查者。按 checklist 审查,输出 review-result JSON。R14 必须逐要点核对语义覆盖清单。' +
|
|
89
|
+
'对「上一轮 findings」逐条核实:对应章节已含 review-fix 注记且问题已解决 → 标 status=fixed;仅报告仍未解决的问题。',
|
|
90
|
+
messages: [{ role: 'user', content: contentFinal }],
|
|
68
91
|
};
|
|
69
92
|
let res;
|
|
70
93
|
try {
|
|
@@ -74,18 +97,33 @@ export async function reviewWithAgent(llm, packet, prompt) {
|
|
|
74
97
|
throw new Error(`reviewWithAgent: LLM call failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
75
98
|
}
|
|
76
99
|
try {
|
|
77
|
-
|
|
100
|
+
// 剥离 ```json 围栏与前后缀文本(P2):LLM 常用围栏包裹/附带说明,裸 JSON.parse 会硬失败。
|
|
101
|
+
const content = res.content.trim();
|
|
102
|
+
const fence = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
103
|
+
const jsonText = fence ? fence[1].trim() : content;
|
|
104
|
+
return parseReviewResult(JSON.parse(jsonText));
|
|
78
105
|
}
|
|
79
106
|
catch (e) {
|
|
80
107
|
throw new Error(`reviewWithAgent: unparsable review result: ${e instanceof Error ? e.message : String(e)}`);
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
// ============= Apply findings by target (T8.3) =============
|
|
84
|
-
|
|
111
|
+
/**
|
|
112
|
+
* 应用待修复的 findings。闭环语义(decision 9):
|
|
113
|
+
* - status==='open'(默认)→ 尚未解决,需要应用修复(追加 fix note / 重填实体)。
|
|
114
|
+
* - status==='fixed' → 评审者已确认上一轮注记生效,仅作闭环核对记账,不再应用。
|
|
115
|
+
* - status==='waived' → 豁免,不应用。
|
|
116
|
+
* 同一 finding 可能跨轮被回显,按 id 去重避免注记/修复重复累积。
|
|
117
|
+
*/
|
|
118
|
+
export async function applyFindings(workRoot, findings, apply, appliedIds) {
|
|
119
|
+
const seen = appliedIds ?? new Set();
|
|
85
120
|
for (const f of findings) {
|
|
86
|
-
if (f.status
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
if (f.status !== 'open')
|
|
122
|
+
continue;
|
|
123
|
+
if (seen.has(f.id))
|
|
124
|
+
continue;
|
|
125
|
+
seen.add(f.id);
|
|
126
|
+
await apply(f);
|
|
89
127
|
}
|
|
90
128
|
}
|
|
91
129
|
// ============= Part hashes (T8.4) =============
|
|
@@ -105,7 +143,7 @@ export async function computePartHashes(workRoot, chapterIds) {
|
|
|
105
143
|
}
|
|
106
144
|
return out;
|
|
107
145
|
}
|
|
108
|
-
export async function checkReviewResult(workRoot, review, outline) {
|
|
146
|
+
export async function checkReviewResult(workRoot, review, outline, currentHashesParam) {
|
|
109
147
|
const diags = [];
|
|
110
148
|
if (review.verdict === 'fail') {
|
|
111
149
|
diags.push({ code: 'review_verdict_fail', severity: 'error', message: 'review verdict is fail' });
|
|
@@ -122,9 +160,19 @@ export async function checkReviewResult(workRoot, review, outline) {
|
|
|
122
160
|
// part_hashes tamper detection (decision 9): review-result MUST include a hash for every chapter
|
|
123
161
|
// that exists on disk, so post-review changes are detected. Missing coverage for a persisted
|
|
124
162
|
// chapter = incomplete review (no tamper verification).
|
|
125
|
-
|
|
163
|
+
//
|
|
164
|
+
// `part_hashes` is the engine-computed snapshot of what the reviewer ACTUALLY saw (the LLM
|
|
165
|
+
// cannot compute sha256 reliably). Chapters the engine legitimately changed after that snapshot
|
|
166
|
+
// (entity refill / narrative fix notes) are recorded in `post_review_repairs` and exempted from
|
|
167
|
+
// the "stale" verdict — everything else must still match the snapshot, so genuine post-review
|
|
168
|
+
// tampering is still caught. The engine no longer refreshes hashes before checking, which made
|
|
169
|
+
// the check vacuous.
|
|
170
|
+
// currentHashes 由调用方提供(P2):persist=false 时磁盘无章节,引擎传内存哈希;
|
|
171
|
+
// 缺省时回退读磁盘(兼容旧调用方/validateWork)。
|
|
172
|
+
const currentHashes = currentHashesParam ?? await computePartHashes(workRoot, outline.chapters.map((c) => c.id));
|
|
126
173
|
const current = new Map(currentHashes.map((h) => [h.id, h.sha256]));
|
|
127
174
|
const reported = new Set((review.part_hashes ?? []).map((ph) => ph.id));
|
|
175
|
+
const repaired = new Set((review.post_review_repairs ?? []).map((r) => r.chapter));
|
|
128
176
|
for (const [chId] of current) {
|
|
129
177
|
if (!reported.has(chId)) {
|
|
130
178
|
diags.push({
|
|
@@ -136,7 +184,13 @@ export async function checkReviewResult(workRoot, review, outline) {
|
|
|
136
184
|
}
|
|
137
185
|
for (const ph of review.part_hashes ?? []) {
|
|
138
186
|
const now = current.get(ph.id);
|
|
139
|
-
if (now
|
|
187
|
+
if (now === undefined) {
|
|
188
|
+
// 审查后章节被删除(P2):此前 computePartHashes 静默跳过缺失文件,删除型篡改不可见。
|
|
189
|
+
if (!repaired.has(ph.id)) {
|
|
190
|
+
diags.push({ code: 'review_part_missing', severity: 'error', message: `Chapter "${ph.id}" existed at review time but is now missing — possible post-review deletion` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (now !== ph.sha256 && !repaired.has(ph.id)) {
|
|
140
194
|
diags.push({ code: 'review_part_stale', severity: 'error', message: `Chapter "${ph.id}" changed after review; re-run review` });
|
|
141
195
|
}
|
|
142
196
|
}
|
|
@@ -146,12 +200,3 @@ export async function checkReviewResult(workRoot, review, outline) {
|
|
|
146
200
|
export async function writeReviewResult(workRoot, review) {
|
|
147
201
|
await fs.writeFile(join(workRoot, 'review-result.json'), JSON.stringify(review, null, 2), 'utf-8');
|
|
148
202
|
}
|
|
149
|
-
export async function readReviewResult(workRoot) {
|
|
150
|
-
try {
|
|
151
|
-
const raw = JSON.parse(await fs.readFile(join(workRoot, 'review-result.json'), 'utf-8'));
|
|
152
|
-
return parseReviewResult(raw);
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature
|
|
3
|
-
* natural language; confirm with the user when uncertain).
|
|
2
|
+
* Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature /
|
|
3
|
+
* poc / migration from natural language; confirm with the user when uncertain).
|
|
4
4
|
*
|
|
5
5
|
* Deterministic keyword scoring as a first pass. The Agent (IDE model) may refine and confirm
|
|
6
6
|
* when confidence is low. Returns the best match + confidence + candidates.
|
|
7
|
+
*
|
|
8
|
+
* Scene axis (what we are doing) is orthogonal to the tech-stack axis (which layers are
|
|
9
|
+
* involved — derived by input-features hasBackend/uiInScope). `frontend-0to1` is the one
|
|
10
|
+
* stack-specific variant: pure-frontend 0→1 (no backend signals → 0to1 is narrowed to it).
|
|
7
11
|
*/
|
|
8
|
-
export type SceneProfile = '0to1' | 'bugfix' | 'feature';
|
|
12
|
+
export type SceneProfile = '0to1' | 'frontend-0to1' | 'bugfix' | 'feature' | 'poc' | 'migration';
|
|
9
13
|
export interface SceneDetectResult {
|
|
10
14
|
profile: SceneProfile | null;
|
|
11
15
|
confidence: number;
|
|
@@ -17,6 +21,9 @@ export interface SceneDetectResult {
|
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Detect the most likely scene profile from natural language.
|
|
24
|
+
* - `frontend-0to1` is NOT a competing candidate: it is a narrowing of `0to1` (pure-frontend,
|
|
25
|
+
* no backend hints) — so the "strongest scene" race runs on the 5 base scenes only, and 0to1
|
|
26
|
+
* is narrowed to frontend-0to1 when backend is absent and pure-frontend signals are present.
|
|
20
27
|
* - Returns a profile when its score clearly leads (≥ 2× runner-up, or score ≥ threshold).
|
|
21
28
|
* - Returns null when signals are weak/conflicting → caller should confirm with the user.
|
|
22
29
|
*/
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature
|
|
3
|
-
* natural language; confirm with the user when uncertain).
|
|
2
|
+
* Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature /
|
|
3
|
+
* poc / migration from natural language; confirm with the user when uncertain).
|
|
4
4
|
*
|
|
5
5
|
* Deterministic keyword scoring as a first pass. The Agent (IDE model) may refine and confirm
|
|
6
6
|
* when confidence is low. Returns the best match + confidence + candidates.
|
|
7
|
+
*
|
|
8
|
+
* Scene axis (what we are doing) is orthogonal to the tech-stack axis (which layers are
|
|
9
|
+
* involved — derived by input-features hasBackend/uiInScope). `frontend-0to1` is the one
|
|
10
|
+
* stack-specific variant: pure-frontend 0→1 (no backend signals → 0to1 is narrowed to it).
|
|
7
11
|
*/
|
|
8
12
|
const KEYWORDS = {
|
|
9
13
|
// 从零做一个产品/项目
|
|
@@ -23,7 +27,24 @@ const KEYWORDS = {
|
|
|
23
27
|
{ text: 'mvp', weight: 1 },
|
|
24
28
|
{ text: 'MVP', weight: 1 },
|
|
25
29
|
],
|
|
26
|
-
//
|
|
30
|
+
// 纯前端 0→1(无后端信号的 0to1 收敛变体;hasBackend 命中时回落 0to1)
|
|
31
|
+
'frontend-0to1': [
|
|
32
|
+
{ text: '纯前端', weight: 4 },
|
|
33
|
+
{ text: '前端项目', weight: 3 },
|
|
34
|
+
{ text: '静态站点', weight: 3 },
|
|
35
|
+
{ text: '静态网站', weight: 3 },
|
|
36
|
+
{ text: '官网', weight: 2 },
|
|
37
|
+
{ text: '落地页', weight: 2 },
|
|
38
|
+
{ text: 'landing page', weight: 3 },
|
|
39
|
+
{ text: 'landing', weight: 2 },
|
|
40
|
+
{ text: '前端应用', weight: 3 },
|
|
41
|
+
{ text: 'web 前端', weight: 2 },
|
|
42
|
+
{ text: '无后端', weight: 3 },
|
|
43
|
+
{ text: '不要后端', weight: 3 },
|
|
44
|
+
{ text: '只有页面', weight: 2 },
|
|
45
|
+
{ text: '纯 UI', weight: 2 },
|
|
46
|
+
],
|
|
47
|
+
// 修 bug / 排查故障(含前端/多端维度;不再单列 fullstack-bugfix)
|
|
27
48
|
bugfix: [
|
|
28
49
|
{ text: '报错', weight: 3 },
|
|
29
50
|
{ text: '500', weight: 3 },
|
|
@@ -43,8 +64,19 @@ const KEYWORDS = {
|
|
|
43
64
|
{ text: '排查', weight: 2 },
|
|
44
65
|
{ text: '定位问题', weight: 2 },
|
|
45
66
|
{ text: 'debug', weight: 2 },
|
|
67
|
+
{ text: '前端报错', weight: 3 },
|
|
68
|
+
{ text: '页面崩溃', weight: 3 },
|
|
69
|
+
{ text: '页面白屏', weight: 3 },
|
|
70
|
+
{ text: '渲染问题', weight: 3 },
|
|
71
|
+
{ text: '样式错乱', weight: 3 },
|
|
72
|
+
{ text: '浏览器兼容', weight: 2 },
|
|
73
|
+
{ text: '兼容性问题', weight: 3 },
|
|
74
|
+
{ text: '多端', weight: 2 },
|
|
75
|
+
{ text: '跨端', weight: 2 },
|
|
76
|
+
{ text: '联调 bug', weight: 3 },
|
|
77
|
+
{ text: '数据对不上', weight: 2 },
|
|
46
78
|
],
|
|
47
|
-
//
|
|
79
|
+
// 给已有系统加功能(含全栈/接口/前端维度,由输入特征伸缩;不再单列 fullstack-feature)
|
|
48
80
|
feature: [
|
|
49
81
|
{ text: '新增', weight: 3 },
|
|
50
82
|
{ text: '增加', weight: 2 },
|
|
@@ -56,50 +88,131 @@ const KEYWORDS = {
|
|
|
56
88
|
{ text: '功能', weight: 1 },
|
|
57
89
|
{ text: '开发一个功能', weight: 3 },
|
|
58
90
|
{ text: 'feat', weight: 2 },
|
|
91
|
+
{ text: '新增模块', weight: 3 },
|
|
92
|
+
{ text: '新页面', weight: 3 },
|
|
93
|
+
{ text: '新路由', weight: 3 },
|
|
94
|
+
{ text: '多个页面', weight: 3 },
|
|
95
|
+
{ text: '状态管理', weight: 2 },
|
|
96
|
+
{ text: '接口对接', weight: 2 },
|
|
97
|
+
{ text: '前后端联调', weight: 3 },
|
|
98
|
+
{ text: '功能开发', weight: 2 },
|
|
99
|
+
{ text: '组件化', weight: 2 },
|
|
100
|
+
{ text: '页面组件', weight: 2 },
|
|
101
|
+
],
|
|
102
|
+
// 技术预研 / PoC / 方案选型(原 fullstack-poc;预研不限于全栈,是否全栈由特征决定)
|
|
103
|
+
poc: [
|
|
104
|
+
{ text: '预研', weight: 3 },
|
|
105
|
+
{ text: '技术调研', weight: 3 },
|
|
106
|
+
{ text: '方案对比', weight: 3 },
|
|
107
|
+
{ text: '候选', weight: 2 },
|
|
108
|
+
{ text: 'poc', weight: 3 },
|
|
109
|
+
{ text: 'PoC', weight: 3 },
|
|
110
|
+
{ text: 'benchmark', weight: 2 },
|
|
111
|
+
{ text: '选型评估', weight: 3 },
|
|
112
|
+
{ text: '评估一下', weight: 2 },
|
|
113
|
+
{ text: '对比一下', weight: 2 },
|
|
114
|
+
{ text: '可行性验证', weight: 3 },
|
|
115
|
+
],
|
|
116
|
+
// 技术升级(原 fullstack-migration;是否全栈由特征决定)
|
|
117
|
+
migration: [
|
|
118
|
+
{ text: '迁移', weight: 3 },
|
|
119
|
+
{ text: '大版本', weight: 3 },
|
|
120
|
+
{ text: '框架升级', weight: 3 },
|
|
121
|
+
{ text: '技术升级', weight: 3 },
|
|
122
|
+
{ text: '构建工具升级', weight: 3 },
|
|
123
|
+
{ text: 'vue2', weight: 2 },
|
|
124
|
+
{ text: 'vue3', weight: 2 },
|
|
125
|
+
{ text: 'webpack', weight: 2 },
|
|
126
|
+
{ text: 'vite', weight: 2 },
|
|
127
|
+
{ text: '代码迁移', weight: 3 },
|
|
128
|
+
{ text: '升级到', weight: 2 },
|
|
59
129
|
],
|
|
60
130
|
};
|
|
61
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* 关键词匹配(P2):
|
|
133
|
+
* - CJK 关键词:子串匹配。
|
|
134
|
+
* - ASCII/数字关键词(api/server/rest/500/vue2 等):词边界匹配,避免 'api'⊂rapid、'500'⊂1500、'server'⊂serverless 误报。
|
|
135
|
+
*/
|
|
136
|
+
function keywordHit(text, kw) {
|
|
137
|
+
if (/^[A-Za-z0-9_.-]+$/.test(kw)) {
|
|
138
|
+
const re = new RegExp(`(^|[^A-Za-z0-9_])${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^A-Za-z0-9_])`);
|
|
139
|
+
return re.test(text);
|
|
140
|
+
}
|
|
141
|
+
return text.includes(kw);
|
|
142
|
+
}
|
|
143
|
+
/** Score a text against one profile's keywords. 重叠关键词取最长匹配(P2):
|
|
144
|
+
* '想做一个' 命中后不再计 '想做'/'做一个';'landing page' 命中后不再计 'landing';
|
|
145
|
+
* '开发一个功能' 命中后不再计 '功能'——避免同一短语重复计分导致误判场景。 */
|
|
62
146
|
function score(text, profile) {
|
|
147
|
+
// 按长度降序处理:先匹配长的(更具体),命中则其包含的短词不再计分。
|
|
148
|
+
const sorted = [...KEYWORDS[profile]].sort((a, b) => b.text.length - a.text.length);
|
|
63
149
|
let score = 0;
|
|
64
150
|
const hits = [];
|
|
65
|
-
for (const kw of
|
|
66
|
-
if (text
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
151
|
+
for (const kw of sorted) {
|
|
152
|
+
if (!keywordHit(text, kw.text))
|
|
153
|
+
continue;
|
|
154
|
+
// 若存在更长的已命中关键词包含本词(如 '想做一个' 包含 '想做'),跳过本词避免重复计分。
|
|
155
|
+
const coveredByLonger = sorted.some((other) => other !== kw && other.text.length > kw.text.length && other.text.includes(kw.text) && keywordHit(text, other.text));
|
|
156
|
+
if (coveredByLonger)
|
|
157
|
+
continue;
|
|
158
|
+
score += kw.weight;
|
|
159
|
+
hits.push(kw.text);
|
|
70
160
|
}
|
|
71
161
|
return { score, hits };
|
|
72
162
|
}
|
|
163
|
+
/** Backend signal: if the text mentions backend/API/database, a 0to1 is NOT pure-frontend.
|
|
164
|
+
* Negated forms (无后端/不要后端/没有后端…) are stripped first so they don't false-positive.
|
|
165
|
+
* ASCII hints use word-boundary matching(P2):'api' 不应命中 rapid/capital,'server' 不应命中 serverless。 */
|
|
166
|
+
const BACKEND_HINTS = ['后端', '服务端', '服务器', '接口', '数据库', '微服务', '后台', '网关', 'api', 'rest', 'backend', 'server', 'daemon'];
|
|
167
|
+
const BACKEND_NEGATIONS = ['无后端', '不要后端', '没有后端', '不用后端', '不需要后端', '无需后端', 'no backend', 'without backend', '不接后端'];
|
|
168
|
+
function hasBackendSignal(lower) {
|
|
169
|
+
let text = lower;
|
|
170
|
+
for (const neg of BACKEND_NEGATIONS)
|
|
171
|
+
text = text.split(neg).join(' ');
|
|
172
|
+
return BACKEND_HINTS.some((h) => keywordHit(text, h));
|
|
173
|
+
}
|
|
73
174
|
/**
|
|
74
175
|
* Detect the most likely scene profile from natural language.
|
|
176
|
+
* - `frontend-0to1` is NOT a competing candidate: it is a narrowing of `0to1` (pure-frontend,
|
|
177
|
+
* no backend hints) — so the "strongest scene" race runs on the 5 base scenes only, and 0to1
|
|
178
|
+
* is narrowed to frontend-0to1 when backend is absent and pure-frontend signals are present.
|
|
75
179
|
* - Returns a profile when its score clearly leads (≥ 2× runner-up, or score ≥ threshold).
|
|
76
180
|
* - Returns null when signals are weak/conflicting → caller should confirm with the user.
|
|
77
181
|
*/
|
|
78
182
|
export function detectScene(text) {
|
|
79
183
|
const lower = text.toLowerCase();
|
|
80
|
-
const
|
|
184
|
+
const all = Object.keys(KEYWORDS).map((p) => ({
|
|
81
185
|
profile: p,
|
|
82
186
|
score: score(lower, p).score,
|
|
83
187
|
}));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
const
|
|
188
|
+
const candidates = [...all].sort((a, b) => b.score - a.score);
|
|
189
|
+
// Base-scene race excludes frontend-0to1 (it narrows 0to1, never outranks it).
|
|
190
|
+
const base = all.filter((s) => s.profile !== 'frontend-0to1').sort((a, b) => b.score - a.score);
|
|
191
|
+
const best = base[0];
|
|
192
|
+
const runner = base[1];
|
|
193
|
+
const total = base.reduce((s, x) => s + x.score, 0);
|
|
88
194
|
const reasons = [];
|
|
89
195
|
// Strong signal: clear winner.
|
|
90
196
|
if (best.score >= 3 && (runner.score === 0 || best.score >= runner.score * 2)) {
|
|
197
|
+
let winner = best.profile;
|
|
198
|
+
// 0to1 → frontend-0to1 narrowing: pure-frontend signals with no backend hints.
|
|
199
|
+
if (best.profile === '0to1' && !hasBackendSignal(lower)) {
|
|
200
|
+
const feScore = candidates.find((s) => s.profile === 'frontend-0to1')?.score ?? 0;
|
|
201
|
+
if (feScore > 0)
|
|
202
|
+
winner = 'frontend-0to1';
|
|
203
|
+
}
|
|
91
204
|
const confidence = Math.min(1, best.score / (best.score + runner.score + 1));
|
|
92
|
-
reasons.push(`匹配「${
|
|
93
|
-
return { profile:
|
|
205
|
+
reasons.push(`匹配「${winner}」关键词 ${best.score} 分(次优 ${runner.profile} ${runner.score} 分)`);
|
|
206
|
+
return { profile: winner, confidence, candidates, reasons };
|
|
94
207
|
}
|
|
95
208
|
// Conflicting/weak → uncertain.
|
|
96
209
|
if (total === 0) {
|
|
97
210
|
reasons.push('未检测到明显场景信号,需向用户确认');
|
|
98
211
|
}
|
|
99
212
|
else {
|
|
100
|
-
reasons.push(`场景信号较弱或冲突(${
|
|
213
|
+
reasons.push(`场景信号较弱或冲突(${base.map((s) => `${s.profile}:${s.score}`).join(', ')}),需确认`);
|
|
101
214
|
}
|
|
102
|
-
return { profile: null, confidence: 0, candidates
|
|
215
|
+
return { profile: null, confidence: 0, candidates, reasons };
|
|
103
216
|
}
|
|
104
217
|
/** The user-facing prompt when detection is uncertain. */
|
|
105
218
|
export function confirmationPrompt(result, text) {
|
|
@@ -113,9 +226,12 @@ export function confirmationPrompt(result, text) {
|
|
|
113
226
|
.map((c) => `- ${c.profile}: ${c.score} 分`),
|
|
114
227
|
'',
|
|
115
228
|
`请选择场景(或直接输入新的需求描述):`,
|
|
116
|
-
`- 0to1 →
|
|
117
|
-
`-
|
|
118
|
-
`-
|
|
229
|
+
`- 0to1 → 从零启动一个产品/项目(全栈;纯前端自动走 frontend-0to1)`,
|
|
230
|
+
`- frontend-0to1 → 从零启动一个纯前端项目(无后端/仅页面与样式)`,
|
|
231
|
+
`- bugfix → 排查并修复一个问题(含前端/多端维度,按输入特征伸缩)`,
|
|
232
|
+
`- feature → 给已有系统新增功能(按输入特征伸缩:纯逻辑/含后端/含前端/全栈)`,
|
|
233
|
+
`- poc → 技术预研/选型(新技术评估/PoC/方案对比)`,
|
|
234
|
+
`- migration → 技术升级(框架迁移/大版本/构建工具升级)`,
|
|
119
235
|
];
|
|
120
236
|
return lines.join('\n');
|
|
121
237
|
}
|