@gordon.gan/specflow 1.8.0-beta → 1.8.2-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.
Files changed (181) hide show
  1. package/README.md +7 -5
  2. package/dist/cli/commands/document-run.d.ts +98 -0
  3. package/dist/cli/commands/document-run.js +1064 -0
  4. package/dist/cli/index.js +2 -2
  5. package/dist/core/artifact-language.js +11 -1
  6. package/dist/core/document/asset-paths.d.ts +15 -0
  7. package/dist/core/document/asset-paths.js +38 -0
  8. package/dist/core/document/chapters.d.ts +6 -0
  9. package/dist/core/document/chapters.js +38 -0
  10. package/dist/core/document/coverage.d.ts +25 -0
  11. package/dist/core/document/coverage.js +49 -0
  12. package/dist/core/document/digests.d.ts +40 -0
  13. package/dist/core/document/digests.js +144 -0
  14. package/dist/core/document/engine.d.ts +101 -0
  15. package/dist/core/document/engine.js +722 -0
  16. package/dist/core/document/extract.d.ts +61 -0
  17. package/dist/core/document/extract.js +437 -0
  18. package/dist/core/{approval → document}/forbidden-patterns.d.ts +4 -2
  19. package/dist/core/document/forbidden-patterns.js +36 -0
  20. package/dist/core/document/gates.d.ts +39 -0
  21. package/dist/core/document/gates.js +75 -0
  22. package/dist/core/document/input-digest.d.ts +63 -0
  23. package/dist/core/document/input-digest.js +200 -0
  24. package/dist/core/document/input-features.d.ts +43 -0
  25. package/dist/core/document/input-features.js +161 -0
  26. package/dist/core/document/lint.d.ts +39 -0
  27. package/dist/core/document/lint.js +147 -0
  28. package/dist/core/document/llm.d.ts +28 -0
  29. package/dist/core/document/llm.js +22 -0
  30. package/dist/core/document/map.d.ts +52 -0
  31. package/dist/core/document/map.js +296 -0
  32. package/dist/core/document/outline.d.ts +29 -0
  33. package/dist/core/document/outline.js +171 -0
  34. package/dist/core/document/paths.d.ts +23 -0
  35. package/dist/core/document/paths.js +36 -0
  36. package/dist/core/document/profile-validator.d.ts +23 -0
  37. package/dist/core/document/profile-validator.js +155 -0
  38. package/dist/core/document/profiles.d.ts +23 -0
  39. package/dist/core/document/profiles.js +76 -0
  40. package/dist/core/document/render.d.ts +61 -0
  41. package/dist/core/document/render.js +243 -0
  42. package/dist/core/document/review.d.ts +57 -0
  43. package/dist/core/document/review.js +202 -0
  44. package/dist/core/document/scene-detect.d.ts +32 -0
  45. package/dist/core/document/scene-detect.js +237 -0
  46. package/dist/core/document/schemas.d.ts +1111 -0
  47. package/dist/core/document/schemas.js +191 -0
  48. package/dist/integrations/shared/capability-evidence.js +4 -9
  49. package/dist/integrations/shared/command-catalog.js +2 -1
  50. package/dist/integrations/shared/parity-manifest.js +4 -9
  51. package/package.json +2 -1
  52. package/prompts/apply/phase-a-plan.md +1 -1
  53. package/prompts/document/map/acceptance.md +9 -0
  54. package/prompts/document/map/anti-ai.md +29 -0
  55. package/prompts/document/map/api-design.md +26 -0
  56. package/prompts/document/map/architecture.md +27 -0
  57. package/prompts/document/map/benchmark.md +26 -0
  58. package/prompts/document/map/closed-loop.md +9 -0
  59. package/prompts/document/map/compat-migration.md +31 -0
  60. package/prompts/document/map/component-design.md +30 -0
  61. package/prompts/document/map/config-runtime.md +9 -0
  62. package/prompts/document/map/core-flow.md +62 -0
  63. package/prompts/document/map/core-logic.md +9 -0
  64. package/prompts/document/map/data-model.md +18 -0
  65. package/prompts/document/map/deploy.md +26 -0
  66. package/prompts/document/map/fix.md +8 -0
  67. package/prompts/document/map/frontend-architecture.md +35 -0
  68. package/prompts/document/map/goal.md +8 -0
  69. package/prompts/document/map/impact.md +8 -0
  70. package/prompts/document/map/implementability.md +9 -0
  71. package/prompts/document/map/migration-guide.md +36 -0
  72. package/prompts/document/map/mvp-boundary.md +8 -0
  73. package/prompts/document/map/non-goals.md +8 -0
  74. package/prompts/document/map/ops.md +33 -0
  75. package/prompts/document/map/performance.md +32 -0
  76. package/prompts/document/map/poc-demo.md +25 -0
  77. package/prompts/document/map/regression.md +8 -0
  78. package/prompts/document/map/reproduce.md +8 -0
  79. package/prompts/document/map/requirement.md +8 -0
  80. package/prompts/document/map/research.md +25 -0
  81. package/prompts/document/map/root-cause.md +8 -0
  82. package/prompts/document/map/signoff.md +9 -0
  83. package/prompts/document/map/state-management.md +23 -0
  84. package/prompts/document/map/tech-selection.md +29 -0
  85. package/prompts/document/map/test-strategy.md +24 -0
  86. package/prompts/document/map/ui-design.md +21 -0
  87. package/prompts/document/outline/general.md +46 -0
  88. package/prompts/document/review/ai-review.md +57 -0
  89. package/prompts/document/shared/grounding.md +84 -0
  90. package/prompts/explore/explore-session.md +1 -1
  91. package/prompts/propose/design-draft.md +1 -1
  92. package/prompts/propose/proposal.md +1 -1
  93. package/prompts/propose/specs.md +1 -1
  94. package/prompts/propose/tasks-draft.md +1 -1
  95. package/prompts/refine/design-output.md +1 -1
  96. package/prompts/refine/update-artifacts.md +2 -0
  97. package/prompts/shared/artifact-language.md +13 -0
  98. package/skills/specflow-techdoc/SKILL.md +143 -0
  99. package/skills/specflow-techdoc-synth/SKILL.md +115 -0
  100. package/templates/document/chapters/acceptance.yaml +16 -0
  101. package/templates/document/chapters/api-design.yaml +29 -0
  102. package/templates/document/chapters/architecture.yaml +21 -0
  103. package/templates/document/chapters/benchmark.yaml +20 -0
  104. package/templates/document/chapters/closed-loop.yaml +22 -0
  105. package/templates/document/chapters/compat-migration.yaml +21 -0
  106. package/templates/document/chapters/component-design.yaml +22 -0
  107. package/templates/document/chapters/config-runtime.yaml +16 -0
  108. package/templates/document/chapters/core-flow.yaml +27 -0
  109. package/templates/document/chapters/core-logic.yaml +16 -0
  110. package/templates/document/chapters/data-model.yaml +25 -0
  111. package/templates/document/chapters/deploy.yaml +22 -0
  112. package/templates/document/chapters/fix.yaml +18 -0
  113. package/templates/document/chapters/frontend-architecture.yaml +22 -0
  114. package/templates/document/chapters/goal.yaml +17 -0
  115. package/templates/document/chapters/impact.yaml +16 -0
  116. package/templates/document/chapters/implementability.yaml +22 -0
  117. package/templates/document/chapters/migration-guide.yaml +21 -0
  118. package/templates/document/chapters/mvp-boundary.yaml +20 -0
  119. package/templates/document/chapters/non-goals.yaml +17 -0
  120. package/templates/document/chapters/ops.yaml +25 -0
  121. package/templates/document/chapters/performance.yaml +21 -0
  122. package/templates/document/chapters/poc-demo.yaml +22 -0
  123. package/templates/document/chapters/regression.yaml +16 -0
  124. package/templates/document/chapters/reproduce.yaml +16 -0
  125. package/templates/document/chapters/requirement.yaml +18 -0
  126. package/templates/document/chapters/research.yaml +22 -0
  127. package/templates/document/chapters/root-cause.yaml +18 -0
  128. package/templates/document/chapters/signoff.yaml +15 -0
  129. package/templates/document/chapters/state-management.yaml +22 -0
  130. package/templates/document/chapters/tech-selection.yaml +23 -0
  131. package/templates/document/chapters/test-strategy.yaml +23 -0
  132. package/templates/document/chapters/ui-design.yaml +28 -0
  133. package/templates/document/profiles/0to1.yaml +44 -0
  134. package/templates/document/profiles/approve.yaml +22 -0
  135. package/templates/document/profiles/bugfix.yaml +15 -0
  136. package/templates/document/profiles/feature.yaml +23 -0
  137. package/templates/document/profiles/frontend-0to1.yaml +47 -0
  138. package/templates/document/profiles/migration.yaml +42 -0
  139. package/templates/document/profiles/poc.yaml +46 -0
  140. package/dist/cli/commands/approval-assemble.d.ts +0 -64
  141. package/dist/cli/commands/approval-assemble.js +0 -408
  142. package/dist/core/approval/assemble.d.ts +0 -10
  143. package/dist/core/approval/assemble.js +0 -384
  144. package/dist/core/approval/bundle.d.ts +0 -9
  145. package/dist/core/approval/bundle.js +0 -172
  146. package/dist/core/approval/forbidden-patterns.js +0 -40
  147. package/dist/core/approval/index-schema.d.ts +0 -676
  148. package/dist/core/approval/index-schema.js +0 -193
  149. package/dist/core/approval/index.d.ts +0 -12
  150. package/dist/core/approval/index.js +0 -9
  151. package/dist/core/approval/lint.d.ts +0 -10
  152. package/dist/core/approval/lint.js +0 -302
  153. package/dist/core/approval/paths.d.ts +0 -13
  154. package/dist/core/approval/paths.js +0 -43
  155. package/dist/core/approval/pipeline.d.ts +0 -28
  156. package/dist/core/approval/pipeline.js +0 -146
  157. package/dist/core/approval/playbook-schema.d.ts +0 -182
  158. package/dist/core/approval/playbook-schema.js +0 -51
  159. package/dist/core/approval/render.d.ts +0 -20
  160. package/dist/core/approval/render.js +0 -210
  161. package/dist/core/approval/review-pack.d.ts +0 -26
  162. package/dist/core/approval/review-pack.js +0 -205
  163. package/dist/core/approval/types.d.ts +0 -223
  164. package/dist/core/approval/types.js +0 -1
  165. package/prompts/approval/acp-pipeline.md +0 -106
  166. package/prompts/approval/ai-review.md +0 -145
  167. package/prompts/approval/api-guidance.md +0 -179
  168. package/prompts/approval/database-guidance.md +0 -105
  169. package/prompts/approval/frontend-guidance.md +0 -249
  170. package/prompts/approval/generate.md +0 -1596
  171. package/prompts/approval/multi-repo-guidance.md +0 -263
  172. package/prompts/approval/multi-repo-spoke-subagent.md +0 -94
  173. package/prompts/approval/project-conventions-guidance.md +0 -171
  174. package/prompts/approval/runtime-guidance.md +0 -64
  175. package/prompts/approval/segmented-generation.md +0 -157
  176. package/skills/specflow-approval/SKILL.md +0 -778
  177. package/templates/approval-index.yaml +0 -93
  178. package/templates/approval-part.md +0 -15
  179. package/templates/approval-playbook-talos-scenario-job-compile.yaml +0 -29
  180. package/templates/approval-playbook.yaml +0 -28
  181. package/templates/approval.md +0 -261
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Document Engine — Pass 3 AI review (decision 9 · target + max_rounds + part_hashes).
3
+ *
4
+ * Fresh-context reviewer (separate from generator) produces findings with a `target`
5
+ * (entity|narrative|mixed). Findings apply by target; review-result is validated against
6
+ * part_hashes (post-review tamper detection) and max_rounds (infinite-loop guard).
7
+ */
8
+ import { createHash } from 'node:crypto';
9
+ import { promises as fs } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { parseReviewResult, } from './schemas.js';
12
+ import { chapterPath } from './paths.js';
13
+ import { semanticCoverageChecklist } from './coverage.js';
14
+ export const AI_REVIEW_CHECKLIST = [
15
+ 'R1 跨章命名一致',
16
+ 'R2 决策落地',
17
+ 'R3 实体↔正文',
18
+ 'R4 深度均衡',
19
+ 'R5 交叉引用稳定',
20
+ 'R6 图文一致',
21
+ 'R7 契约完整',
22
+ 'R8 枚举闭合',
23
+ 'R9 分支闭合',
24
+ 'R10 UI↔API 对齐',
25
+ 'R11 场景一致',
26
+ 'R12 stub 清除',
27
+ 'R13 结论一致',
28
+ 'R14 大纲要点覆盖',
29
+ ];
30
+ // ============= Packet (T8.1) =============
31
+ export async function buildReviewPacket(workRoot, change, outline, prevFindings = [],
32
+ /** 内存态章节覆盖(P2):persist=false 时磁盘无章节文件,传 narrativeMap 让评审者
33
+ * 看到真实内容而非空串;缺省时回退读磁盘。 */
34
+ narratives) {
35
+ const chapters = new Map();
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
+ }
42
+ try {
43
+ chapters.set(ch.id, await fs.readFile(chapterPath(workRoot, ch.id), 'utf-8'));
44
+ }
45
+ catch {
46
+ chapters.set(ch.id, '');
47
+ }
48
+ }
49
+ return {
50
+ schema: 'specflow.document.review-packet/v1',
51
+ change,
52
+ outline,
53
+ chapters,
54
+ checklist: [...AI_REVIEW_CHECKLIST],
55
+ semanticChecklist: semanticCoverageChecklist(outline),
56
+ prevFindings,
57
+ };
58
+ }
59
+ // ============= Review call (T8.2) =============
60
+ export async function reviewWithAgent(llm, packet, prompt) {
61
+ const checklist = packet.checklist.join('\n');
62
+ const semantic = packet.semanticChecklist.length > 0
63
+ ? packet.semanticChecklist.map((s) => `- ${s.chapter} · ${s.pointId}: ${s.text}`).join('\n')
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
+ : '(无)';
70
+ const content = [
71
+ `# Review packet`,
72
+ `change: ${packet.change}`,
73
+ `checklist:\n${checklist}`,
74
+ `## 上一轮 findings(闭环核对:对应章节已追加 <!-- review-fix --> 注记;已解决 → status=fixed,勿重复报告)`,
75
+ prev,
76
+ `## R14 语义覆盖核对清单(逐要点核对:对应小节存在且有实质内容)`,
77
+ semantic,
78
+ `outline: ${JSON.stringify(packet.outline)}`,
79
+ `chapters:\n${[...packet.chapters.entries()].map(([id, c]) => `### ${id}\n${c}`).join('\n\n')}`,
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;
86
+ const input = {
87
+ system: prompt ??
88
+ '你是独立长文档审查者。按 checklist 审查,输出 review-result JSON。R14 必须逐要点核对语义覆盖清单。' +
89
+ '对「上一轮 findings」逐条核实:对应章节已含 review-fix 注记且问题已解决 → 标 status=fixed;仅报告仍未解决的问题。',
90
+ messages: [{ role: 'user', content: contentFinal }],
91
+ };
92
+ let res;
93
+ try {
94
+ res = await llm.complete(input);
95
+ }
96
+ catch (e) {
97
+ throw new Error(`reviewWithAgent: LLM call failed: ${e instanceof Error ? e.message : String(e)}`);
98
+ }
99
+ try {
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));
105
+ }
106
+ catch (e) {
107
+ throw new Error(`reviewWithAgent: unparsable review result: ${e instanceof Error ? e.message : String(e)}`);
108
+ }
109
+ }
110
+ // ============= Apply findings by target (T8.3) =============
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();
120
+ for (const f of findings) {
121
+ if (f.status !== 'open')
122
+ continue;
123
+ if (seen.has(f.id))
124
+ continue;
125
+ seen.add(f.id);
126
+ await apply(f);
127
+ }
128
+ }
129
+ // ============= Part hashes (T8.4) =============
130
+ export function sha256(content) {
131
+ return createHash('sha256').update(content, 'utf8').digest('hex');
132
+ }
133
+ export async function computePartHashes(workRoot, chapterIds) {
134
+ const out = [];
135
+ for (const id of chapterIds) {
136
+ try {
137
+ const content = await fs.readFile(chapterPath(workRoot, id), 'utf-8');
138
+ out.push({ id, sha256: sha256(content) });
139
+ }
140
+ catch {
141
+ // missing chapter skipped
142
+ }
143
+ }
144
+ return out;
145
+ }
146
+ export async function checkReviewResult(workRoot, review, outline, currentHashesParam) {
147
+ const diags = [];
148
+ if (review.verdict === 'fail') {
149
+ diags.push({ code: 'review_verdict_fail', severity: 'error', message: 'review verdict is fail' });
150
+ }
151
+ // open critical/important findings
152
+ for (const f of review.findings) {
153
+ if (f.status === 'open' && f.severity !== 'minor') {
154
+ diags.push({ code: 'review_finding_open', severity: 'error', message: `[${f.id}] ${f.message}` });
155
+ }
156
+ if (f.status === 'waived' && !f.waiver_reason?.trim()) {
157
+ diags.push({ code: 'review_waiver_missing_reason', severity: 'error', message: `[${f.id}] waived finding must include waiver_reason` });
158
+ }
159
+ }
160
+ // part_hashes tamper detection (decision 9): review-result MUST include a hash for every chapter
161
+ // that exists on disk, so post-review changes are detected. Missing coverage for a persisted
162
+ // chapter = incomplete review (no tamper verification).
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));
173
+ const current = new Map(currentHashes.map((h) => [h.id, h.sha256]));
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));
176
+ for (const [chId] of current) {
177
+ if (!reported.has(chId)) {
178
+ diags.push({
179
+ code: 'review_part_hashes_missing',
180
+ severity: 'error',
181
+ message: `review-result missing part_hashes for persisted chapter "${chId}" — cannot verify no post-review tampering`,
182
+ });
183
+ }
184
+ }
185
+ for (const ph of review.part_hashes ?? []) {
186
+ const now = current.get(ph.id);
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)) {
194
+ diags.push({ code: 'review_part_stale', severity: 'error', message: `Chapter "${ph.id}" changed after review; re-run review` });
195
+ }
196
+ }
197
+ return { ok: diags.every((d) => d.severity !== 'error'), diagnostics: diags };
198
+ }
199
+ // ============= Persistence (T8.5) =============
200
+ export async function writeReviewResult(workRoot, review) {
201
+ await fs.writeFile(join(workRoot, 'review-result.json'), JSON.stringify(review, null, 2), 'utf-8');
202
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature /
3
+ * poc / migration from natural language; confirm with the user when uncertain).
4
+ *
5
+ * Deterministic keyword scoring as a first pass. The Agent (IDE model) may refine and confirm
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).
11
+ */
12
+ export type SceneProfile = '0to1' | 'frontend-0to1' | 'bugfix' | 'feature' | 'poc' | 'migration';
13
+ export interface SceneDetectResult {
14
+ profile: SceneProfile | null;
15
+ confidence: number;
16
+ candidates: Array<{
17
+ profile: SceneProfile;
18
+ score: number;
19
+ }>;
20
+ reasons: string[];
21
+ }
22
+ /**
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.
27
+ * - Returns a profile when its score clearly leads (≥ 2× runner-up, or score ≥ threshold).
28
+ * - Returns null when signals are weak/conflicting → caller should confirm with the user.
29
+ */
30
+ export declare function detectScene(text: string): SceneDetectResult;
31
+ /** The user-facing prompt when detection is uncertain. */
32
+ export declare function confirmationPrompt(result: SceneDetectResult, text: string): string;
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Document Engine — scene detection (decision: auto-detect 0to1 / bugfix / feature /
3
+ * poc / migration from natural language; confirm with the user when uncertain).
4
+ *
5
+ * Deterministic keyword scoring as a first pass. The Agent (IDE model) may refine and confirm
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).
11
+ */
12
+ const KEYWORDS = {
13
+ // 从零做一个产品/项目
14
+ '0to1': [
15
+ { text: '想做', weight: 3 },
16
+ { text: '想做一个', weight: 4 },
17
+ { text: '打算做', weight: 3 },
18
+ { text: '做一个', weight: 2 },
19
+ { text: '从零', weight: 3 },
20
+ { text: '新产品', weight: 2 },
21
+ { text: 'startup', weight: 2 },
22
+ { text: '想法', weight: 2 },
23
+ { text: 'idea', weight: 2 },
24
+ { text: '开始做', weight: 2 },
25
+ { text: '立项', weight: 2 },
26
+ { text: '技术选型', weight: 1 },
27
+ { text: 'mvp', weight: 1 },
28
+ { text: 'MVP', weight: 1 },
29
+ ],
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)
48
+ bugfix: [
49
+ { text: '报错', weight: 3 },
50
+ { text: '500', weight: 3 },
51
+ { text: '崩溃', weight: 3 },
52
+ { text: '挂了', weight: 3 },
53
+ { text: '修复', weight: 2 },
54
+ { text: '修一下', weight: 3 },
55
+ { text: 'bug', weight: 2 },
56
+ { text: 'BUG', weight: 2 },
57
+ { text: '异常', weight: 2 },
58
+ { text: '失败', weight: 2 },
59
+ { text: '闪退', weight: 3 },
60
+ { text: '无法使用', weight: 2 },
61
+ { text: '不工作', weight: 2 },
62
+ { text: '错误', weight: 2 },
63
+ { text: '故障', weight: 3 },
64
+ { text: '排查', weight: 2 },
65
+ { text: '定位问题', weight: 2 },
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 },
78
+ ],
79
+ // 给已有系统加功能(含全栈/接口/前端维度,由输入特征伸缩;不再单列 fullstack-feature)
80
+ feature: [
81
+ { text: '新增', weight: 3 },
82
+ { text: '增加', weight: 2 },
83
+ { text: '加一个', weight: 3 },
84
+ { text: '加个', weight: 3 },
85
+ { text: '扩展', weight: 2 },
86
+ { text: '添加', weight: 2 },
87
+ { text: '支持', weight: 1 },
88
+ { text: '功能', weight: 1 },
89
+ { text: '开发一个功能', weight: 3 },
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 },
129
+ ],
130
+ };
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
+ * '开发一个功能' 命中后不再计 '功能'——避免同一短语重复计分导致误判场景。 */
146
+ function score(text, profile) {
147
+ // 按长度降序处理:先匹配长的(更具体),命中则其包含的短词不再计分。
148
+ const sorted = [...KEYWORDS[profile]].sort((a, b) => b.text.length - a.text.length);
149
+ let score = 0;
150
+ const hits = [];
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);
160
+ }
161
+ return { score, hits };
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
+ }
174
+ /**
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.
179
+ * - Returns a profile when its score clearly leads (≥ 2× runner-up, or score ≥ threshold).
180
+ * - Returns null when signals are weak/conflicting → caller should confirm with the user.
181
+ */
182
+ export function detectScene(text) {
183
+ const lower = text.toLowerCase();
184
+ const all = Object.keys(KEYWORDS).map((p) => ({
185
+ profile: p,
186
+ score: score(lower, p).score,
187
+ }));
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);
194
+ const reasons = [];
195
+ // Strong signal: clear winner.
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
+ }
204
+ const confidence = Math.min(1, best.score / (best.score + runner.score + 1));
205
+ reasons.push(`匹配「${winner}」关键词 ${best.score} 分(次优 ${runner.profile} ${runner.score} 分)`);
206
+ return { profile: winner, confidence, candidates, reasons };
207
+ }
208
+ // Conflicting/weak → uncertain.
209
+ if (total === 0) {
210
+ reasons.push('未检测到明显场景信号,需向用户确认');
211
+ }
212
+ else {
213
+ reasons.push(`场景信号较弱或冲突(${base.map((s) => `${s.profile}:${s.score}`).join(', ')}),需确认`);
214
+ }
215
+ return { profile: null, confidence: 0, candidates, reasons };
216
+ }
217
+ /** The user-facing prompt when detection is uncertain. */
218
+ export function confirmationPrompt(result, text) {
219
+ const lines = [
220
+ `未能明确判断你的需求属于哪个场景:`,
221
+ `「${text.slice(0, 60)}${text.length > 60 ? '…' : ''}」`,
222
+ '',
223
+ `检测到的候选:`,
224
+ ...result.candidates
225
+ .filter((c) => c.score > 0)
226
+ .map((c) => `- ${c.profile}: ${c.score} 分`),
227
+ '',
228
+ `请选择场景(或直接输入新的需求描述):`,
229
+ `- 0to1 → 从零启动一个产品/项目(全栈;纯前端自动走 frontend-0to1)`,
230
+ `- frontend-0to1 → 从零启动一个纯前端项目(无后端/仅页面与样式)`,
231
+ `- bugfix → 排查并修复一个问题(含前端/多端维度,按输入特征伸缩)`,
232
+ `- feature → 给已有系统新增功能(按输入特征伸缩:纯逻辑/含后端/含前端/全栈)`,
233
+ `- poc → 技术预研/选型(新技术评估/PoC/方案对比)`,
234
+ `- migration → 技术升级(框架迁移/大版本/构建工具升级)`,
235
+ ];
236
+ return lines.join('\n');
237
+ }