@gordon.gan/specflow 1.7.0-beta → 1.8.0-alpha

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 (151) hide show
  1. package/README.md +4 -4
  2. package/dist/cli/commands/document-run.d.ts +84 -0
  3. package/dist/cli/commands/document-run.js +539 -0
  4. package/dist/cli/index.js +2 -2
  5. package/dist/core/artifact-language.js +11 -1
  6. package/dist/core/document/chapters.d.ts +6 -0
  7. package/dist/core/document/chapters.js +21 -0
  8. package/dist/core/document/coverage.d.ts +25 -0
  9. package/dist/core/document/coverage.js +49 -0
  10. package/dist/core/document/digests.d.ts +41 -0
  11. package/dist/core/document/digests.js +136 -0
  12. package/dist/core/document/engine.d.ts +92 -0
  13. package/dist/core/document/engine.js +487 -0
  14. package/dist/core/{approval → document}/forbidden-patterns.d.ts +4 -2
  15. package/dist/core/document/forbidden-patterns.js +33 -0
  16. package/dist/core/document/gates.d.ts +27 -0
  17. package/dist/core/document/gates.js +33 -0
  18. package/dist/core/document/index.d.ts +7 -0
  19. package/dist/core/document/index.js +7 -0
  20. package/dist/core/document/input-digest.d.ts +55 -0
  21. package/dist/core/document/input-digest.js +121 -0
  22. package/dist/core/document/input-features.d.ts +24 -0
  23. package/dist/core/document/input-features.js +112 -0
  24. package/dist/core/document/lint.d.ts +22 -0
  25. package/dist/core/document/lint.js +29 -0
  26. package/dist/core/document/llm.d.ts +35 -0
  27. package/dist/core/document/llm.js +27 -0
  28. package/dist/core/document/map.d.ts +46 -0
  29. package/dist/core/document/map.js +212 -0
  30. package/dist/core/document/outline.d.ts +26 -0
  31. package/dist/core/document/outline.js +128 -0
  32. package/dist/core/document/paths.d.ts +23 -0
  33. package/dist/core/document/paths.js +31 -0
  34. package/dist/core/document/profile-validator.d.ts +23 -0
  35. package/dist/core/document/profile-validator.js +127 -0
  36. package/dist/core/document/profiles.d.ts +23 -0
  37. package/dist/core/document/profiles.js +69 -0
  38. package/dist/core/document/render.d.ts +19 -0
  39. package/dist/core/document/render.js +97 -0
  40. package/dist/core/document/review.d.ts +42 -0
  41. package/dist/core/document/review.js +157 -0
  42. package/dist/core/document/scene-detect.d.ts +25 -0
  43. package/dist/core/document/scene-detect.js +121 -0
  44. package/dist/core/document/schemas.d.ts +949 -0
  45. package/dist/core/document/schemas.js +158 -0
  46. package/dist/integrations/shared/capability-evidence.js +4 -8
  47. package/dist/integrations/shared/command-catalog.js +1 -1
  48. package/dist/integrations/shared/parity-manifest.js +4 -8
  49. package/package.json +1 -1
  50. package/prompts/apply/phase-a-plan.md +1 -1
  51. package/prompts/document/map/acceptance.md +8 -0
  52. package/prompts/document/map/api-design.md +17 -0
  53. package/prompts/document/map/architecture.md +7 -0
  54. package/prompts/document/map/closed-loop.md +8 -0
  55. package/prompts/document/map/compat-migration.md +8 -0
  56. package/prompts/document/map/config-runtime.md +8 -0
  57. package/prompts/document/map/core-logic.md +8 -0
  58. package/prompts/document/map/data-model.md +17 -0
  59. package/prompts/document/map/deploy.md +8 -0
  60. package/prompts/document/map/fix.md +7 -0
  61. package/prompts/document/map/goal.md +7 -0
  62. package/prompts/document/map/impact.md +7 -0
  63. package/prompts/document/map/implementability.md +8 -0
  64. package/prompts/document/map/mvp-boundary.md +7 -0
  65. package/prompts/document/map/non-goals.md +7 -0
  66. package/prompts/document/map/regression.md +7 -0
  67. package/prompts/document/map/reproduce.md +7 -0
  68. package/prompts/document/map/requirement.md +7 -0
  69. package/prompts/document/map/root-cause.md +7 -0
  70. package/prompts/document/map/signoff.md +8 -0
  71. package/prompts/document/map/tech-selection.md +17 -0
  72. package/prompts/document/map/test-strategy.md +7 -0
  73. package/prompts/document/map/ui-design.md +18 -0
  74. package/prompts/document/outline/general.md +37 -0
  75. package/prompts/document/review/ai-review.md +56 -0
  76. package/prompts/explore/explore-session.md +1 -1
  77. package/prompts/propose/design-draft.md +1 -1
  78. package/prompts/propose/proposal.md +1 -1
  79. package/prompts/propose/specs.md +1 -1
  80. package/prompts/propose/tasks-draft.md +1 -1
  81. package/prompts/refine/design-output.md +1 -1
  82. package/prompts/refine/update-artifacts.md +2 -0
  83. package/prompts/shared/artifact-language.md +13 -0
  84. package/skills/specflow-document/SKILL.md +124 -0
  85. package/templates/document/chapters/acceptance.yaml +16 -0
  86. package/templates/document/chapters/api-design.yaml +24 -0
  87. package/templates/document/chapters/architecture.yaml +18 -0
  88. package/templates/document/chapters/closed-loop.yaml +22 -0
  89. package/templates/document/chapters/compat-migration.yaml +17 -0
  90. package/templates/document/chapters/config-runtime.yaml +16 -0
  91. package/templates/document/chapters/core-logic.yaml +16 -0
  92. package/templates/document/chapters/data-model.yaml +25 -0
  93. package/templates/document/chapters/deploy.yaml +18 -0
  94. package/templates/document/chapters/fix.yaml +18 -0
  95. package/templates/document/chapters/goal.yaml +17 -0
  96. package/templates/document/chapters/impact.yaml +16 -0
  97. package/templates/document/chapters/implementability.yaml +22 -0
  98. package/templates/document/chapters/mvp-boundary.yaml +20 -0
  99. package/templates/document/chapters/non-goals.yaml +17 -0
  100. package/templates/document/chapters/regression.yaml +16 -0
  101. package/templates/document/chapters/reproduce.yaml +16 -0
  102. package/templates/document/chapters/requirement.yaml +18 -0
  103. package/templates/document/chapters/root-cause.yaml +18 -0
  104. package/templates/document/chapters/signoff.yaml +15 -0
  105. package/templates/document/chapters/tech-selection.yaml +20 -0
  106. package/templates/document/chapters/test-strategy.yaml +20 -0
  107. package/templates/document/chapters/ui-design.yaml +22 -0
  108. package/templates/document/profiles/0to1.yaml +12 -0
  109. package/templates/document/profiles/approve.yaml +22 -0
  110. package/templates/document/profiles/bugfix.yaml +10 -0
  111. package/templates/document/profiles/feature.yaml +11 -0
  112. package/dist/cli/commands/approval-assemble.d.ts +0 -64
  113. package/dist/cli/commands/approval-assemble.js +0 -408
  114. package/dist/core/approval/assemble.d.ts +0 -10
  115. package/dist/core/approval/assemble.js +0 -384
  116. package/dist/core/approval/bundle.d.ts +0 -9
  117. package/dist/core/approval/bundle.js +0 -172
  118. package/dist/core/approval/forbidden-patterns.js +0 -37
  119. package/dist/core/approval/index-schema.d.ts +0 -676
  120. package/dist/core/approval/index-schema.js +0 -193
  121. package/dist/core/approval/index.d.ts +0 -12
  122. package/dist/core/approval/index.js +0 -9
  123. package/dist/core/approval/lint.d.ts +0 -10
  124. package/dist/core/approval/lint.js +0 -302
  125. package/dist/core/approval/paths.d.ts +0 -13
  126. package/dist/core/approval/paths.js +0 -43
  127. package/dist/core/approval/pipeline.d.ts +0 -28
  128. package/dist/core/approval/pipeline.js +0 -146
  129. package/dist/core/approval/playbook-schema.d.ts +0 -182
  130. package/dist/core/approval/playbook-schema.js +0 -51
  131. package/dist/core/approval/render.d.ts +0 -20
  132. package/dist/core/approval/render.js +0 -210
  133. package/dist/core/approval/review-pack.d.ts +0 -26
  134. package/dist/core/approval/review-pack.js +0 -205
  135. package/dist/core/approval/types.d.ts +0 -223
  136. package/dist/core/approval/types.js +0 -1
  137. package/prompts/approval/acp-pipeline.md +0 -104
  138. package/prompts/approval/ai-review.md +0 -145
  139. package/prompts/approval/api-guidance.md +0 -179
  140. package/prompts/approval/database-guidance.md +0 -105
  141. package/prompts/approval/frontend-guidance.md +0 -249
  142. package/prompts/approval/generate.md +0 -1583
  143. package/prompts/approval/multi-repo-guidance.md +0 -238
  144. package/prompts/approval/project-conventions-guidance.md +0 -171
  145. package/prompts/approval/runtime-guidance.md +0 -64
  146. package/prompts/approval/segmented-generation.md +0 -157
  147. package/skills/specflow-approval/SKILL.md +0 -663
  148. package/templates/approval-index.yaml +0 -93
  149. package/templates/approval-part.md +0 -15
  150. package/templates/approval-playbook.yaml +0 -28
  151. package/templates/approval.md +0 -261
@@ -0,0 +1,487 @@
1
+ /**
2
+ * Document Engine — unified orchestration entry (T11.2).
3
+ *
4
+ * runDocument wires Pass 0.5 → 1 → 2 → 3 → 4 + gates. Consumers inject the LLM,
5
+ * the work root, and the input. An `outlineOnly` mode stops after Pass 1 (for user gate).
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ import { estimateTokens } from './llm.js';
10
+ import { compressInputs } from './input-digest.js';
11
+ import { generateOutline, lintOutline, outlineHasErrors, freezeOutline, readOutline } from './outline.js';
12
+ import { expandProfile } from './profiles.js';
13
+ import { generateDigest, extractConstraintList, lintDigest, deterministicDigest, recordDigestSources, affectedDownstream } from './digests.js';
14
+ import { fillChapter, fillOnce, shouldFillOnce } from './map.js';
15
+ import { buildReviewPacket, reviewWithAgent, checkReviewResult, writeReviewResult, computePartHashes, applyFindings } from './review.js';
16
+ import { renderDocument, renderHtml } from './render.js';
17
+ import { runGates } from './gates.js';
18
+ import { resolvePaths } from './paths.js';
19
+ import { parseEntities } from './schemas.js';
20
+ import { lintNarrative } from './lint.js';
21
+ import { checkStructuredCoverage } from './coverage.js';
22
+ import { resolveProjectConventionPaths } from '../project-conventions.js';
23
+ const DEFAULT_MAX_ROUNDS = 3;
24
+ const OUTLINE_MAX_CONFIRM_ROUNDS = 2;
25
+ export async function runDocument(options) {
26
+ const { llm, workRoot, profile, library, inputs, inputFeatures, change } = options;
27
+ const maxRounds = options.maxRounds ?? DEFAULT_MAX_ROUNDS;
28
+ const persist = options.persist ?? true;
29
+ const paths = resolvePaths(workRoot);
30
+ // --- Pass 0.5: input compression (T4) ---
31
+ const inputDigest = await compressInputs(workRoot, inputs, llm);
32
+ // --- Pass 1: outline (T5) with user confirmation gate (T5.4) ---
33
+ const expanded = expandProfile(profile, library, inputFeatures);
34
+ if (expanded.chapters.length === 0) {
35
+ return { ok: false, stage: 'profile-expand', diagnostics: expanded.diagnostics.map((d) => ({ gate: 'profile', code: d.code, severity: d.severity, message: d.message })) };
36
+ }
37
+ let outline;
38
+ let confirmed = false;
39
+ const confirmer = options.outlineConfirmer;
40
+ for (let round = 0; round < OUTLINE_MAX_CONFIRM_ROUNDS; round++) {
41
+ outline = await generateOutline(llm, { profile, inputDigest, prompt: options.outlinePrompt });
42
+ const outlineDiags = lintOutline(outline, profile);
43
+ if (outlineHasErrors(outlineDiags)) {
44
+ return { ok: false, stage: 'outline-lint', diagnostics: outlineDiags.map((d) => ({ gate: 'outline', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter })) };
45
+ }
46
+ // User confirmation gate: if a confirmer is provided, ask; reject → regenerate.
47
+ if (confirmer) {
48
+ if (await confirmer(outline)) {
49
+ confirmed = true;
50
+ break;
51
+ }
52
+ // rejected → loop regenerates
53
+ }
54
+ else {
55
+ confirmed = true; // non-interactive / test path: auto-accept
56
+ break;
57
+ }
58
+ }
59
+ if (!confirmed || !outline) {
60
+ return { ok: false, stage: 'outline-unconfirmed', outline: outline ?? undefined, diagnostics: [{ gate: 'outline', code: 'outline_rejected', severity: 'error', message: 'Outline rejected by user after max confirm rounds' }] };
61
+ }
62
+ if (persist)
63
+ await freezeOutline(workRoot, outline);
64
+ if (options.outlineOnly) {
65
+ return { ok: true, stage: 'outline-only', outline, diagnostics: [] };
66
+ }
67
+ // --- Chapter-by-chapter fill (Pass 2, T6) with real dependency digests (T7) ---
68
+ const chapterById = library;
69
+ const ctx = { outline, digests: new Map(), upstreamEntities: { interfaces: [], tables: [], decisions: [] }, workRoot, chapterById };
70
+ // Process in DAG order: outline.chapters is already topological from expandProfile.
71
+ const narrativeMap = new Map();
72
+ const entityMap = new Map();
73
+ const tokenMap = new Map();
74
+ const budgetMap = new Map();
75
+ const allEntities = { interfaces: [], tables: [], decisions: [] };
76
+ const useFillOnce = await shouldFillOnce(outline, ctx);
77
+ if (useFillOnce) {
78
+ const result = await fillOnce(llm, outline, ctx);
79
+ if (!result.ok) {
80
+ // Honest degradation: fill_once failed → fall back to per-chapter fill (decision 9).
81
+ const fillErr = await fillAllChapters(outline, ctx, narrativeMap, entityMap, tokenMap, budgetMap, allEntities, llm, persist, paths, options.incremental);
82
+ if (fillErr.length > 0) {
83
+ return { ok: false, stage: 'fill', outline, diagnostics: fillErr };
84
+ }
85
+ }
86
+ else {
87
+ for (const f of result.chapters) {
88
+ narrativeMap.set(f.chapterId, f.narrative);
89
+ entityMap.set(f.chapterId, f.entities);
90
+ tokenMap.set(f.chapterId, f.tokens);
91
+ budgetMap.set(f.chapterId, chapterById.get(f.chapterId)?.output_budget_tokens);
92
+ allEntities.interfaces.push(...f.entities.interfaces);
93
+ allEntities.tables.push(...f.entities.tables);
94
+ allEntities.decisions.push(...f.entities.decisions);
95
+ if (persist) {
96
+ await fs.mkdir(paths.chaptersDir, { recursive: true });
97
+ await fs.writeFile(join(paths.chaptersDir, `${f.chapterId}.md`), f.narrative, 'utf-8');
98
+ }
99
+ }
100
+ }
101
+ }
102
+ else {
103
+ const fillErr = await fillAllChapters(outline, ctx, narrativeMap, entityMap, tokenMap, budgetMap, allEntities, llm, persist, paths, options.incremental);
104
+ if (fillErr.length > 0) {
105
+ return { ok: false, stage: 'fill', outline, diagnostics: fillErr };
106
+ }
107
+ }
108
+ // --- Pass 3: AI review with real fix→re-review loop (T8, decision 9) ---
109
+ if (persist)
110
+ await fs.writeFile(paths.entities, JSON.stringify(allEntities, null, 2), 'utf-8');
111
+ let review;
112
+ let reviewDiags = [];
113
+ if (!options.skipReview) {
114
+ for (let round = 0; round < maxRounds; round++) {
115
+ const packet = await buildReviewPacket(workRoot, change, outline);
116
+ const candidate = await reviewWithAgent(llm, packet, options.reviewPrompt);
117
+ // Apply findings by target:
118
+ // narrative → append fix note to chapters/<id>.md
119
+ // entity → regenerate the affected chapter (real entity repair via re-fill)
120
+ const entityRefills = [];
121
+ await applyFindings(workRoot, candidate.findings, async (finding) => {
122
+ const target = finding.target ?? 'narrative';
123
+ if (target === 'entity' || target === 'mixed') {
124
+ if (finding.chapter)
125
+ entityRefills.push(finding.chapter);
126
+ }
127
+ if (target === 'narrative' || target === 'mixed') {
128
+ const chId = finding.chapter;
129
+ if (chId) {
130
+ const existing = narrativeMap.get(chId) ?? '';
131
+ narrativeMap.set(chId, `${existing}\n\n<!-- review-fix ${finding.id} -->\n${finding.message}`);
132
+ if (persist) {
133
+ await fs.mkdir(paths.chaptersDir, { recursive: true });
134
+ await fs.writeFile(join(paths.chaptersDir, `${chId}.md`), narrativeMap.get(chId), 'utf-8');
135
+ }
136
+ }
137
+ }
138
+ });
139
+ // Entity targets: re-fill the affected chapters so contract entities are regenerated (real fix).
140
+ for (const chId of new Set(entityRefills)) {
141
+ const ch = outline.chapters.find((c) => c.id === chId);
142
+ if (!ch)
143
+ continue;
144
+ ctx.upstreamEntities = allEntities;
145
+ ctx.digests = await buildDigestsFor(ch, ctx.chapterById, outline, narrativeMap, allEntities, llm);
146
+ const result = await fillChapter(llm, ch, ctx, ctx.chapterById.get(ch.id)?.output_budget_tokens);
147
+ if (result.ok && result.fill) {
148
+ narrativeMap.set(ch.id, result.fill.narrative);
149
+ entityMap.set(ch.id, result.fill.entities);
150
+ tokenMap.set(ch.id, result.fill.tokens);
151
+ // Replace this chapter's entities in allEntities (remove old, add new).
152
+ allEntities.interfaces = allEntities.interfaces.filter((i) => !result.fill.entities.interfaces.some((n) => n.id === i.id));
153
+ allEntities.tables = allEntities.tables.filter((t) => !result.fill.entities.tables.some((n) => n.id === t.id));
154
+ allEntities.decisions = allEntities.decisions.filter((d) => !result.fill.entities.decisions.some((n) => n.id === d.id));
155
+ allEntities.interfaces.push(...result.fill.entities.interfaces);
156
+ allEntities.tables.push(...result.fill.entities.tables);
157
+ allEntities.decisions.push(...result.fill.entities.decisions);
158
+ if (persist) {
159
+ await fs.mkdir(paths.chaptersDir, { recursive: true });
160
+ await fs.writeFile(join(paths.chaptersDir, `${ch.id}.md`), result.fill.narrative, 'utf-8');
161
+ await fs.writeFile(paths.entities, JSON.stringify(allEntities, null, 2), 'utf-8');
162
+ }
163
+ }
164
+ }
165
+ // After applying fixes, refresh part_hashes so tamper checks verify against the fixed state
166
+ // (repairs are engine-driven, not external tampering).
167
+ if (persist) {
168
+ candidate.part_hashes = await computePartHashes(workRoot, outline.chapters.map((c) => c.id));
169
+ }
170
+ const check = await checkReviewResult(workRoot, candidate, outline);
171
+ review = candidate;
172
+ reviewDiags = check.diagnostics.map((d) => ({ gate: 'review', code: d.code, severity: d.severity, message: d.message }));
173
+ if (check.ok) {
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ if (review && persist) {
179
+ await writeReviewResult(workRoot, review);
180
+ }
181
+ // --- Pass 4: render (T9) ---
182
+ const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap });
183
+ if (persist)
184
+ await fs.writeFile(paths.documentMd, documentMd, 'utf-8');
185
+ let documentHtml;
186
+ if (options.renderHtmlOutput !== false) {
187
+ documentHtml = await renderHtml(documentMd, change);
188
+ if (persist)
189
+ await fs.writeFile(paths.documentHtml, documentHtml, 'utf-8');
190
+ }
191
+ // --- Gates (T10) ---
192
+ const gates = await runGates({
193
+ outline,
194
+ chapterNarratives: narrativeMap,
195
+ chapterEntities: entityMap,
196
+ chapterTokens: tokenMap,
197
+ chapterBudget: budgetMap,
198
+ entities: allEntities,
199
+ review,
200
+ workRoot,
201
+ });
202
+ let totalTokens = 0;
203
+ for (const t of tokenMap.values())
204
+ totalTokens += t;
205
+ return {
206
+ ok: gates.ok,
207
+ stage: gates.ok ? 'done' : 'gates-fail',
208
+ outline,
209
+ documentMd,
210
+ documentHtml,
211
+ diagnostics: [...reviewDiags, ...gates.diagnostics],
212
+ tokenEstimate: totalTokens,
213
+ };
214
+ }
215
+ /**
216
+ * Build dependency digests for a chapter's direct deps (decisions 2 + 8).
217
+ * For each dep chapter, derive its constraint list (points/entities/decisions it must preserve),
218
+ * generate a bounded LLM summary carrying those constraints, then bidirectional lint:
219
+ * - forward: digest ids must exist in source entities
220
+ * - reverse: every constraint item must appear in the digest
221
+ * On lint error, fall back to the deterministic entity-registry digest (honest degradation).
222
+ */
223
+ async function buildDigestsFor(chapter, library, outline, narrativeMap, allEntities, llm) {
224
+ const digests = new Map();
225
+ const chapterById = new Map(outline.chapters.map((ch) => [ch.id, ch]));
226
+ const comp = library.get(chapter.id);
227
+ for (const dep of comp?.depends_on ?? []) {
228
+ const depChapter = chapterById.get(dep);
229
+ if (!depChapter)
230
+ continue;
231
+ const source = narrativeMap.get(dep) ?? '';
232
+ const constraints = extractConstraintList(depChapter, allEntities);
233
+ const knownIds = new Set([
234
+ ...allEntities.interfaces.map((i) => i.id),
235
+ ...allEntities.tables.map((t) => t.id),
236
+ ...allEntities.decisions.map((d) => d.id),
237
+ ]);
238
+ let digest = '';
239
+ if (source.trim().length > 0) {
240
+ try {
241
+ digest = await generateDigest(llm, {
242
+ chapterId: dep,
243
+ chapterTitle: depChapter.title,
244
+ source,
245
+ constraints,
246
+ });
247
+ }
248
+ catch {
249
+ digest = ''; // LLM failure → fall through to deterministic registry
250
+ }
251
+ }
252
+ // Bidirectional lint; on error fall back to deterministic registry.
253
+ const lint = lintDigest(digest || '', constraints, knownIds);
254
+ if (digest && lint.every((d) => d.severity !== 'error')) {
255
+ digests.set(dep, digest);
256
+ }
257
+ else {
258
+ const ids = constraints.filter((c) => c.kind === 'entity').map((c) => c.id);
259
+ digests.set(dep, deterministicDigest(allEntities, ids));
260
+ }
261
+ }
262
+ return digests;
263
+ }
264
+ /**
265
+ * Fill all chapters in DAG order (real dependency digests + per-chapter budget).
266
+ * Returns [] on success, or a non-empty error diagnostic array on failure.
267
+ */
268
+ async function fillAllChapters(outline, ctx, narrativeMap, entityMap, tokenMap, budgetMap, allEntities, llm, persist, paths, incremental) {
269
+ // Compute invalidation set (decision 9): which chapters' digests reference the changed entities.
270
+ let refillSet;
271
+ if (incremental) {
272
+ const changedChapterId = incremental.changedChapterId;
273
+ const changedEntityIds = incremental.changedEntityIds ?? [];
274
+ if (changedChapterId || changedEntityIds.length > 0) {
275
+ const sources = outline.chapters.map((ch) => recordDigestSources(ch, allEntities));
276
+ const affected = affectedDownstream(changedChapterId ?? '', changedEntityIds, sources);
277
+ refillSet = new Set(affected);
278
+ }
279
+ }
280
+ for (const ch of outline.chapters) {
281
+ // Incremental skip (decision 9): reuse cache only for chapters that (a) are not in the
282
+ // invalidation set, (b) are not the change source, and (c) produce NO contract entities —
283
+ // entity-producing chapters always refill to avoid entity-state divergence across maps.
284
+ const producesEntities = ch.points.some((p) => (p.kind === 'entity' || p.kind === 'mixed') && (p.references ?? []).length > 0);
285
+ if (refillSet && !refillSet.has(ch.id) && !(incremental?.changedChapterId === ch.id) && !producesEntities) {
286
+ if (persist) {
287
+ try {
288
+ const cached = await fs.readFile(join(paths.chaptersDir, `${ch.id}.md`), 'utf-8');
289
+ if (cached.trim().length > 0) {
290
+ narrativeMap.set(ch.id, cached);
291
+ tokenMap.set(ch.id, estimateTokens(cached));
292
+ budgetMap.set(ch.id, ctx.chapterById.get(ch.id)?.output_budget_tokens);
293
+ continue;
294
+ }
295
+ }
296
+ catch {
297
+ // no cache → fall through to refill
298
+ }
299
+ }
300
+ }
301
+ // Real dependency digests (decision 2/8): for each direct dep, build a digest that carries
302
+ // the dep chapter's constraint list (entities/points it must preserve) via a bounded LLM
303
+ // summary, then bidirectional lint; fall back to deterministic registry on lint error.
304
+ ctx.upstreamEntities = allEntities;
305
+ ctx.digests = await buildDigestsFor(ch, ctx.chapterById, outline, narrativeMap, allEntities, llm);
306
+ const budget = ctx.chapterById.get(ch.id)?.output_budget_tokens;
307
+ const result = await fillChapter(llm, ch, ctx, budget);
308
+ if (!result.ok || !result.fill) {
309
+ return [{ gate: 'map', code: 'fill_failed', severity: 'error', message: result.error ?? `fill ${ch.id} failed`, chapter: ch.id }];
310
+ }
311
+ const fill = result.fill;
312
+ narrativeMap.set(ch.id, fill.narrative);
313
+ entityMap.set(ch.id, fill.entities);
314
+ tokenMap.set(ch.id, fill.tokens);
315
+ budgetMap.set(ch.id, budget);
316
+ // Merge entities for downstream digest/render.
317
+ allEntities.interfaces.push(...fill.entities.interfaces);
318
+ allEntities.tables.push(...fill.entities.tables);
319
+ allEntities.decisions.push(...fill.entities.decisions);
320
+ if (persist) {
321
+ await fs.mkdir(paths.chaptersDir, { recursive: true });
322
+ await fs.writeFile(join(paths.chaptersDir, `${ch.id}.md`), fill.narrative, 'utf-8');
323
+ await fs.writeFile(paths.entities, JSON.stringify(allEntities, null, 2), 'utf-8');
324
+ }
325
+ }
326
+ return [];
327
+ }
328
+ /** Map a scene profile to the project-convention topics it should read (approval-era: 项目约定 > guidance > LLM). */
329
+ export function sceneTopics(profileId) {
330
+ switch (profileId) {
331
+ case '0to1':
332
+ return ['architecture', 'database', 'api', 'frontend'];
333
+ case 'bugfix':
334
+ return ['database', 'api', 'architecture'];
335
+ case 'feature':
336
+ return ['api', 'database', 'frontend', 'architecture'];
337
+ default:
338
+ return [];
339
+ }
340
+ }
341
+ /** Emit the Agent-executable step list (no LLM call — the Agent's IDE model does the work). */
342
+ export function planSteps(options) {
343
+ const { profile, library, inputFeatures, inputs, workRoot, projectRoot } = options;
344
+ const expanded = expandProfile(profile, library, inputFeatures);
345
+ const steps = [];
346
+ // Prep: read project conventions / IDE rules & skills (approval-era "项目约定 > guidance > LLM").
347
+ // The Agent MUST ground the document in the project's own conventions before generating —
348
+ // even when no convention files are found, it scans IDE skills/rules + guidance packs.
349
+ if (projectRoot) {
350
+ const topics = sceneTopics(profile.id);
351
+ const conventionFiles = [];
352
+ for (const topic of topics) {
353
+ conventionFiles.push(...resolveProjectConventionPaths({ projectRoot, topic: topic }));
354
+ }
355
+ const found = conventionFiles.length > 0
356
+ ? `解析到的规约文件:${conventionFiles.join('、')}`
357
+ : '未发现项目规约文件(config conventions 或中立文档),请扫描 IDE skills/rules(.cursor / .claude / .agents)与 guidance packs(skills/guidance-packs.yaml 等)';
358
+ steps.push({
359
+ id: 'conventions',
360
+ phase: 'prep',
361
+ title: '读取项目规约与 IDE 规则',
362
+ description: `生成前先读取项目规约(优先级:项目约定 > SpecFlow guidance > LLM)。${found}。` +
363
+ '将相关约束(数据库 DDL 约定、API 规范、前端规约、架构禁令)落实到后续章节;未发现时显式标注「未发现项目规约」。',
364
+ prompt: '.claude/specflow/prompts/document/outline/general.md',
365
+ inputFiles: conventionFiles,
366
+ outputFile: 'digests/conventions.md',
367
+ required: false,
368
+ });
369
+ }
370
+ // Prep: compress inputs into a digest (skip for single short description).
371
+ const multiInput = inputs.length > 1 || inputs.some((i) => i.content.length > 800);
372
+ if (multiInput) {
373
+ steps.push({
374
+ id: 'input-summary',
375
+ phase: 'prep',
376
+ title: '压缩输入为全局摘要',
377
+ description: '阅读全部输入(proposal/design/specs/code),产出紧凑摘要(保留实体候选与约束),写入 digests/input-summary.md。',
378
+ prompt: '.claude/specflow/prompts/document/outline/general.md',
379
+ inputFiles: inputs.map((i) => i.source),
380
+ outputFile: 'digests/input-summary.md',
381
+ required: true,
382
+ });
383
+ }
384
+ // Outline.
385
+ steps.push({
386
+ id: 'outline',
387
+ phase: 'outline',
388
+ title: '生成文档大纲',
389
+ description: `按 ${profile.id} 场景(必选章 ${profile.required.join('、')})生成 outline.yaml:章节顺序 + 每章要点(kind/references)+ 实体注册表 + 决策。`,
390
+ prompt: '.claude/specflow/prompts/document/outline/general.md',
391
+ inputFiles: multiInput ? ['digests/input-summary.md'] : [],
392
+ outputFile: 'outline.yaml',
393
+ required: true,
394
+ });
395
+ // Fill each chapter.
396
+ for (const chId of expanded.chapters) {
397
+ steps.push({
398
+ id: `chapter:${chId}`,
399
+ phase: 'fill',
400
+ title: `填充章节 ${chId}`,
401
+ description: `按 outline.yaml 中 ${chId} 的要点填空,契约实体写入 entities.json(接口须含 failure_examples,表须含 ddl),叙述写入 chapters/${chId}.md。`,
402
+ prompt: `.claude/specflow/prompts/document/map/${chId}.md`,
403
+ inputFiles: ['outline.yaml'],
404
+ outputFile: `chapters/${chId}.md`,
405
+ required: true,
406
+ });
407
+ }
408
+ // Review (optional).
409
+ steps.push({
410
+ id: 'review',
411
+ phase: 'review',
412
+ title: 'AI 审查',
413
+ description: '独立审查全部章节,产出 review-result.json(findings + part_hashes,R14 覆盖核对)。',
414
+ prompt: '.claude/specflow/prompts/document/review/ai-review.md',
415
+ inputFiles: expanded.chapters.map((c) => `chapters/${c}.md`),
416
+ outputFile: 'review-result.json',
417
+ required: false,
418
+ });
419
+ return { steps, chapters: expanded.chapters };
420
+ }
421
+ /** Read Agent-written artifacts from workRoot, validate (lint/coverage/zod) and render. No LLM. */
422
+ export async function validateWork(options) {
423
+ const { profile, workRoot } = options;
424
+ const paths = resolvePaths(workRoot);
425
+ const diags = [];
426
+ // Read outline (readOutline handles both YAML and JSON).
427
+ let outline;
428
+ try {
429
+ outline = await readOutline(workRoot);
430
+ }
431
+ catch (e) {
432
+ return { ok: false, diagnostics: [{ gate: 'outline', code: 'outline_unreadable', severity: 'error', message: `Cannot read outline.yaml: ${e instanceof Error ? e.message : String(e)}` }] };
433
+ }
434
+ // Outline lint.
435
+ const outlineDiags = lintOutline(outline, profile);
436
+ diags.push(...outlineDiags.map((d) => ({ gate: 'outline', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter })));
437
+ // Read chapters + entities.
438
+ const narrativeMap = new Map();
439
+ const entityMap = new Map();
440
+ const allEntities = { interfaces: [], tables: [], decisions: [] };
441
+ const tokenMap = new Map();
442
+ for (const ch of outline.chapters) {
443
+ try {
444
+ const content = await fs.readFile(join(paths.chaptersDir, `${ch.id}.md`), 'utf-8');
445
+ narrativeMap.set(ch.id, content);
446
+ tokenMap.set(ch.id, estimateTokens(content));
447
+ }
448
+ catch (e) {
449
+ diags.push({ gate: 'lint', code: 'chapter_missing', severity: 'error', message: `Missing chapter file ${ch.id}.md`, chapter: ch.id });
450
+ }
451
+ }
452
+ // entities.json.
453
+ try {
454
+ const raw = await fs.readFile(paths.entities, 'utf-8');
455
+ const parsed = parseEntities(JSON.parse(raw));
456
+ allEntities.interfaces = parsed.interfaces;
457
+ allEntities.tables = parsed.tables;
458
+ allEntities.decisions = parsed.decisions;
459
+ }
460
+ catch (e) {
461
+ diags.push({ gate: 'entities', code: 'entities_unreadable', severity: 'warning', message: `entities.json unreadable or invalid: ${e instanceof Error ? e.message : String(e)}` });
462
+ }
463
+ // Narrative lint.
464
+ for (const [chId, narrative] of narrativeMap) {
465
+ for (const d of lintNarrative(chId, narrative)) {
466
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
467
+ }
468
+ }
469
+ // Structured coverage (entity points must be filled).
470
+ for (const d of checkStructuredCoverage(outline, entityMap)) {
471
+ diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
472
+ }
473
+ const hasError = diags.some((d) => d.severity === 'error');
474
+ if (hasError) {
475
+ return { ok: false, diagnostics: diags };
476
+ }
477
+ // Render.
478
+ const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap });
479
+ await fs.writeFile(paths.documentMd, documentMd, 'utf-8');
480
+ let documentHtml;
481
+ if (options.renderHtmlOutput !== false) {
482
+ documentHtml = await renderHtml(documentMd, outline.profile);
483
+ await fs.writeFile(paths.documentHtml, documentHtml, 'utf-8');
484
+ }
485
+ return { ok: true, diagnostics: diags, documentMd, documentHtml };
486
+ }
487
+ export { expandProfile };
@@ -1,6 +1,8 @@
1
- /** Shared stub / inconsistency patterns for assemble lazy-check and approval lint. */
1
+ /**
2
+ * Document Engine — anti-lazy patterns (carried over from ACP, reworked).
3
+ * Used by lint.ts to reject stub / lazy content in narratives and digests.
4
+ */
2
5
  export declare const FORBIDDEN_STUB_PATTERNS: readonly RegExp[];
3
- /** Semantic inconsistency hints (warnings unless strict mode) — long-document generic. */
4
6
  export declare const INCONSISTENCY_HINT_PATTERNS: readonly RegExp[];
5
7
  export declare function findForbiddenMatch(content: string): RegExp | null;
6
8
  export declare function findInconsistencyHints(content: string): readonly RegExp[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Document Engine — anti-lazy patterns (carried over from ACP, reworked).
3
+ * Used by lint.ts to reject stub / lazy content in narratives and digests.
4
+ */
5
+ export const FORBIDDEN_STUB_PATTERNS = [
6
+ /\bTODO\b/i,
7
+ /待补充/,
8
+ /此处省略/,
9
+ /详见\s*(?:上文|下文|附件)(?!.*§)/,
10
+ /略\s*[。.]?$/,
11
+ /\bTBD\b\s*[。.]?$/,
12
+ /(?:^|\n)\s*(?:同上|同前)\s*[。.]?\s*(?:\n|$)/,
13
+ /实现时(?:命名|对齐)/,
14
+ /(?:RPC|rpc).*暂定/,
15
+ /暂定\s*[`']?\w+[`']?/,
16
+ ];
17
+ export const INCONSISTENCY_HINT_PATTERNS = [
18
+ /\b(?:等|etc\.?)\s*[。.)]/,
19
+ /(?:status|状态|enum|枚举)[^.\n]{0,30}(?:等|etc)/i,
20
+ /(?:^|[\s,。;])同前(?!缀)/,
21
+ /(?:^|[\s,。;])同上(?!所述)/,
22
+ /\b(?:待定|未定)\b/i,
23
+ ];
24
+ export function findForbiddenMatch(content) {
25
+ for (const p of FORBIDDEN_STUB_PATTERNS) {
26
+ if (p.test(content))
27
+ return p;
28
+ }
29
+ return null;
30
+ }
31
+ export function findInconsistencyHints(content) {
32
+ return INCONSISTENCY_HINT_PATTERNS.filter((p) => p.test(content));
33
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Document Engine — gate orchestration (T10.4).
3
+ * Runs lint → structured coverage → review check → budget → zod, collecting diagnostics.
4
+ */
5
+ import { Outline, Entities, ReviewResult } from './schemas.js';
6
+ export interface GateDiagnostic {
7
+ gate: string;
8
+ code: string;
9
+ severity: 'error' | 'warning';
10
+ message: string;
11
+ chapter?: string;
12
+ }
13
+ export interface GatesInput {
14
+ outline: Outline;
15
+ chapterNarratives: Map<string, string>;
16
+ chapterEntities: Map<string, Entities>;
17
+ chapterTokens: Map<string, number>;
18
+ chapterBudget: Map<string, number | undefined>;
19
+ entities: Entities;
20
+ review?: ReviewResult;
21
+ workRoot: string;
22
+ }
23
+ export interface GatesResult {
24
+ ok: boolean;
25
+ diagnostics: GateDiagnostic[];
26
+ }
27
+ export declare function runGates(input: GatesInput): Promise<GatesResult>;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Document Engine — gate orchestration (T10.4).
3
+ * Runs lint → structured coverage → review check → budget → zod, collecting diagnostics.
4
+ */
5
+ import { lintNarrative, checkBudget } from './lint.js';
6
+ import { checkStructuredCoverage } from './coverage.js';
7
+ import { checkReviewResult } from './review.js';
8
+ export async function runGates(input) {
9
+ const diags = [];
10
+ // lint: narrative stub/short + budget per chapter
11
+ for (const [chId, narrative] of input.chapterNarratives) {
12
+ for (const d of lintNarrative(chId, narrative)) {
13
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
14
+ }
15
+ const tokens = input.chapterTokens.get(chId) ?? 0;
16
+ const budget = input.chapterBudget.get(chId);
17
+ for (const b of checkBudget(chId, tokens, budget)) {
18
+ diags.push({ gate: 'budget', code: b.code, severity: b.severity, message: b.message, chapter: chId });
19
+ }
20
+ }
21
+ // structured coverage
22
+ for (const d of checkStructuredCoverage(input.outline, input.chapterEntities)) {
23
+ diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
24
+ }
25
+ // review check (semantic channel + tamper + verdict)
26
+ if (input.review) {
27
+ const r = await checkReviewResult(input.workRoot, input.review, input.outline);
28
+ for (const d of r.diagnostics) {
29
+ diags.push({ gate: 'review', code: d.code, severity: d.severity, message: d.message });
30
+ }
31
+ }
32
+ return { ok: diags.every((d) => d.severity !== 'error'), diagnostics: diags };
33
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Document Engine — public entry point.
3
+ * Exports schemas, LLM interface, paths, and (as implemented) the pipeline.
4
+ */
5
+ export * from './schemas.js';
6
+ export * from './llm.js';
7
+ export * from './paths.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Document Engine — public entry point.
3
+ * Exports schemas, LLM interface, paths, and (as implemented) the pipeline.
4
+ */
5
+ export * from './schemas.js';
6
+ export * from './llm.js';
7
+ export * from './paths.js';
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Document Engine — Pass 0.5 input compression + source segmentation (decision 8·P0-A, decision 9).
3
+ *
4
+ * Long inputs are split into stable segments (seg id → original file + line range),
5
+ * each segment is compressed in a bounded-context LLM call, then segment digests are
6
+ * merged into a global input digest (~1–2k tokens). Pass 1 generates the outline from the
7
+ * global digest; Pass 2 pulls original detail by `source_segments` from the segment index.
8
+ */
9
+ import type { DocumentLLM } from './llm.js';
10
+ export interface InputSegment {
11
+ id: string;
12
+ type: string;
13
+ label: string;
14
+ source: string;
15
+ startLine: number;
16
+ endLine: number;
17
+ content: string;
18
+ }
19
+ export interface SegmentDigest {
20
+ id: string;
21
+ type: string;
22
+ label: string;
23
+ summary: string;
24
+ entity_candidates: string[];
25
+ }
26
+ export interface GlobalInputDigest {
27
+ segments: SegmentDigest[];
28
+ summary: string;
29
+ }
30
+ export interface SegmentIndex {
31
+ schema: 'specflow.document.segments/v1';
32
+ segments: Array<{
33
+ id: string;
34
+ type: string;
35
+ label: string;
36
+ source: string;
37
+ startLine: number;
38
+ endLine: number;
39
+ }>;
40
+ }
41
+ export interface SegmentableInput {
42
+ type: string;
43
+ source: string;
44
+ content: string;
45
+ }
46
+ export declare function segmentInputs(inputs: SegmentableInput[]): InputSegment[];
47
+ export declare function compressSegment(seg: InputSegment, llm: DocumentLLM): Promise<SegmentDigest>;
48
+ /** Heuristic entity-candidate extraction: backtick tokens + PascalCase/snake tokens. */
49
+ export declare function extractEntityCandidates(text: string): string[];
50
+ export declare function mergeSegmentDigests(digests: SegmentDigest[], llm: DocumentLLM): Promise<GlobalInputDigest>;
51
+ export declare function writeSegmentIndex(workRoot: string, segments: InputSegment[]): Promise<string>;
52
+ export declare function readSegmentIndex(workRoot: string): Promise<SegmentIndex>;
53
+ /** Fetch original text for a set of source_segments (bounded, decision 9). */
54
+ export declare function fetchSegments(workRoot: string, segmentIds: string[]): Promise<string>;
55
+ export declare function compressInputs(workRoot: string, inputs: SegmentableInput[], llm: DocumentLLM): Promise<GlobalInputDigest>;