@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,23 @@
1
+ /**
2
+ * Document Engine — Profile static validator (decision 8 · P1-H).
3
+ *
4
+ * Validates a ScenarioProfile at load time (fail-fast) before any generation:
5
+ * 1. Existence — every required/optional chapter id exists in the library
6
+ * 2. Required met — required is non-empty and fully present
7
+ * 3. Closure — every depends_on target is in the profile (or declared shared)
8
+ * 4. Acyclic — depends_on forms a DAG (topological sort, cycle → error)
9
+ * 5. When-usable — when expressions reference known input features
10
+ */
11
+ import { ScenarioProfile, ChapterComponent } from './schemas.js';
12
+ export interface ValidationDiagnostic {
13
+ code: string;
14
+ severity: 'error' | 'warning';
15
+ message: string;
16
+ chapter?: string;
17
+ }
18
+ export interface ProfileValidationOptions {
19
+ profile: ScenarioProfile;
20
+ library: Map<string, ChapterComponent>;
21
+ }
22
+ export declare function validateProfile(options: ProfileValidationOptions): ValidationDiagnostic[];
23
+ export declare function hasErrors(diags: ValidationDiagnostic[]): boolean;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Document Engine — Profile static validator (decision 8 · P1-H).
3
+ *
4
+ * Validates a ScenarioProfile at load time (fail-fast) before any generation:
5
+ * 1. Existence — every required/optional chapter id exists in the library
6
+ * 2. Required met — required is non-empty and fully present
7
+ * 3. Closure — every depends_on target is in the profile (or declared shared)
8
+ * 4. Acyclic — depends_on forms a DAG (topological sort, cycle → error)
9
+ * 5. When-usable — when expressions reference known input features
10
+ */
11
+ import { isKnownFeature, parseWhen, featuresInWhen } from './input-features.js';
12
+ export function validateProfile(options) {
13
+ const { profile, library } = options;
14
+ const diags = [];
15
+ const declared = new Set([
16
+ ...profile.required,
17
+ ...profile.optional_candidates,
18
+ ...profile.forbidden,
19
+ ...profile.shared,
20
+ ]);
21
+ // 1. Existence: every referenced chapter id exists in the library.
22
+ for (const id of declared) {
23
+ if (!library.has(id)) {
24
+ diags.push({
25
+ code: 'chapter_not_found',
26
+ severity: 'error',
27
+ message: `Chapter "${id}" referenced by profile "${profile.id}" does not exist in the library`,
28
+ });
29
+ }
30
+ }
31
+ // 2. Required non-empty + all present.
32
+ if (profile.required.length === 0) {
33
+ diags.push({
34
+ code: 'required_empty',
35
+ severity: 'error',
36
+ message: `Profile "${profile.id}" must declare at least one required chapter`,
37
+ });
38
+ }
39
+ // 2b. 分类冲突(P2):同一章节出现在 required∩forbidden / optional∩forbidden 时,
40
+ // expandProfile 会静默从 selected 删除,必需章节凭空消失。加载期即报错。
41
+ const requiredSet = new Set(profile.required);
42
+ const optionalSet = new Set(profile.optional_candidates);
43
+ for (const id of profile.forbidden) {
44
+ if (requiredSet.has(id)) {
45
+ diags.push({ code: 'category_conflict', severity: 'error', chapter: id, message: `Chapter "${id}" is in both required and forbidden` });
46
+ }
47
+ if (optionalSet.has(id)) {
48
+ diags.push({ code: 'category_conflict', severity: 'error', chapter: id, message: `Chapter "${id}" is in both optional_candidates and forbidden` });
49
+ }
50
+ }
51
+ // 2c. 数组内重复 id。
52
+ for (const [label, list] of [['required', profile.required], ['optional_candidates', profile.optional_candidates], ['forbidden', profile.forbidden], ['shared', profile.shared]]) {
53
+ const seenIds = new Set();
54
+ for (const id of list) {
55
+ if (seenIds.has(id)) {
56
+ diags.push({ code: 'duplicate_id', severity: 'warning', chapter: id, message: `Chapter "${id}" appears more than once in ${label}` });
57
+ }
58
+ seenIds.add(id);
59
+ }
60
+ }
61
+ // 3+4. Dependency closure + acyclic over the selected chapters.
62
+ // A chapter may depend on: another selected chapter, a shared chapter, or a chapter in
63
+ // required/optional that we include in the closure graph.
64
+ // closure 检查覆盖 required + optional_candidates(P2):可选章节是 LLM 可能选中的
65
+ // 渲染章节,其悬空依赖必须报错;shared 章节仅作依赖闭合、不渲染,其 depends_on
66
+ // 不需要闭包(引擎不会填充 shared 章节)。
67
+ const closureNodes = new Set([...profile.required, ...profile.optional_candidates, ...profile.shared]);
68
+ const renderedIds = [...profile.required, ...profile.optional_candidates];
69
+ for (const id of renderedIds) {
70
+ const ch = library.get(id);
71
+ if (!ch)
72
+ continue;
73
+ for (const dep of ch.depends_on) {
74
+ if (!closureNodes.has(dep)) {
75
+ diags.push({
76
+ code: 'dependency_unclosed',
77
+ severity: 'error',
78
+ chapter: id,
79
+ message: `Chapter "${id}" depends on "${dep}" which is not in the profile's required/optional/shared set`,
80
+ });
81
+ }
82
+ }
83
+ }
84
+ // Acyclic check via Kahn's algorithm on selected + shared nodes.
85
+ // 注意:不再排除自依赖(dep !== n 过滤已删除,P2)——自环必须被正确判为环。
86
+ const nodes = [...closureNodes].filter((n) => library.has(n));
87
+ const indegree = new Map();
88
+ const adj = new Map();
89
+ for (const n of nodes) {
90
+ indegree.set(n, 0);
91
+ adj.set(n, []);
92
+ }
93
+ for (const n of nodes) {
94
+ const ch = library.get(n);
95
+ for (const dep of ch.depends_on) {
96
+ if (closureNodes.has(dep) && library.has(dep)) {
97
+ adj.get(dep).push(n);
98
+ indegree.set(n, (indegree.get(n) ?? 0) + 1);
99
+ }
100
+ }
101
+ }
102
+ const queue = nodes.filter((n) => indegree.get(n) === 0);
103
+ const seen = new Set();
104
+ while (queue.length > 0) {
105
+ const cur = queue.shift();
106
+ seen.add(cur);
107
+ for (const next of adj.get(cur) ?? []) {
108
+ indegree.set(next, (indegree.get(next) ?? 1) - 1);
109
+ if (indegree.get(next) === 0)
110
+ queue.push(next);
111
+ }
112
+ }
113
+ for (const n of nodes) {
114
+ if (!seen.has(n)) {
115
+ diags.push({
116
+ code: 'dependency_cycle',
117
+ severity: 'error',
118
+ chapter: n,
119
+ message: `Dependency cycle detected involving chapter "${n}"`,
120
+ });
121
+ }
122
+ }
123
+ // 5. When-usable: every when expression must parse and reference known input features.
124
+ // (覆盖 shared 章节,P2;未知 feature 提级为 error——拼错 feature 会静默丢章节)
125
+ for (const id of [...profile.required, ...profile.optional_candidates, ...profile.shared]) {
126
+ const ch = library.get(id);
127
+ if (!ch?.when)
128
+ continue;
129
+ try {
130
+ parseWhen(ch.when);
131
+ for (const f of featuresInWhen(ch.when)) {
132
+ if (!isKnownFeature(f)) {
133
+ diags.push({
134
+ code: 'when_unknown_feature',
135
+ severity: 'error',
136
+ chapter: id,
137
+ message: `when expression of chapter "${id}" references unknown input feature "${f}"`,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ catch (e) {
143
+ diags.push({
144
+ code: 'when_invalid',
145
+ severity: 'error',
146
+ chapter: id,
147
+ message: `when expression of chapter "${id}" is invalid: ${e instanceof Error ? e.message : String(e)}`,
148
+ });
149
+ }
150
+ }
151
+ return diags;
152
+ }
153
+ export function hasErrors(diags) {
154
+ return diags.some((d) => d.severity === 'error');
155
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Document Engine — scenario profile loader + expansion.
3
+ * Loads templates/document/profiles/*.yaml, validates via profile-validator,
4
+ * and expands a profile into the concrete chapter selection given input features.
5
+ */
6
+ import { ScenarioProfile } from './schemas.js';
7
+ import { ValidationDiagnostic } from './profile-validator.js';
8
+ import { ChapterComponent } from './schemas.js';
9
+ export interface ExpandedProfile {
10
+ profile: ScenarioProfile;
11
+ /** Chapters in render order: required first (in order), then selected optional candidates. */
12
+ chapters: string[];
13
+ diagnostics: ValidationDiagnostic[];
14
+ }
15
+ export declare function loadProfile(profileId: string, dir?: string): Promise<ScenarioProfile>;
16
+ /**
17
+ * Expand a profile against input features:
18
+ * - required chapters always included (in declared order)
19
+ * - optional candidates included when their `when` (if any) evaluates true, or when no `when`
20
+ * - forbidden chapters never included
21
+ * - output respects dependency order (topological) so dependents come after dependencies
22
+ */
23
+ export declare function expandProfile(profile: ScenarioProfile, library: Map<string, ChapterComponent>, features: Record<string, boolean>): ExpandedProfile;
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Document Engine — scenario profile loader + expansion.
3
+ * Loads templates/document/profiles/*.yaml, validates via profile-validator,
4
+ * and expands a profile into the concrete chapter selection given input features.
5
+ */
6
+ import { promises as fs } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import yaml from 'js-yaml';
9
+ import { parseScenarioProfile } from './schemas.js';
10
+ import { validateProfile, hasErrors } from './profile-validator.js';
11
+ import { evaluateWhen } from './input-features.js';
12
+ import { resolveTemplateDir } from './asset-paths.js';
13
+ /** profile id 白名单(P2):拒绝路径穿越(../、绝对路径)。 */
14
+ const PROFILE_ID_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
15
+ export async function loadProfile(profileId, dir) {
16
+ if (!PROFILE_ID_RE.test(profileId)) {
17
+ throw new Error(`Invalid profile id "${profileId}": must match ${PROFILE_ID_RE}`);
18
+ }
19
+ // 默认从「用户 cwd/templates 优先、包内 templates fallback」解析(多仓工作区也能跑)。
20
+ const resolvedDir = dir ?? (await resolveTemplateDir(join('document', 'profiles')));
21
+ const raw = yaml.load(await fs.readFile(join(resolvedDir, `${profileId}.yaml`), 'utf-8'));
22
+ return parseScenarioProfile(raw);
23
+ }
24
+ /**
25
+ * Expand a profile against input features:
26
+ * - required chapters always included (in declared order)
27
+ * - optional candidates included when their `when` (if any) evaluates true, or when no `when`
28
+ * - forbidden chapters never included
29
+ * - output respects dependency order (topological) so dependents come after dependencies
30
+ */
31
+ export function expandProfile(profile, library, features) {
32
+ const diags = validateProfile({ profile, library });
33
+ if (hasErrors(diags)) {
34
+ return { profile, chapters: [], diagnostics: diags };
35
+ }
36
+ const selected = new Set(profile.required);
37
+ for (const id of profile.optional_candidates) {
38
+ const ch = library.get(id);
39
+ if (!ch)
40
+ continue;
41
+ if (ch.when && !evaluateWhen(ch.when, features))
42
+ continue;
43
+ selected.add(id);
44
+ }
45
+ for (const id of profile.forbidden)
46
+ selected.delete(id);
47
+ // Topological order over selected chapters (dependencies first).
48
+ const ordered = [];
49
+ const visited = new Set();
50
+ const stack = new Set();
51
+ const visit = (id) => {
52
+ if (visited.has(id))
53
+ return;
54
+ if (stack.has(id))
55
+ return; // cycle — validator should have caught it
56
+ stack.add(id);
57
+ const ch = library.get(id);
58
+ if (ch) {
59
+ for (const dep of ch.depends_on) {
60
+ if (selected.has(dep))
61
+ visit(dep);
62
+ }
63
+ }
64
+ stack.delete(id);
65
+ visited.add(id);
66
+ if (selected.has(id))
67
+ ordered.push(id);
68
+ };
69
+ for (const id of profile.required)
70
+ visit(id);
71
+ for (const id of profile.optional_candidates) {
72
+ if (selected.has(id))
73
+ visit(id);
74
+ }
75
+ return { profile, chapters: ordered, diagnostics: diags };
76
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Document Engine — Pass 4 deterministic rendering (decisions 7/9).
3
+ *
4
+ * Renders the final document by concatenating chapters in outline order (NOT assemble):
5
+ * contract entities → Markdown tables/code blocks; narrative → verbatim into its chapter.
6
+ * Optional HTML render reuses `marked`.
7
+ */
8
+ import { Outline, Entities, InterfaceEntity, TableEntity } from './schemas.js';
9
+ /** HTML 转义:用于 <title>/<h1> 插值(防存储型 XSS)。 */
10
+ export declare function escapeHtml(s: string): string;
11
+ /** Markdown 表格单元格转义:转义 | 与换行(防破坏列对齐/拆行)。 */
12
+ export declare function escapeTableCell(s: string): string;
13
+ /** 代码围栏内容转义:把可能提前终止 ``` 围栏的行替换为安全形式。 */
14
+ export declare function escapeFenceContent(s: string): string;
15
+ /** 单行内联文本:转义换行,避免注入新标题/新行。 */
16
+ export declare function escapeInline(s: string): string;
17
+ export declare function renderInterface(i: InterfaceEntity): string;
18
+ export declare function renderTable(t: TableEntity): string;
19
+ export declare function renderDecisions(decisions: Array<{
20
+ id: string;
21
+ text: string;
22
+ }>): string;
23
+ export declare function renderEntities(entities: Entities): string;
24
+ export interface RenderDocumentInput {
25
+ outline: Outline;
26
+ entities: Entities;
27
+ /** chapterId → narrative markdown */
28
+ narratives: Map<string, string>;
29
+ /** Scenario profile — drives appendices rendering (profile.appendices, §7.2/7.3). */
30
+ profile?: {
31
+ appendices?: string[];
32
+ };
33
+ /** LLM-written anti-AI self-check items (from appendices/anti-ai.md, optional). */
34
+ appendixAntiAI?: string;
35
+ }
36
+ /** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
37
+ * stays in `chapters/<id>.md` and `review-result.json`; the deliverable document must not show
38
+ * internal fix notes. */
39
+ export declare function stripReviewNotes(text: string): string;
40
+ /**
41
+ * 从章节叙述中剥离内嵌的**契约实体 JSON 块**(```json {"entities": {...}} ```)。
42
+ * 这些块是 Agent 写给引擎提取的机器中间格式(map 提示词要求 kind=entity|mixed 要点产出),
43
+ * 引擎已把其中实体提取进 entities.json,并在文档末尾「契约实体」章节渲染为人类可读的表格。
44
+ * 最终 document.md 不应再出现原始 JSON——否则读者看到看不懂的 `decisions`/`interfaces` 结构。
45
+ *
46
+ * 判定:JSON 可解析且按 Entities schema 解析成功(含 interfaces/tables/decisions 任一非空)
47
+ * 才剥离;无法解析的块(叙述中的示例 JSON)原样保留,避免误删正文。
48
+ */
49
+ export declare function stripEntityJsonBlocks(text: string): string;
50
+ /**
51
+ * 剥离章节叙述首行的**冗余 H1 标题**:叙述以 `# <章节标题>` 开头(Agent 生成正文时习惯性地
52
+ * 重复了大纲章节标题),而 renderDocument 已为每章输出 `## <title>`——二者叠加造成标题重复。
53
+ * 仅当首行非空是 H1 且其内容与章节标题 trim 后一致时才剥离,避免误删正文中的真实内容。
54
+ */
55
+ export declare function stripLeadingTitle(text: string, chapterTitle: string): string;
56
+ export declare function renderDocument(input: RenderDocumentInput): string;
57
+ /** 附录 B · 去AI味自检(frontend-dev-guide §十一 → README §1.6 五维表)。 */
58
+ export declare function renderAppendixAntiAI(content?: string): string;
59
+ /** 附录 C · 下游交付衔接(frontend-dev-guide §八 → README §1.5 矩阵 + 三步)。 */
60
+ export declare function renderAppendixHandoff(): string;
61
+ export declare function renderHtml(markdown: string, title?: string): Promise<string>;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Document Engine — Pass 4 deterministic rendering (decisions 7/9).
3
+ *
4
+ * Renders the final document by concatenating chapters in outline order (NOT assemble):
5
+ * contract entities → Markdown tables/code blocks; narrative → verbatim into its chapter.
6
+ * Optional HTML render reuses `marked`.
7
+ */
8
+ import { marked } from 'marked';
9
+ import { parseEntities } from './schemas.js';
10
+ // ============= Escaping helpers(P1-5/6:HTML 与 markdown 注入面) =============
11
+ /** HTML 转义:用于 <title>/<h1> 插值(防存储型 XSS)。 */
12
+ export function escapeHtml(s) {
13
+ return String(s)
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&#39;');
19
+ }
20
+ /** Markdown 表格单元格转义:转义 | 与换行(防破坏列对齐/拆行)。 */
21
+ export function escapeTableCell(s) {
22
+ return String(s).replace(/\|/g, '\\|').replace(/\r?\n/g, ' ');
23
+ }
24
+ /** 代码围栏内容转义:把可能提前终止 ``` 围栏的行替换为安全形式。 */
25
+ export function escapeFenceContent(s) {
26
+ return String(s).split('\n').map((line) => (line.trimStart().startsWith('```') ? `\\\`\\\`\\\`${line.slice(3)}` : line)).join('\n');
27
+ }
28
+ /** 单行内联文本:转义换行,避免注入新标题/新行。 */
29
+ export function escapeInline(s) {
30
+ return String(s).replace(/\r?\n/g, ' ');
31
+ }
32
+ // ============= Contract entity → Markdown (T9.1) =============
33
+ export function renderInterface(i) {
34
+ const rows = [
35
+ `##### ${i.id} · ${escapeInline(i.name)}(${i.method} ${i.path})`,
36
+ '',
37
+ '**字段表**',
38
+ '',
39
+ '| 字段 | 类型 | 必填 | 说明 |',
40
+ '|------|------|------|------|',
41
+ ...[...i.request_fields ?? [], ...i.response_fields ?? []].map((f) => `| ${escapeTableCell(f.name)} | ${escapeTableCell(f.type)} | ${f.required ? '是' : '否'} | ${escapeTableCell(f.desc ?? '')} |`),
42
+ '',
43
+ '**成功示例**',
44
+ '',
45
+ '```http',
46
+ `${i.method} ${escapeFenceContent(i.path)}`,
47
+ '```',
48
+ '',
49
+ '**失败示例**',
50
+ '',
51
+ ...i.failure_examples.map((ex) => `- ${escapeInline(ex)}`),
52
+ '',
53
+ '**错误**',
54
+ '',
55
+ '| 条件 | 状态 | 说明 |',
56
+ '|------|------|------|',
57
+ ...i.errors.map((e) => `| ${escapeTableCell(e.condition)} | ${escapeTableCell(e.status)} | ${escapeTableCell(e.note ?? '')} |`),
58
+ ];
59
+ return rows.join('\n');
60
+ }
61
+ export function renderTable(t) {
62
+ return [
63
+ `##### ${t.id} · ${escapeInline(t.name)}`,
64
+ '',
65
+ '```sql',
66
+ escapeFenceContent(t.ddl),
67
+ '```',
68
+ '',
69
+ ...(t.migration ? ['**存量填充策略**', '', t.migration, ''] : []),
70
+ ...(t.rollback ? ['**回滚兼容**', '', t.rollback, ''] : []),
71
+ ].join('\n');
72
+ }
73
+ export function renderDecisions(decisions) {
74
+ if (decisions.length === 0)
75
+ return '';
76
+ return [
77
+ '**决策记录**',
78
+ '',
79
+ '| 决策 | 结论 |',
80
+ '|------|------|',
81
+ ...decisions.map((d) => `| ${escapeTableCell(d.id)} | ${escapeTableCell(d.text)} |`),
82
+ ].join('\n');
83
+ }
84
+ export function renderEntities(entities) {
85
+ const parts = [];
86
+ for (const i of entities.interfaces)
87
+ parts.push(renderInterface(i));
88
+ for (const t of entities.tables)
89
+ parts.push(renderTable(t));
90
+ const decisionsMd = renderDecisions(entities.decisions);
91
+ if (decisionsMd)
92
+ parts.push(decisionsMd);
93
+ return parts.join('\n\n');
94
+ }
95
+ /** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
96
+ * stays in `chapters/<id>.md` and `review-result.json`; the deliverable document must not show
97
+ * internal fix notes. */
98
+ export function stripReviewNotes(text) {
99
+ return text.replace(/<!--\s*review-fix[\s\S]*?-->/g, '').replace(/\n{3,}/g, '\n\n').trim();
100
+ }
101
+ /**
102
+ * 从章节叙述中剥离内嵌的**契约实体 JSON 块**(```json {"entities": {...}} ```)。
103
+ * 这些块是 Agent 写给引擎提取的机器中间格式(map 提示词要求 kind=entity|mixed 要点产出),
104
+ * 引擎已把其中实体提取进 entities.json,并在文档末尾「契约实体」章节渲染为人类可读的表格。
105
+ * 最终 document.md 不应再出现原始 JSON——否则读者看到看不懂的 `decisions`/`interfaces` 结构。
106
+ *
107
+ * 判定:JSON 可解析且按 Entities schema 解析成功(含 interfaces/tables/decisions 任一非空)
108
+ * 才剥离;无法解析的块(叙述中的示例 JSON)原样保留,避免误删正文。
109
+ */
110
+ export function stripEntityJsonBlocks(text) {
111
+ const jsonBlockRe = /```json\s*([\s\S]*?)```/g;
112
+ const parts = [];
113
+ let last = 0;
114
+ let m;
115
+ while ((m = jsonBlockRe.exec(text)) !== null) {
116
+ parts.push(text.slice(last, m.index));
117
+ let isEntityBlock = false;
118
+ try {
119
+ const parsed = parseEntities(JSON.parse(m[1]));
120
+ isEntityBlock = (parsed.interfaces?.length ?? 0) + (parsed.tables?.length ?? 0) + (parsed.decisions?.length ?? 0) > 0;
121
+ }
122
+ catch {
123
+ isEntityBlock = false;
124
+ }
125
+ if (!isEntityBlock)
126
+ parts.push(m[0]);
127
+ last = m.index + m[0].length;
128
+ }
129
+ parts.push(text.slice(last));
130
+ return parts.join('').replace(/\n{3,}/g, '\n\n').trim();
131
+ }
132
+ /**
133
+ * 剥离章节叙述首行的**冗余 H1 标题**:叙述以 `# <章节标题>` 开头(Agent 生成正文时习惯性地
134
+ * 重复了大纲章节标题),而 renderDocument 已为每章输出 `## <title>`——二者叠加造成标题重复。
135
+ * 仅当首行非空是 H1 且其内容与章节标题 trim 后一致时才剥离,避免误删正文中的真实内容。
136
+ */
137
+ export function stripLeadingTitle(text, chapterTitle) {
138
+ const trimmed = text.trim();
139
+ const firstLine = trimmed.split('\n', 1)[0] ?? '';
140
+ const m = /^#\s+(.*)$/.exec(firstLine.trim());
141
+ if (!m)
142
+ return text;
143
+ if (m[1].trim() !== chapterTitle.trim())
144
+ return text;
145
+ // 去掉首行后,剩余部分(去掉紧邻的空行)即为叙述正文。
146
+ const rest = trimmed.slice(firstLine.length).replace(/^\n+/, '');
147
+ return rest;
148
+ }
149
+ export function renderDocument(input) {
150
+ const { outline, entities, narratives, profile, appendixAntiAI } = input;
151
+ const body = [`# 方案文档:${escapeInline(outline.profile)}`, ''];
152
+ for (const ch of outline.chapters) {
153
+ body.push(`## ${escapeInline(ch.title)}`, '');
154
+ // 依次净化:剥内嵌契约 JSON 块(机器中间格式)→ 剥与章节标题重复的首行 H1 → 剥引擎修复注记。
155
+ const narrative = stripLeadingTitle(stripEntityJsonBlocks(stripReviewNotes(narratives.get(ch.id) ?? '')), ch.title);
156
+ if (narrative.trim())
157
+ body.push(narrative.trim(), '');
158
+ }
159
+ // Contract entities: append to the chapters that reference them (simplified: append in a
160
+ // final "契约" section, since entities are cross-chapter).
161
+ const contractMd = renderEntities(entities);
162
+ if (contractMd.trim()) {
163
+ body.push('---', '', '## 契约实体', '', contractMd, '');
164
+ }
165
+ // 产物溯源附录 (borrowed from approval · appendix A): each chapter's data source.
166
+ body.push('---', '', '## 附录 A: 产物溯源', '');
167
+ body.push('| 章节 | 数据来源 | 处理方式 |');
168
+ body.push('|------|---------|---------|');
169
+ for (const ch of outline.chapters) {
170
+ body.push(`| ${escapeTableCell(ch.title)} | 输入素材 / 项目规约 / 大纲要点 | LLM 生成 + 引擎校验 |`);
171
+ }
172
+ body.push('', '');
173
+ // 附录 B/C: rendered from profile.appendices (decision: deterministic templates, §7.3).
174
+ const appendices = profile?.appendices ?? [];
175
+ if (appendices.includes('anti-ai')) {
176
+ body.push('---', '', renderAppendixAntiAI(appendixAntiAI), '');
177
+ }
178
+ if (appendices.includes('handoff')) {
179
+ body.push('---', '', renderAppendixHandoff(), '');
180
+ }
181
+ return body.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
182
+ }
183
+ // ============= 附录 B/C(§7.3 确定性渲染) =============
184
+ /** 附录 B · 去AI味自检(frontend-dev-guide §十一 → README §1.6 五维表)。 */
185
+ export function renderAppendixAntiAI(content) {
186
+ const items = (content ?? '').trim();
187
+ const table = [
188
+ '## 附录 B: 去AI味自检',
189
+ '',
190
+ '> 每份交付末尾必须附「去AI味自检」,至少 3 条;缺一视为交付不完整(README §1.6)。',
191
+ '',
192
+ '| 维度 | 自检问题 | AI 味反例 |',
193
+ '|------|---------|----------|',
194
+ '| 不做决策 | 明确说了「不做什么」? | 只列功能清单 |',
195
+ '| 数字具体 | 指标/约束具体到数字? | 「提升体验」 |',
196
+ '| 竞品立场 | 与竞品差异有一句话立场? | 「借鉴头部最佳实践」 |',
197
+ '| 此刻合理性 | 为什么现在做?不做会怎样? | 不解释时机 |',
198
+ '| 优先级倾斜 | P0/P1 有倾斜逻辑? | 全功能同等权重 |',
199
+ '',
200
+ items
201
+ ? items
202
+ : '> (自检条目由生成流程填充,至少 3 条;缺失将触发引擎 warning)',
203
+ '',
204
+ ];
205
+ return table.join('\n');
206
+ }
207
+ /** 附录 C · 下游交付衔接(frontend-dev-guide §八 → README §1.5 矩阵 + 三步)。 */
208
+ export function renderAppendixHandoff() {
209
+ const matrix = [
210
+ '## 附录 C: 下游交付衔接',
211
+ '',
212
+ '| 下游角色 | 交付物 | 对方需要的核心信息 | 常见退货原因 |',
213
+ '|---------|--------|------------------|-------------|',
214
+ '| 后端 | 接口对接文档/契约 | 请求/响应/错误码约定、时序、分页排序约定 | 字段名与后端对不上、错误处理各写各的 |',
215
+ '| QA | 自测报告/测试范围 | 已测/未测项、环境地址、Mock 说明、已知问题 | 无自测记录、环境缺数据 |',
216
+ '| UI 设计师 | 还原实现(可访问预览) | 各断点/状态实现效果、偏差清单及原因 | 偏差无说明,被当 bug 打回 |',
217
+ '| DevOps | 构建产物/部署配置 | 环境变量清单、构建命令、SPA 路由回退、缓存策略 | 部署后白屏 404 |',
218
+ '',
219
+ '**交付动作三步**:',
220
+ '',
221
+ '1. **交前核对**:核心链路自测 + 契约确认 + 预览可访问。',
222
+ '2. **交底说明**:测什么 / 不测什么 / 已知问题。',
223
+ '3. **交后跟进**:Bug 24h 分级响应、首周每日巡检。',
224
+ '',
225
+ ];
226
+ return matrix.join('\n');
227
+ }
228
+ // ============= HTML (T9.3) =============
229
+ export async function renderHtml(markdown, title = '方案文档') {
230
+ const bodyHtml = await marked.parse(markdown);
231
+ const hasMermaid = bodyHtml.includes('class="language-mermaid"') || bodyHtml.includes('mermaid');
232
+ const mermaidScript = hasMermaid
233
+ ? '<script type="module">\nimport mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";\nmermaid.initialize({ startOnLoad: true });\n</script>'
234
+ : '';
235
+ const safeTitle = escapeHtml(title);
236
+ return `<!DOCTYPE html>
237
+ <html lang="zh-CN">
238
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" />
239
+ <title>${safeTitle}</title>
240
+ <style>body{max-width:920px;margin:0 auto;padding:2rem;font-family:system-ui,sans-serif;line-height:1.65}table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:.4rem}pre{background:#f6f8fa;padding:1rem;overflow-x:auto}</style>
241
+ </head>
242
+ <body><h1>${safeTitle}</h1><article>${bodyHtml}</article>${mermaidScript}</body></html>`;
243
+ }
@@ -0,0 +1,57 @@
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 { ReviewResult, ReviewFinding, Outline } from './schemas.js';
9
+ import type { DocumentLLM } from './llm.js';
10
+ export declare const AI_REVIEW_CHECKLIST: readonly ["R1 跨章命名一致", "R2 决策落地", "R3 实体↔正文", "R4 深度均衡", "R5 交叉引用稳定", "R6 图文一致", "R7 契约完整", "R8 枚举闭合", "R9 分支闭合", "R10 UI↔API 对齐", "R11 场景一致", "R12 stub 清除", "R13 结论一致", "R14 大纲要点覆盖"];
11
+ export interface ReviewPacket {
12
+ schema: 'specflow.document.review-packet/v1';
13
+ change: string;
14
+ outline: Outline;
15
+ chapters: Map<string, string>;
16
+ checklist: readonly string[];
17
+ /** decision 8/9 · P1-B: concrete narrative/mixed points the reviewer must verify (semantic channel). */
18
+ semanticChecklist: Array<{
19
+ chapter: string;
20
+ pointId: string;
21
+ text: string;
22
+ }>;
23
+ /** Previous round's findings (decision 9 · closed loop): the reviewer must verify each was
24
+ * addressed via the `<!-- review-fix -->` note in the chapter and not re-report fixed ones. */
25
+ prevFindings: ReviewFinding[];
26
+ }
27
+ export declare function buildReviewPacket(workRoot: string, change: string, outline: Outline, prevFindings?: ReviewFinding[],
28
+ /** 内存态章节覆盖(P2):persist=false 时磁盘无章节文件,传 narrativeMap 让评审者
29
+ * 看到真实内容而非空串;缺省时回退读磁盘。 */
30
+ narratives?: Map<string, string>): Promise<ReviewPacket>;
31
+ export declare function reviewWithAgent(llm: DocumentLLM, packet: ReviewPacket, prompt?: string): Promise<ReviewResult>;
32
+ /**
33
+ * 应用待修复的 findings。闭环语义(decision 9):
34
+ * - status==='open'(默认)→ 尚未解决,需要应用修复(追加 fix note / 重填实体)。
35
+ * - status==='fixed' → 评审者已确认上一轮注记生效,仅作闭环核对记账,不再应用。
36
+ * - status==='waived' → 豁免,不应用。
37
+ * 同一 finding 可能跨轮被回显,按 id 去重避免注记/修复重复累积。
38
+ */
39
+ export declare function applyFindings(workRoot: string, findings: ReviewFinding[], apply: (finding: ReviewFinding) => Promise<void>, appliedIds?: Set<string>): Promise<void>;
40
+ export declare function sha256(content: string): string;
41
+ export declare function computePartHashes(workRoot: string, chapterIds: string[]): Promise<{
42
+ id: string;
43
+ sha256: string;
44
+ }[]>;
45
+ export interface ReviewCheckResult {
46
+ ok: boolean;
47
+ diagnostics: Array<{
48
+ code: string;
49
+ severity: 'error' | 'warning';
50
+ message: string;
51
+ }>;
52
+ }
53
+ export declare function checkReviewResult(workRoot: string, review: ReviewResult, outline: Outline, currentHashesParam?: Array<{
54
+ id: string;
55
+ sha256: string;
56
+ }>): Promise<ReviewCheckResult>;
57
+ export declare function writeReviewResult(workRoot: string, review: ReviewResult): Promise<void>;