@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,28 @@
1
+ /**
2
+ * Document Engine — injectable LLM interface
3
+ *
4
+ * The engine does NOT bind to any specific LLM SDK. Consumers provide a
5
+ * `DocumentLLM` implementation. The engine only ever calls `complete`; token
6
+ * estimation for budget checks is done via `estimateTokens` (character-based).
7
+ */
8
+ export interface LLMMessage {
9
+ role: 'system' | 'user' | 'assistant';
10
+ content: string;
11
+ }
12
+ export interface LLMInput {
13
+ system?: string;
14
+ messages: LLMMessage[];
15
+ }
16
+ export interface LLMOutput {
17
+ content: string;
18
+ }
19
+ export interface DocumentLLM {
20
+ /** Generic completion — returns raw text. */
21
+ complete(input: LLMInput): Promise<LLMOutput>;
22
+ }
23
+ /**
24
+ * Default character-based token estimate.
25
+ * NOTE: under-counts Chinese (~1 char ≈ 1 token vs len/4); budget gates are
26
+ * intentionally lenient.
27
+ */
28
+ export declare function estimateTokens(text: string): number;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Document Engine — injectable LLM interface
3
+ *
4
+ * The engine does NOT bind to any specific LLM SDK. Consumers provide a
5
+ * `DocumentLLM` implementation. The engine only ever calls `complete`; token
6
+ * estimation for budget checks is done via `estimateTokens` (character-based).
7
+ */
8
+ /**
9
+ * Default character-based token estimate.
10
+ * NOTE: under-counts Chinese (~1 char ≈ 1 token vs len/4); budget gates are
11
+ * intentionally lenient.
12
+ */
13
+ export function estimateTokens(text) {
14
+ if (!text)
15
+ return 0;
16
+ // Count CJK chars at 1 token/char, others at ~4 chars/token.
17
+ let tokens = 0;
18
+ for (const ch of text) {
19
+ tokens += /[\u2E80-\u9FFF\uF900-\uFAFF\uFF00-\uFFEF\u3000-\u303F]/.test(ch) ? 1 : 0.25;
20
+ }
21
+ return Math.max(1, Math.ceil(tokens));
22
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Document Engine — Pass 2 per-chapter fill-in (decisions 6/8/9).
3
+ *
4
+ * Four-layer context injection per chapter:
5
+ * ① outline points + global entities/decisions
6
+ * ② dependency digests (navigation)
7
+ * ③ upstream contract entities injected deterministically (registry + referenced full contracts)
8
+ * ④ source segments (original input by source_segments)
9
+ *
10
+ * Fills by point kind: entity/mixed → structured contract entities; narrative/mixed → Markdown.
11
+ * Output budget enforced per chapter (decision 6/P6). fill_once falls back to per-chapter when
12
+ * the estimated input context exceeds the budget (decision 9).
13
+ */
14
+ import { Outline, OutlineChapter, Entities, ChapterComponent } from './schemas.js';
15
+ import type { DocumentLLM, LLMInput } from './llm.js';
16
+ export interface ChapterFill {
17
+ chapterId: string;
18
+ narrative: string;
19
+ entities: Entities;
20
+ tokens: number;
21
+ }
22
+ export interface MapContext {
23
+ outline: Outline;
24
+ digests: Map<string, string>;
25
+ upstreamEntities: Entities;
26
+ workRoot: string;
27
+ chapterById: Map<string, ChapterComponent>;
28
+ /** buildChapterContext 结果缓存(P2):fill_once/shouldFillOnce/回退/review 重填会重复
29
+ * 构建同一章节上下文(含重复 fetchSegments 整文件 IO),按 chapterId 缓存消除。 */
30
+ contextCache?: Map<string, {
31
+ input: LLMInput;
32
+ estimatedTokens: number;
33
+ }>;
34
+ }
35
+ export declare function buildChapterContext(chapter: OutlineChapter, ctx: MapContext): Promise<{
36
+ input: LLMInput;
37
+ estimatedTokens: number;
38
+ }>;
39
+ export interface FillChapterResult {
40
+ ok: boolean;
41
+ fill?: ChapterFill;
42
+ error?: string;
43
+ }
44
+ export declare function fillChapter(llm: DocumentLLM, chapter: OutlineChapter, ctx: MapContext, budgetTokens?: number): Promise<FillChapterResult>;
45
+ export declare function shouldFillOnce(outline: Outline, ctx: MapContext): Promise<boolean>;
46
+ export interface FillOnceResult {
47
+ ok: boolean;
48
+ chapters: ChapterFill[];
49
+ fellBackToPerChapter: boolean;
50
+ error?: string;
51
+ }
52
+ export declare function fillOnce(llm: DocumentLLM, outline: Outline, ctx: MapContext): Promise<FillOnceResult>;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Document Engine — Pass 2 per-chapter fill-in (decisions 6/8/9).
3
+ *
4
+ * Four-layer context injection per chapter:
5
+ * ① outline points + global entities/decisions
6
+ * ② dependency digests (navigation)
7
+ * ③ upstream contract entities injected deterministically (registry + referenced full contracts)
8
+ * ④ source segments (original input by source_segments)
9
+ *
10
+ * Fills by point kind: entity/mixed → structured contract entities; narrative/mixed → Markdown.
11
+ * Output budget enforced per chapter (decision 6/P6). fill_once falls back to per-chapter when
12
+ * the estimated input context exceeds the budget (decision 9).
13
+ */
14
+ import { parseEntities, parseInterfaceEntity, parseTableEntity, parseDecisionEntity } from './schemas.js';
15
+ import { estimateTokens } from './llm.js';
16
+ import { fetchSegments } from './input-digest.js';
17
+ const MAP_SYSTEM = '你是方案文档撰写员。按给定章节要点"填空题"式展开:逐要点填充,不自由发挥。' +
18
+ 'kind=entity 或 mixed 的要点 → 产出结构化契约实体(JSON);kind=narrative 或 mixed 的要点 → 产出叙述 Markdown。' +
19
+ '契约实体必须满足字段约束(接口须含 failure_examples,表须含 ddl/migration/rollback)。';
20
+ // ============= Four-layer context build (T6.2) =============
21
+ export async function buildChapterContext(chapter, ctx) {
22
+ // 缓存命中直接返回(P2:同一章节上下文在 fill_once 决策/执行/回退/review 重填中会被多次构建)
23
+ if (ctx.contextCache) {
24
+ const cached = ctx.contextCache.get(chapter.id);
25
+ if (cached)
26
+ return cached;
27
+ }
28
+ const { outline, digests, upstreamEntities, workRoot } = ctx;
29
+ // ① outline points + global entities/decisions
30
+ const chapterPoints = chapter.points.map((p) => `${p.id} [${p.kind}] ${p.text}${(p.references ?? []).length ? ` refs=${(p.references ?? []).join(',')}` : ''}`).join('\n');
31
+ const globalEntities = outline.entities.map((e) => `${e.id} (${e.frozen}${e.standalone ? ', standalone' : ''})`).join(', ');
32
+ // ② dependency digests
33
+ const deps = [];
34
+ const chComp = ctx.chapterById.get(chapter.id);
35
+ for (const dep of chComp?.depends_on ?? []) {
36
+ const d = digests.get(dep);
37
+ if (d)
38
+ deps.push(`[依赖 ${dep}] ${d}`);
39
+ }
40
+ // ③ upstream contract entities: registry (all) + full contracts for referenced ids
41
+ const referencedIds = new Set();
42
+ for (const p of chapter.points)
43
+ for (const r of p.references ?? [])
44
+ referencedIds.add(r);
45
+ const registry = [
46
+ ...upstreamEntities.interfaces.map((i) => `I ${i.id}: ${i.name}`),
47
+ ...upstreamEntities.tables.map((t) => `T ${t.id}: ${t.name}`),
48
+ ].join('\n');
49
+ const fullContracts = [
50
+ ...upstreamEntities.interfaces.filter((i) => referencedIds.has(i.id)),
51
+ ...upstreamEntities.tables.filter((t) => referencedIds.has(t.id)),
52
+ ].map((e) => JSON.stringify(e)).join('\n');
53
+ // ④ source segments
54
+ const segmentIds = new Set();
55
+ for (const p of chapter.points)
56
+ for (const s of p.source_segments ?? [])
57
+ segmentIds.add(s);
58
+ const segments = segmentIds.size > 0 ? await fetchSegments(workRoot, [...segmentIds]) : '';
59
+ const content = [
60
+ `## 章节 ${chapter.id}(${chapter.title})`,
61
+ `### 要点清单(填空)`,
62
+ chapterPoints,
63
+ `### 全局实体/决策`,
64
+ globalEntities,
65
+ deps.length ? `### 依赖摘要\n${deps.join('\n\n')}` : '',
66
+ registry ? `### 上游契约实体注册表\n${registry}` : '',
67
+ fullContracts ? `### 上游契约实体完整契约(引用)\n${fullContracts}` : '',
68
+ segments ? `### 本片片段(原文)\n${segments}` : '',
69
+ ].filter(Boolean).join('\n\n');
70
+ const input = {
71
+ system: MAP_SYSTEM,
72
+ messages: [{ role: 'user', content }],
73
+ };
74
+ const built = { input, estimatedTokens: estimateTokens(content) + estimateTokens(MAP_SYSTEM) };
75
+ if (ctx.contextCache)
76
+ ctx.contextCache.set(chapter.id, built);
77
+ return built;
78
+ }
79
+ export async function fillChapter(llm, chapter, ctx, budgetTokens) {
80
+ let input;
81
+ let estimatedTokens;
82
+ try {
83
+ const built = await buildChapterContext(chapter, ctx);
84
+ input = built.input;
85
+ estimatedTokens = built.estimatedTokens;
86
+ }
87
+ catch (e) {
88
+ return { ok: false, error: `Chapter "${chapter.id}" context build failed: ${e instanceof Error ? e.message : String(e)}` };
89
+ }
90
+ // Input-side budget guard(P2-adjacent):在 LLM 调用之前拒绝明显超预算输入,
91
+ // 避免昂贵调用已发出后才失败(decision 6/P6 hard gate)。
92
+ const maxInputTokens = budgetTokens ? budgetTokens * 4 : undefined;
93
+ if (maxInputTokens !== undefined && estimatedTokens > maxInputTokens) {
94
+ return { ok: false, error: `Chapter "${chapter.id}" input context ${estimatedTokens} tokens exceeds 4× output budget` };
95
+ }
96
+ let res;
97
+ try {
98
+ res = await llm.complete(input);
99
+ }
100
+ catch (e) {
101
+ return { ok: false, error: `Chapter "${chapter.id}" LLM call failed: ${e instanceof Error ? e.message : String(e)}` };
102
+ }
103
+ const narrative = res.content.trim();
104
+ // Extract embedded contract entities (JSON blocks). Strict: if this chapter's points require
105
+ // entities (kind=entity|mixed with references) and none are extracted, fail loudly rather than
106
+ // silently produce a contract-less chapter (decision 1 layered source of truth).
107
+ let entities = { interfaces: [], tables: [], decisions: [] };
108
+ const jsonMatch = narrative.match(/```json\s*([\s\S]*?)```/g);
109
+ // 记录成功解析为实体块的围栏文本:清洗时只移除这些块,保留叙述中的 JSON 示例(P2)。
110
+ const parsedEntityBlocks = [];
111
+ if (jsonMatch) {
112
+ // 按 id 去重合并(P1-9):同一实体跨块/跨引用重复输出时只保留最后出现的版本,
113
+ // 防止共享实体在 entities.json 与最终文档中重复渲染。
114
+ const merged = { interfaces: [], tables: [], decisions: [] };
115
+ const seenInterface = new Set();
116
+ const seenTable = new Set();
117
+ const seenDecision = new Set();
118
+ for (const block of jsonMatch) {
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(block.replace(/```json\s*/, '').replace(/```$/, '').trim());
122
+ }
123
+ catch {
124
+ continue; // 非法 JSON → 非实体块,忽略
125
+ }
126
+ if (!parsed || typeof parsed !== 'object' || !parsed.entities)
127
+ continue;
128
+ const rawEntities = parsed.entities;
129
+ // 逐实体容错(P2):单个实体字段不合 zod(如接口缺 failure_examples)不再丢弃同块其它
130
+ // 合法实体;非法项跳过并继续,尽量保留有效契约。
131
+ const e = { interfaces: [], tables: [], decisions: [] };
132
+ for (const it of rawEntities.interfaces ?? []) {
133
+ try {
134
+ e.interfaces.push(parseInterfaceEntity(it));
135
+ }
136
+ catch { /* 跳过非法接口 */ }
137
+ }
138
+ for (const tb of rawEntities.tables ?? []) {
139
+ try {
140
+ e.tables.push(parseTableEntity(tb));
141
+ }
142
+ catch { /* 跳过非法表 */ }
143
+ }
144
+ for (const d of rawEntities.decisions ?? []) {
145
+ try {
146
+ e.decisions.push(parseDecisionEntity(d));
147
+ }
148
+ catch { /* 跳过非法决策 */ }
149
+ }
150
+ if (e.interfaces.length + e.tables.length + e.decisions.length === 0)
151
+ continue;
152
+ parsedEntityBlocks.push(block);
153
+ for (const it of e.interfaces) {
154
+ if (!seenInterface.has(it.id)) {
155
+ seenInterface.add(it.id);
156
+ merged.interfaces.push(it);
157
+ }
158
+ }
159
+ for (const tb of e.tables) {
160
+ if (!seenTable.has(tb.id)) {
161
+ seenTable.add(tb.id);
162
+ merged.tables.push(tb);
163
+ }
164
+ }
165
+ for (const d of e.decisions) {
166
+ if (!seenDecision.has(d.id)) {
167
+ seenDecision.add(d.id);
168
+ merged.decisions.push(d);
169
+ }
170
+ }
171
+ }
172
+ entities = merged;
173
+ }
174
+ // Clean the narrative: remove ONLY the successfully-parsed ```json entity blocks so they are
175
+ // not rendered verbatim inside chapters; narrative JSON examples (e.g. API response samples)
176
+ // are preserved (P2). Contract entities are rendered deterministically in the 契约 section.
177
+ let cleanNarrative = narrative;
178
+ for (const block of parsedEntityBlocks) {
179
+ cleanNarrative = cleanNarrative.replace(block, '');
180
+ }
181
+ cleanNarrative = cleanNarrative.replace(/\n{3,}/g, '\n\n').trim();
182
+ // 空叙述早失败(P2):LLM 只输出实体块未产出叙述时立即报错,与 fillOnce 的检查对齐,
183
+ // 避免拖到管线末尾 gates-fail 才暴露。
184
+ if (cleanNarrative.length === 0) {
185
+ return { ok: false, error: `Chapter "${chapter.id}" produced no narrative (LLM returned only entity JSON or empty content)` };
186
+ }
187
+ // Strict: entity-requiring points must have their referenced entities filled.
188
+ const requiredEntityIds = new Set();
189
+ for (const p of chapter.points) {
190
+ if ((p.kind === 'entity' || p.kind === 'mixed') && (p.references ?? []).length > 0) {
191
+ for (const ref of p.references)
192
+ requiredEntityIds.add(ref);
193
+ }
194
+ }
195
+ const filledEntityIds = new Set([
196
+ ...entities.interfaces.map((i) => i.id),
197
+ ...entities.tables.map((t) => t.id),
198
+ ...entities.decisions.map((d) => d.id),
199
+ ]);
200
+ for (const id of requiredEntityIds) {
201
+ if (!filledEntityIds.has(id)) {
202
+ return { ok: false, error: `Chapter "${chapter.id}" requires entity "${id}" (point references) but none was extracted` };
203
+ }
204
+ }
205
+ const tokens = estimateTokens(cleanNarrative) + estimateTokens(JSON.stringify(entities));
206
+ if (budgetTokens !== undefined && tokens > budgetTokens) {
207
+ return { ok: false, error: `Chapter "${chapter.id}" output ${tokens} tokens exceeds budget ${budgetTokens}` };
208
+ }
209
+ return {
210
+ ok: true,
211
+ fill: { chapterId: chapter.id, narrative: cleanNarrative, entities, tokens },
212
+ };
213
+ }
214
+ // ============= fill_once decision (T6.5/T6.6) =============
215
+ export async function shouldFillOnce(outline, ctx) {
216
+ if (!outline.fill_once?.enabled)
217
+ return false;
218
+ const budget = outline.fill_once.input_budget_tokens ?? 8000;
219
+ let total = 0;
220
+ try {
221
+ for (const ch of outline.chapters) {
222
+ const { estimatedTokens } = await buildChapterContext(ch, ctx);
223
+ total += estimatedTokens;
224
+ }
225
+ }
226
+ catch {
227
+ return false; // 上下文构建失败 → 回退 per-chapter(P2)
228
+ }
229
+ return total <= budget;
230
+ }
231
+ const FILL_ONCE_SYSTEM = '你是方案文档撰写员。一次填完全部章节,输出结构化 JSON:' +
232
+ '{"chapters":{"<chapterId>":{"narrative":"本章叙述 Markdown","entities":{"interfaces":[...],"tables":[...],"decisions":[...]}},...}}。' +
233
+ '每章内容必须差异化(针对该章要点),禁止复制同一段到所有章节。契约实体须含 failure_examples(接口)/ddl(表)。';
234
+ export async function fillOnce(llm, outline, ctx) {
235
+ // Build a single combined context (all chapters) — bounded by shouldFillOnce pre-check.
236
+ // try/catch(P2):上下文构建与 LLM 调用任一抛错都应回退 per-chapter,而不是穿透崩溃。
237
+ let res;
238
+ try {
239
+ const sections = [];
240
+ for (const ch of outline.chapters) {
241
+ const { input } = await buildChapterContext(ch, ctx);
242
+ sections.push(input.messages[0].content);
243
+ }
244
+ res = await llm.complete({
245
+ system: FILL_ONCE_SYSTEM,
246
+ messages: [{ role: 'user', content: sections.join('\n\n=====\n\n') }],
247
+ });
248
+ }
249
+ catch (e) {
250
+ return {
251
+ ok: false,
252
+ chapters: [],
253
+ fellBackToPerChapter: true,
254
+ error: `fill_once: context build or LLM call failed (${e instanceof Error ? e.message : String(e)})`,
255
+ };
256
+ }
257
+ const content = res.content.trim();
258
+ // Parse structured per-chapter output; fall back to per-chapter fill on parse failure.
259
+ try {
260
+ const parsed = JSON.parse(content);
261
+ if (!parsed.chapters || typeof parsed.chapters !== 'object') {
262
+ return { ok: false, chapters: [], fellBackToPerChapter: true, error: 'fill_once: missing chapters object' };
263
+ }
264
+ const chapters = [];
265
+ for (const ch of outline.chapters) {
266
+ const entry = parsed.chapters[ch.id];
267
+ if (!entry || typeof entry.narrative !== 'string' || entry.narrative.trim().length === 0) {
268
+ return { ok: false, chapters: [], fellBackToPerChapter: true, error: `fill_once: chapter ${ch.id} missing narrative` };
269
+ }
270
+ let entities = { interfaces: [], tables: [], decisions: [] };
271
+ try {
272
+ if (entry.entities)
273
+ entities = parseEntities(entry.entities);
274
+ }
275
+ catch {
276
+ entities = { interfaces: [], tables: [], decisions: [] };
277
+ }
278
+ const narrative = entry.narrative.trim();
279
+ chapters.push({
280
+ chapterId: ch.id,
281
+ narrative,
282
+ entities,
283
+ tokens: estimateTokens(narrative) + estimateTokens(JSON.stringify(entities)),
284
+ });
285
+ }
286
+ return { ok: true, chapters, fellBackToPerChapter: false };
287
+ }
288
+ catch (e) {
289
+ return {
290
+ ok: false,
291
+ chapters: [],
292
+ fellBackToPerChapter: true,
293
+ error: `fill_once: unparsable output (${e instanceof Error ? e.message : String(e)})`,
294
+ };
295
+ }
296
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Document Engine — Pass 1 outline generation + lint + freeze (decisions 6/8/9).
3
+ *
4
+ * Generates the Outline from the global input digest + profile constraints,
5
+ * lints it (required/forbidden, references forward + reverse, kind consistency),
6
+ * then freezes to work/outline.yaml.
7
+ */
8
+ import { Outline, ScenarioProfile } from './schemas.js';
9
+ import type { DocumentLLM } from './llm.js';
10
+ import { GlobalInputDigest } from './input-digest.js';
11
+ import { ExtractedContracts } from './extract.js';
12
+ export interface OutlineDiagnostic {
13
+ code: string;
14
+ severity: 'error' | 'warning';
15
+ message: string;
16
+ chapter?: string;
17
+ }
18
+ export interface GenerateOutlineOptions {
19
+ profile: ScenarioProfile;
20
+ inputDigest: GlobalInputDigest;
21
+ prompt?: string;
22
+ /** 工程契约候选(grounding P0 确定性提取):冻结实体名供大纲注册表引用,禁止改名/编造。 */
23
+ extractedContracts?: ExtractedContracts;
24
+ }
25
+ export declare function generateOutline(llm: DocumentLLM, options: GenerateOutlineOptions): Promise<Outline>;
26
+ export declare function lintOutline(outline: Outline, profile: ScenarioProfile): OutlineDiagnostic[];
27
+ export declare function outlineHasErrors(diags: OutlineDiagnostic[]): boolean;
28
+ export declare function freezeOutline(workRoot: string, outline: Outline): Promise<string>;
29
+ export declare function readOutline(workRoot: string): Promise<Outline>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Document Engine — Pass 1 outline generation + lint + freeze (decisions 6/8/9).
3
+ *
4
+ * Generates the Outline from the global input digest + profile constraints,
5
+ * lints it (required/forbidden, references forward + reverse, kind consistency),
6
+ * then freezes to work/outline.yaml.
7
+ */
8
+ import { promises as fs } from 'node:fs';
9
+ import yaml from 'js-yaml';
10
+ import { parseOutline } from './schemas.js';
11
+ const DEFAULT_PROMPT = '你是文档大纲设计专家。基于全局输入摘要与场景 Profile 约束,生成结构化文档大纲。见 prompts/document/outline/general.md。';
12
+ const OUTLINE_SYSTEM = '你是文档大纲设计专家。严格按输入要求输出 JSON 大纲,满足所有硬规则(章节约束/要点 kind/references 完整性/source_segments)。';
13
+ // ============= Generation (T5.2) =============
14
+ /** 剥离 LLM 输出中的 ```json 围栏与前后缀文本(P2:与 map.ts 的围栏处理一致)。 */
15
+ function extractJsonBlock(content) {
16
+ const trimmed = content.trim();
17
+ const fence = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
18
+ if (fence)
19
+ return fence[1].trim();
20
+ // 无围栏:尝试取首个平衡 {...} 块(容忍前后缀说明文字)。
21
+ const start = trimmed.indexOf('{');
22
+ const end = trimmed.lastIndexOf('}');
23
+ if (start >= 0 && end > start)
24
+ return trimmed.slice(start, end + 1);
25
+ return trimmed;
26
+ }
27
+ export async function generateOutline(llm, options) {
28
+ const { profile, inputDigest } = options;
29
+ const prompt = options.prompt ?? DEFAULT_PROMPT;
30
+ const extracted = options.extractedContracts;
31
+ // Grounding 注入:工程契约候选(事实)。大纲注册表必须原样引用这些 frozen 名,
32
+ // 禁止改名/编造;无提取时该段省略(行为与旧版一致)。
33
+ const extractedBlock = extracted && (extracted.interfaces.length > 0 || extracted.tables.length > 0)
34
+ ? [
35
+ `## 工程契约候选(确定性提取,事实)`,
36
+ `以下实体来自工程数据源(OpenAPI/proto/DDL),是已确认的事实:`,
37
+ ...extracted.interfaces.map((it) => `- ${it.id} (${it.method} ${it.path}) name=${it.name}${it.request_fields?.length ? ` request=[${it.request_fields.map((f) => `${f.name}:${f.type}`).join(', ')}]` : ''}${it.response_fields?.length ? ` response=[${it.response_fields.map((f) => `${f.name}:${f.type}`).join(', ')}]` : ''}`),
38
+ ...extracted.tables.map((tb) => `- ${tb.id} (table) name=${tb.name} fields=[${tb.fields?.map((f) => `${f.name}:${f.type}`).join(', ') ?? ''}]`),
39
+ `规则:大纲 entities 注册表引用这些实体时,frozen 必须使用上述 name;references 用上述 id;禁止新增同名异义实体。`,
40
+ ].join('\n')
41
+ : '';
42
+ const input = {
43
+ system: OUTLINE_SYSTEM,
44
+ messages: [
45
+ { role: 'user', content: prompt },
46
+ {
47
+ role: 'user',
48
+ content: [
49
+ `Profile: ${JSON.stringify(profile)}`,
50
+ `全局输入摘要: ${inputDigest.summary}`,
51
+ `段摘要: ${inputDigest.segments.map((s) => `[${s.id}] ${s.summary}`).join('\n')}`,
52
+ extractedBlock,
53
+ ].filter(Boolean).join('\n\n'),
54
+ },
55
+ ],
56
+ };
57
+ // Retry up to 2 times on PARSE failure only(P2):LLM 服务/传输错误(限流/5xx)立即抛出,
58
+ // 不背靠背重试放大延迟与成本;确定性解析错误才重试。
59
+ let lastParseErr;
60
+ for (let attempt = 0; attempt < 3; attempt++) {
61
+ let res;
62
+ try {
63
+ res = await llm.complete(input);
64
+ }
65
+ catch (e) {
66
+ throw new Error(`generateOutline: LLM call failed: ${e instanceof Error ? e.message : String(e)}`);
67
+ }
68
+ try {
69
+ const outline = parseOutline(JSON.parse(extractJsonBlock(res.content)));
70
+ return outline;
71
+ }
72
+ catch (e) {
73
+ lastParseErr = e;
74
+ }
75
+ }
76
+ throw new Error(`generateOutline: failed to parse outline after retries: ${lastParseErr}`);
77
+ }
78
+ // ============= Lint (T5.3) =============
79
+ export function lintOutline(outline, profile) {
80
+ const diags = [];
81
+ // 1. Required chapters all present.
82
+ for (const id of profile.required) {
83
+ if (!outline.chapters.some((c) => c.id === id)) {
84
+ diags.push({ code: 'required_missing', severity: 'error', message: `Required chapter "${id}" missing from outline` });
85
+ }
86
+ }
87
+ // 2. Forbidden chapters absent.
88
+ for (const id of profile.forbidden) {
89
+ if (outline.chapters.some((c) => c.id === id)) {
90
+ diags.push({ code: 'forbidden_present', severity: 'error', chapter: id, message: `Forbidden chapter "${id}" present in outline` });
91
+ }
92
+ }
93
+ // 3. Duplicate chapter ids.
94
+ const seenCh = new Set();
95
+ for (const ch of outline.chapters) {
96
+ if (seenCh.has(ch.id))
97
+ diags.push({ code: 'duplicate_chapter', severity: 'error', chapter: ch.id, message: `Duplicate chapter "${ch.id}"` });
98
+ seenCh.add(ch.id);
99
+ }
100
+ // 4. Duplicate point ids within a chapter.
101
+ for (const ch of outline.chapters) {
102
+ const seenP = new Set();
103
+ for (const p of ch.points) {
104
+ if (seenP.has(p.id))
105
+ diags.push({ code: 'duplicate_point', severity: 'error', chapter: ch.id, message: `Duplicate point "${p.id}"` });
106
+ seenP.add(p.id);
107
+ }
108
+ }
109
+ // 4b. Duplicate entity ids in registry(P2):重复注册会被 Set 静默折叠,正向引用校验失效。
110
+ const seenEntity = new Set();
111
+ for (const e of outline.entities) {
112
+ if (seenEntity.has(e.id))
113
+ diags.push({ code: 'duplicate_entity', severity: 'error', message: `Duplicate entity "${e.id}" in registry` });
114
+ seenEntity.add(e.id);
115
+ }
116
+ const entityIds = new Set(outline.entities.map((e) => e.id));
117
+ // 5. References forward: point references must exist in entity registry.
118
+ // 6. Kind consistency: references present → kind must not be narrative; ≤3 unique refs per point.
119
+ for (const ch of outline.chapters) {
120
+ for (const p of ch.points) {
121
+ const refs = [...new Set(p.references ?? [])]; // 去重(P2:重复引用不误触 ≤3 门禁)
122
+ if (refs.length > 3) {
123
+ diags.push({ code: 'too_many_references', severity: 'error', chapter: ch.id, message: `Point "${p.id}" references >3 entities` });
124
+ }
125
+ if ((p.references ?? []).length !== refs.length) {
126
+ diags.push({ code: 'duplicate_reference', severity: 'warning', chapter: ch.id, message: `Point "${p.id}" has duplicate references` });
127
+ }
128
+ for (const ref of refs) {
129
+ if (!entityIds.has(ref)) {
130
+ diags.push({ code: 'reference_unknown', severity: 'error', chapter: ch.id, message: `Point "${p.id}" references unknown entity "${ref}"` });
131
+ }
132
+ }
133
+ if (refs.length > 0 && p.kind === 'narrative') {
134
+ diags.push({ code: 'kind_inconsistent', severity: 'error', chapter: ch.id, message: `Point "${p.id}" has references but kind=narrative` });
135
+ }
136
+ }
137
+ }
138
+ // 7. References reverse (decision 9): every non-standalone entity must be referenced by ≥1 point.
139
+ const referenced = new Set();
140
+ for (const ch of outline.chapters) {
141
+ for (const p of ch.points) {
142
+ for (const ref of p.references ?? [])
143
+ referenced.add(ref);
144
+ }
145
+ }
146
+ for (const ent of outline.entities) {
147
+ if (!ent.standalone && !referenced.has(ent.id)) {
148
+ diags.push({ code: 'entity_unreferenced', severity: 'error', message: `Entity "${ent.id}" not referenced by any point (mark standalone to exempt)` });
149
+ }
150
+ }
151
+ // 8. Every chapter must have ≥1 point.
152
+ for (const ch of outline.chapters) {
153
+ if (ch.points.length === 0) {
154
+ diags.push({ code: 'chapter_no_points', severity: 'error', chapter: ch.id, message: `Chapter "${ch.id}" has no points` });
155
+ }
156
+ }
157
+ return diags;
158
+ }
159
+ export function outlineHasErrors(diags) {
160
+ return diags.some((d) => d.severity === 'error');
161
+ }
162
+ // ============= Freeze (T5.4/T5.5) =============
163
+ export async function freezeOutline(workRoot, outline) {
164
+ const path = `${workRoot}/outline.yaml`;
165
+ await fs.writeFile(path, yaml.dump(outline, { noRefs: true }), 'utf-8');
166
+ return path;
167
+ }
168
+ export async function readOutline(workRoot) {
169
+ const raw = yaml.load(await fs.readFile(`${workRoot}/outline.yaml`, 'utf-8'));
170
+ return parseOutline(raw);
171
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Document Engine — injectable path resolution.
3
+ *
4
+ * Replaces ACP's hardcoded `specflow/changes` + `validateChangeName`. The
5
+ * caller supplies a `workRoot`; all artifact paths derive from it.
6
+ */
7
+ export interface DocumentPaths {
8
+ workRoot: string;
9
+ outline: string;
10
+ entities: string;
11
+ chaptersDir: string;
12
+ documentMd: string;
13
+ documentHtml: string;
14
+ }
15
+ /** Resolve all engine artifact paths under a caller-provided work root. */
16
+ export declare function resolvePaths(workRoot: string): DocumentPaths;
17
+ /** 章节 id 白名单:仅允许安全标识符,拒绝路径分隔符/`..`/空串(防路径穿越,P0-3)。 */
18
+ export declare const CHAPTER_ID_RE: RegExp;
19
+ export declare function assertChapterId(chapterId: string): void;
20
+ export declare function chapterPath(workRoot: string, chapterId: string): string;
21
+ /** Multi-repo synthesize marker: list of repo names (written by document synthesize). */
22
+ export declare function reposPath(workRoot: string): string;
23
+ export declare function segmentIndexPath(workRoot: string): string;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Document Engine — injectable path resolution.
3
+ *
4
+ * Replaces ACP's hardcoded `specflow/changes` + `validateChangeName`. The
5
+ * caller supplies a `workRoot`; all artifact paths derive from it.
6
+ */
7
+ import { join } from 'node:path';
8
+ /** Resolve all engine artifact paths under a caller-provided work root. */
9
+ export function resolvePaths(workRoot) {
10
+ return {
11
+ workRoot,
12
+ outline: join(workRoot, 'outline.yaml'),
13
+ entities: join(workRoot, 'entities.json'),
14
+ chaptersDir: join(workRoot, 'chapters'),
15
+ documentMd: join(workRoot, 'document.md'),
16
+ documentHtml: join(workRoot, 'document.html'),
17
+ };
18
+ }
19
+ /** 章节 id 白名单:仅允许安全标识符,拒绝路径分隔符/`..`/空串(防路径穿越,P0-3)。 */
20
+ export const CHAPTER_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
21
+ export function assertChapterId(chapterId) {
22
+ if (!CHAPTER_ID_RE.test(chapterId)) {
23
+ throw new Error(`Invalid chapter id "${chapterId}": must match ${CHAPTER_ID_RE}`);
24
+ }
25
+ }
26
+ export function chapterPath(workRoot, chapterId) {
27
+ assertChapterId(chapterId);
28
+ return join(workRoot, 'chapters', `${chapterId}.md`);
29
+ }
30
+ /** Multi-repo synthesize marker: list of repo names (written by document synthesize). */
31
+ export function reposPath(workRoot) {
32
+ return join(workRoot, 'repos.json');
33
+ }
34
+ export function segmentIndexPath(workRoot) {
35
+ return join(workRoot, 'segments', 'index.json');
36
+ }