@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
@@ -1,384 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { promises as fs } from 'node:fs';
3
- import yaml from 'js-yaml';
4
- import { parseApprovalIndex } from './index-schema.js';
5
- import { findForbiddenMatch } from './forbidden-patterns.js';
6
- import { getApprovalIndexPath, getApprovalManifestPath, getApprovalOutputPath, getApprovalPartPath, getApprovalPartsDir, } from './paths.js';
7
- const OPTIONAL_PREFIXES = {
8
- s5: ['05-', '05.'],
9
- s7: ['07-', '07.'],
10
- s8: ['08-', '08.'],
11
- };
12
- const CHAPTER_HEADERS = {
13
- '01-intro': '## 1. 绪论与边界 (Introduction & Boundaries)',
14
- '02-design-review': '## 2. 技术方案评估 (Technical Design Review)',
15
- '03-architecture': '## 3. 架构整体设计 (Architecture Design)',
16
- '05-acceptance': '## 5. 验收标准 (Acceptance Criteria)',
17
- '06-test': '## 6. 测试策略 (Test Strategy)',
18
- '07-deploy': '## 7. 部署/发布/回滚方案 (Deployment & Release)',
19
- '08-closed-loop': '## 8. 闭环性检查 (Closed-Loop Verification)',
20
- '09-implementability': '## 9. 可实施性评估 (Implementability Assessment)',
21
- '10-signoff': '## 10. 审批意见 (Approval Decision)',
22
- 'appendix-a': '## 附录 A: 产物溯源',
23
- };
24
- const SECTION4_HEADER = '## 4. 方案详细设计 (Detailed Design)';
25
- const MIN_PART_BYTES = {
26
- '04.4': 200,
27
- '04.5': 200,
28
- '04.6': 150,
29
- '05-': 100,
30
- };
31
- function sha256(content) {
32
- return createHash('sha256').update(content, 'utf8').digest('hex');
33
- }
34
- function isOptionalPart(partId, optional) {
35
- if (OPTIONAL_PREFIXES.s5.some((p) => partId.startsWith(p))) {
36
- return !optional.s5;
37
- }
38
- if (OPTIONAL_PREFIXES.s7.some((p) => partId.startsWith(p))) {
39
- return !optional.s7;
40
- }
41
- if (OPTIONAL_PREFIXES.s8.some((p) => partId.startsWith(p))) {
42
- return !optional.s8;
43
- }
44
- return false;
45
- }
46
- function isSection4Part(partId) {
47
- return partId === '04-detail-core' || partId.startsWith('04.');
48
- }
49
- function headerForPart(partId) {
50
- if (isSection4Part(partId)) {
51
- return null;
52
- }
53
- if (CHAPTER_HEADERS[partId]) {
54
- return CHAPTER_HEADERS[partId];
55
- }
56
- for (const [key, header] of Object.entries(CHAPTER_HEADERS)) {
57
- if (partId.startsWith(key)) {
58
- return header;
59
- }
60
- }
61
- return null;
62
- }
63
- function detectLazyContent(partId, content) {
64
- const trimmed = content.trim();
65
- if (trimmed.length < 20) {
66
- return 'part content too short (< 20 chars)';
67
- }
68
- for (const prefix of Object.keys(MIN_PART_BYTES)) {
69
- if (partId.startsWith(prefix) && trimmed.length < MIN_PART_BYTES[prefix]) {
70
- return `part content below minimum for ${prefix} (${trimmed.length} bytes)`;
71
- }
72
- }
73
- const forbidden = findForbiddenMatch(trimmed);
74
- if (forbidden) {
75
- return `lazy/stub pattern detected: ${forbidden.source}`;
76
- }
77
- if (/^#+\s*4\.\s/m.test(trimmed)) {
78
- return 'part must not contain top-level "## 4." heading (assemble injects section 4)';
79
- }
80
- if (/^##\s+[1-9]\d?\./m.test(trimmed) && !partId.startsWith('05-c')) {
81
- const match = trimmed.match(/^##\s+([1-9]\d?)\./m);
82
- if (match && !partId.startsWith(`${match[1].padStart(2, '0')}`)) {
83
- return 'part must not inject foreign chapter "## N." headings';
84
- }
85
- }
86
- return null;
87
- }
88
- function partHasAppendixA(partContents) {
89
- for (const content of partContents.values()) {
90
- if (/^#{2,3}\s+附录\s*A/m.test(content)) {
91
- return true;
92
- }
93
- }
94
- return false;
95
- }
96
- function buildDocumentHeader(index) {
97
- const meta = index.meta ?? {};
98
- const lang = meta.language ?? 'en';
99
- const stack = meta.tech_stack ?? 'unknown';
100
- const projectMode = meta.project_mode ?? 'brownfield';
101
- const modeNote = index.mode === 'segmented' ? 'segmented+assemble' : 'monolithic';
102
- let multiRepoBlock = '';
103
- const mr = index.multi_repo;
104
- if (mr?.enabled && mr.repos.length >= 2) {
105
- const mode = mr.document_mode ?? 'pending';
106
- const primary = mr.primary_repo ?? '—';
107
- const bundleNote = mr.bundle?.enabled ? ' | 合订: approval bundle (只读)' : '';
108
- const repoLines = mr.repos
109
- .map((r) => `- ${r.label} \`${r.id}/${r.change}\``)
110
- .join('\n');
111
- multiRepoBlock = `
112
- > **多仓范围** (${mode}${mode === 'unified' ? `, 主仓: ${primary}` : mr.bundle?.enabled ? `, bundle 主仓: ${primary}` : ''}${bundleNote}):
113
- ${repoLines}
114
- `;
115
- }
116
- return `# 技术方案审批文档: ${index.change}
117
- ${multiRepoBlock}
118
- > 本文档由 \`/specflow:approval\` 基于 refine 收敛后的四件套 + 现有代码与 spec 基线生成,
119
- > 含架构与详细设计等章节,供人工审批使用。AI 闭环/预审结论在对话中反馈,不写入本文。
120
- > 生成时间: ${index.generated_at} | phase: refined | 产物语言: ${lang} | 技术栈: ${stack} | 项目模式: ${projectMode} | 生成模式: ${modeNote}
121
-
122
- > **质量红线(必须满足)**:G1 超 5 行流程→图示 · G2 接口须有失败示例 · G3 JSON/新列须有存量填充策略 · G4 须有回滚数据兼容说明 · G5 有 UI 须有页面/路由清单+栈五元组 · G6 关键页须有空/加载/错态。
123
- > **文风**:通俗中文、缩写首次注解、禁用含糊词;**正文禁止代码腔**(路径/函数名堆砌→可读中文)。
124
-
125
- ---
126
- `;
127
- }
128
- function buildAppendix() {
129
- return `
130
- ---
131
-
132
- ## 附录 A: 产物溯源
133
-
134
- | 章节 | 数据来源 | 处理方式 |
135
- |------|---------|---------|
136
- | 绪论与边界 | proposal + explore(可选) | AI 提炼 |
137
- | 技术方案评估 | design + 用户确认选型(若有) | 约束 + 技术选型 + 决策/风险/质量 |
138
- | 架构 / 详细设计 | design + specs + 锚点 + 约定 | 图+要点+数据/接口/前端 |
139
- | 验收标准 | specs | **可选** |
140
- | 测试策略 | §5 或 specs 名 | 分层矩阵 |
141
- | 部署/闭环 | 运行环境 / Pass 1–7 | **可选** |
142
- | 可实施性 | 四件套+代码 | 写入文档 |
143
- | 审批意见 | 人工 | 仅签字栏 |
144
- `;
145
- }
146
- export async function readApprovalIndex(changeDir) {
147
- const raw = yaml.load(await fs.readFile(getApprovalIndexPath(changeDir), 'utf-8'));
148
- return parseApprovalIndex(raw);
149
- }
150
- export async function readApprovalManifest(changeDir) {
151
- const path = getApprovalManifestPath(changeDir);
152
- try {
153
- const raw = JSON.parse(await fs.readFile(path, 'utf-8'));
154
- return raw;
155
- }
156
- catch {
157
- return null;
158
- }
159
- }
160
- export async function assembleApprovalDocument(options) {
161
- const diagnostics = [];
162
- const { changeDir, checkOnly = false, force = false, validateLazy = true } = options;
163
- let index;
164
- try {
165
- index = await readApprovalIndex(changeDir);
166
- }
167
- catch (error) {
168
- const message = error instanceof Error ? error.message : String(error);
169
- return {
170
- ok: false,
171
- diagnostics: [
172
- {
173
- code: 'missing_index',
174
- severity: 'error',
175
- message: `Cannot read approval/index.yaml: ${message}`,
176
- },
177
- ],
178
- };
179
- }
180
- if (index.multi_repo?.enabled) {
181
- const mr = index.multi_repo;
182
- if (!mr.document_mode) {
183
- diagnostics.push({
184
- code: 'multi_repo_mode_missing',
185
- severity: 'error',
186
- message: 'multi_repo.enabled but document_mode not set; ask user unified vs per_repo before assemble',
187
- });
188
- }
189
- if (mr.document_mode === 'unified') {
190
- if (!mr.primary_repo) {
191
- diagnostics.push({
192
- code: 'multi_repo_primary_missing',
193
- severity: 'error',
194
- message: 'multi_repo.document_mode=unified requires primary_repo',
195
- });
196
- }
197
- else {
198
- const primary = mr.repos.find((r) => r.id === mr.primary_repo);
199
- if (primary && primary.change !== index.change) {
200
- diagnostics.push({
201
- code: 'multi_repo_change_mismatch',
202
- severity: 'warning',
203
- message: `index.change "${index.change}" differs from primary repo change "${primary.change}"; assemble in primary planning root`,
204
- });
205
- }
206
- }
207
- }
208
- if (mr.document_mode === 'per_repo' && !mr.outputs?.per_repo?.length) {
209
- diagnostics.push({
210
- code: 'multi_repo_outputs_missing',
211
- severity: 'warning',
212
- message: 'per_repo mode should list outputs.per_repo[] with each repo approval path',
213
- });
214
- }
215
- }
216
- const partsDir = getApprovalPartsDir(changeDir);
217
- try {
218
- await fs.access(partsDir);
219
- }
220
- catch {
221
- diagnostics.push({
222
- code: 'missing_parts_dir',
223
- severity: 'error',
224
- message: `Missing approval/parts/ directory at ${partsDir}`,
225
- });
226
- return { ok: false, diagnostics };
227
- }
228
- const partContents = new Map();
229
- let section4Opened = false;
230
- const body = [buildDocumentHeader(index)];
231
- for (const partId of index.parts_order) {
232
- if (isOptionalPart(partId, index.optional)) {
233
- continue;
234
- }
235
- const partPath = getApprovalPartPath(changeDir, partId);
236
- let content;
237
- try {
238
- content = await fs.readFile(partPath, 'utf-8');
239
- }
240
- catch {
241
- diagnostics.push({
242
- code: 'missing_part',
243
- severity: 'error',
244
- message: `Missing part file for "${partId}" at ${partPath}`,
245
- part: partId,
246
- });
247
- continue;
248
- }
249
- if (validateLazy) {
250
- const lazy = detectLazyContent(partId, content);
251
- if (lazy) {
252
- diagnostics.push({
253
- code: 'lazy_part_content',
254
- severity: 'error',
255
- message: `Part "${partId}": ${lazy}`,
256
- part: partId,
257
- });
258
- }
259
- }
260
- partContents.set(partId, content);
261
- if (isSection4Part(partId)) {
262
- if (!section4Opened) {
263
- body.push(SECTION4_HEADER, '', '---', '');
264
- section4Opened = true;
265
- }
266
- body.push(content.trim(), '');
267
- continue;
268
- }
269
- const header = headerForPart(partId);
270
- if (header) {
271
- body.push(header, '');
272
- }
273
- body.push(content.trim(), '', '---', '');
274
- }
275
- if (!index.parts_order.includes('appendix-a') && !partHasAppendixA(partContents)) {
276
- body.push(buildAppendix().trim());
277
- }
278
- for (const table of index.tables) {
279
- if (!partContents.has(table.part)) {
280
- diagnostics.push({
281
- code: 'id_part_mismatch',
282
- severity: 'error',
283
- message: `Table "${table.id}" references missing part "${table.part}"`,
284
- part: table.part,
285
- });
286
- }
287
- }
288
- for (const iface of index.interfaces) {
289
- if (!partContents.has(iface.part)) {
290
- diagnostics.push({
291
- code: 'id_part_mismatch',
292
- severity: 'error',
293
- message: `Interface "${iface.id}" references missing part "${iface.part}"`,
294
- part: iface.part,
295
- });
296
- }
297
- }
298
- if (index.ui_in_scope) {
299
- for (const page of index.pages) {
300
- if (!partContents.has(page.part)) {
301
- diagnostics.push({
302
- code: 'id_part_mismatch',
303
- severity: 'error',
304
- message: `Page "${page.id}" references missing part "${page.part}"`,
305
- part: page.part,
306
- });
307
- }
308
- }
309
- }
310
- for (const partId of index.parts_order) {
311
- if (isOptionalPart(partId, index.optional)) {
312
- continue;
313
- }
314
- if (!partContents.has(partId)) {
315
- continue;
316
- }
317
- }
318
- const diskParts = await fs.readdir(partsDir);
319
- for (const file of diskParts) {
320
- if (!file.endsWith('.md')) {
321
- continue;
322
- }
323
- const id = file.replace(/\.md$/, '');
324
- const optionalSkipped = isOptionalPart(id, index.optional);
325
- const inOrder = index.parts_order.includes(id);
326
- if (!inOrder && !optionalSkipped) {
327
- diagnostics.push({
328
- code: 'orphan_part',
329
- severity: 'warning',
330
- message: `Part file "${file}" exists but is not listed in index.parts_order`,
331
- part: id,
332
- });
333
- }
334
- if (optionalSkipped && inOrder) {
335
- diagnostics.push({
336
- code: 'optional_present_but_disabled',
337
- severity: 'warning',
338
- message: `Optional chapter disabled but part "${id}" is still in parts_order`,
339
- part: id,
340
- });
341
- }
342
- }
343
- const hasError = diagnostics.some((d) => d.severity === 'error');
344
- if (hasError) {
345
- return { ok: false, diagnostics };
346
- }
347
- const markdown = body.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
348
- const outputPath = getApprovalOutputPath(changeDir);
349
- if (checkOnly) {
350
- return { ok: true, markdown, outputPath, diagnostics };
351
- }
352
- if (!force) {
353
- try {
354
- await fs.access(outputPath);
355
- diagnostics.push({
356
- code: 'output_exists',
357
- severity: 'warning',
358
- message: `${outputPath} already exists; use --force to overwrite`,
359
- });
360
- return { ok: false, diagnostics, markdown, outputPath };
361
- }
362
- catch {
363
- // ok to write
364
- }
365
- }
366
- await fs.writeFile(outputPath, markdown, 'utf-8');
367
- const manifest = {
368
- schema: 'specflow.approval.manifest/v1',
369
- change: index.change,
370
- parts: index.parts_order
371
- .filter((id) => partContents.has(id))
372
- .map((id) => ({
373
- id,
374
- path: `approval/parts/${id}.md`,
375
- status: 'ok',
376
- sha256: sha256(partContents.get(id)),
377
- updated_at: index.generated_at,
378
- })),
379
- assembled_at: new Date().toISOString(),
380
- approval_sha256: sha256(markdown),
381
- };
382
- await fs.writeFile(getApprovalManifestPath(changeDir), JSON.stringify(manifest, null, 2), 'utf-8');
383
- return { ok: true, markdown, outputPath, diagnostics };
384
- }
@@ -1,9 +0,0 @@
1
- import type { ApprovalBundleResult } from './types.js';
2
- export interface BundleApprovalOptions {
3
- readonly workspaceRoot: string;
4
- readonly changeDir: string;
5
- readonly playbookPath: string;
6
- readonly write?: boolean;
7
- }
8
- export declare function bundleApprovalDocument(options: BundleApprovalOptions): Promise<ApprovalBundleResult>;
9
- export declare function resolvePlaybookPath(changeDir: string, explicitPath?: string): Promise<string>;
@@ -1,172 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { promises as fs } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import yaml from 'js-yaml';
5
- import { parseApprovalPlaybook } from './playbook-schema.js';
6
- import { readApprovalIndex } from './assemble.js';
7
- import { getApprovalPartPath, getApprovalWorkspaceDir, getChangeDirectory, } from './paths.js';
8
- function sha256(content) {
9
- return createHash('sha256').update(content, 'utf8').digest('hex');
10
- }
11
- function resolveSourceChangeDir(workspaceRoot, source) {
12
- const planningRoot = source.root ?? workspaceRoot;
13
- return getChangeDirectory(planningRoot, source.change);
14
- }
15
- async function loadSourceParts(changeDir, includeParts) {
16
- const index = await readApprovalIndex(changeDir);
17
- const partIds = includeParts === 'all'
18
- ? [...index.parts_order]
19
- : includeParts.filter((id) => index.parts_order.includes(id));
20
- const results = [];
21
- for (const partId of partIds) {
22
- try {
23
- const content = await fs.readFile(getApprovalPartPath(changeDir, partId), 'utf-8');
24
- results.push({ partId, content });
25
- }
26
- catch {
27
- // skip missing
28
- }
29
- }
30
- return results;
31
- }
32
- export async function bundleApprovalDocument(options) {
33
- const diagnostics = [];
34
- const { workspaceRoot, changeDir, playbookPath, write = true } = options;
35
- let playbook;
36
- let indexOutputOverride;
37
- try {
38
- const raw = yaml.load(await fs.readFile(playbookPath, 'utf-8'));
39
- playbook = parseApprovalPlaybook(raw);
40
- try {
41
- const index = await readApprovalIndex(changeDir);
42
- indexOutputOverride = index.multi_repo?.bundle?.output;
43
- }
44
- catch {
45
- // index optional for bundle-only dry runs
46
- }
47
- }
48
- catch (error) {
49
- const message = error instanceof Error ? error.message : String(error);
50
- return {
51
- ok: false,
52
- diagnostics: [
53
- { code: 'invalid_playbook', severity: 'error', message: `Invalid playbook: ${message}` },
54
- ],
55
- };
56
- }
57
- const body = [];
58
- body.push(`# ${playbook.title}`);
59
- body.push('');
60
- body.push(`> **合订视图** (read-only bundle) | change: \`${playbook.change}\``);
61
- body.push(`> 生成方式: \`specflow approval bundle\` — 确定性拼接,禁止 LLM Reduce`);
62
- body.push(`> 主仓: \`${playbook.primary_repo}\``);
63
- body.push('');
64
- body.push('---');
65
- body.push('');
66
- if (playbook.overlay && playbook.overlay.length > 0) {
67
- const approvalDir = getApprovalWorkspaceDir(changeDir);
68
- for (const overlayRel of playbook.overlay) {
69
- const overlayPath = join(approvalDir, overlayRel);
70
- try {
71
- const content = await fs.readFile(overlayPath, 'utf-8');
72
- if (playbook.assemble.inject_headers !== false) {
73
- body.push(`## Overlay · ${overlayRel}`);
74
- body.push('');
75
- }
76
- body.push(content.trim());
77
- body.push('');
78
- body.push('---');
79
- body.push('');
80
- }
81
- catch {
82
- diagnostics.push({
83
- code: 'overlay_missing',
84
- severity: 'error',
85
- message: `Overlay file not found: ${overlayPath}`,
86
- });
87
- }
88
- }
89
- }
90
- for (const source of playbook.sources) {
91
- const sourceChangeDir = resolveSourceChangeDir(workspaceRoot, source);
92
- try {
93
- await fs.access(sourceChangeDir);
94
- }
95
- catch {
96
- diagnostics.push({
97
- code: 'source_change_missing',
98
- severity: 'error',
99
- message: `Source change not found: ${sourceChangeDir} (repo=${source.repo})`,
100
- });
101
- continue;
102
- }
103
- const includeParts = source.include_parts ?? 'all';
104
- const parts = await loadSourceParts(sourceChangeDir, includeParts);
105
- if (parts.length === 0) {
106
- diagnostics.push({
107
- code: 'source_parts_empty',
108
- severity: 'warning',
109
- message: `No parts loaded for source ${source.repo}/${source.change}`,
110
- });
111
- }
112
- if (playbook.assemble.inject_headers !== false) {
113
- body.push(`## 来源 · ${source.repo} (\`${source.change}\`)`);
114
- body.push('');
115
- }
116
- for (const { partId, content } of parts) {
117
- body.push(`<!-- bundle-source: ${source.repo}/${source.change}/${partId} -->`);
118
- body.push(content.trim());
119
- body.push('');
120
- }
121
- body.push('---');
122
- body.push('');
123
- }
124
- if (diagnostics.some((d) => d.severity === 'error')) {
125
- return { ok: false, diagnostics };
126
- }
127
- let markdown = body.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
128
- if (playbook.assemble.dedupe_appendix !== false) {
129
- const appendixMatches = markdown.match(/^##\s+附录\s*A/mg);
130
- if (appendixMatches && appendixMatches.length > 1) {
131
- diagnostics.push({
132
- code: 'duplicate_appendix',
133
- severity: 'warning',
134
- message: `Bundle output contains ${appendixMatches.length} "## 附录 A" headings`,
135
- });
136
- }
137
- }
138
- if (playbook.assemble.readonly) {
139
- markdown =
140
- `> **READ-ONLY VIEW** — apply 请使用各仓 \`approval.md\` 真源,勿直接消费本合订文件。\n\n` +
141
- markdown;
142
- }
143
- const outputRel = indexOutputOverride ?? playbook.assemble.output;
144
- const outputPath = join(changeDir, outputRel);
145
- if (!write) {
146
- return { ok: true, markdown, outputPath, diagnostics };
147
- }
148
- await fs.mkdir(dirname(outputPath), { recursive: true });
149
- await fs.writeFile(outputPath, markdown, 'utf-8');
150
- const bundleManifestPath = join(getApprovalWorkspaceDir(changeDir), 'bundle-manifest.json');
151
- await fs.writeFile(bundleManifestPath, JSON.stringify({
152
- schema: 'specflow.approval.bundle-manifest/v1',
153
- change: playbook.change,
154
- playbook: playbookPath,
155
- output: outputRel,
156
- sha256: sha256(markdown),
157
- assembled_at: new Date().toISOString(),
158
- sources: playbook.sources.map((s) => ({ repo: s.repo, change: s.change })),
159
- }, null, 2), 'utf-8');
160
- return { ok: true, markdown, outputPath, diagnostics };
161
- }
162
- export async function resolvePlaybookPath(changeDir, explicitPath) {
163
- if (explicitPath) {
164
- return explicitPath;
165
- }
166
- const index = await readApprovalIndex(changeDir);
167
- const fromIndex = index.multi_repo?.bundle?.playbook;
168
- if (fromIndex) {
169
- return join(getApprovalWorkspaceDir(changeDir), fromIndex);
170
- }
171
- return join(getApprovalWorkspaceDir(changeDir), 'approval-playbook.yaml');
172
- }
@@ -1,40 +0,0 @@
1
- /** Shared stub / inconsistency patterns for assemble lazy-check and approval lint. */
2
- export const FORBIDDEN_STUB_PATTERNS = [
3
- /\bTODO\b/i,
4
- /待补充/,
5
- /待 refine 澄清(?!.*\[)/,
6
- /此处省略/,
7
- /详见\s*(?:上文|下文|附件)(?!.*§)/,
8
- /略\s*[。.]?$/,
9
- /TBD\s*[。.]?$/,
10
- /(?:^|\n)\s*\.{3}\s*(?:\n|$)/,
11
- /(?:^|\n)\s*(?:同上|同前)\s*[。.]?\s*(?:\n|$)/,
12
- /实现时(?:命名|对齐)/,
13
- /(?:RPC|rpc).*暂定/,
14
- /暂定\s*[`']?\w+[`']?/,
15
- /\b如\s+[A-Z]\w*(?:Result|Request|Response)/,
16
- /内部(?:经|调用)\s*I\d+[^.\n]{0,40}(?:一行|代替|省略)/,
17
- ];
18
- /** Semantic inconsistency hints (warnings unless strict mode) — long-document generic. */
19
- export const INCONSISTENCY_HINT_PATTERNS = [
20
- /约\s*\d+\s*次/,
21
- /\b(?:等|etc\.?)\s*[。.)]/,
22
- /(?:status|状态|enum|枚举)[^.\n]{0,30}(?:等|etc)/i,
23
- /(?:^|[\s,。;])同前(?!缀)/,
24
- /(?:^|[\s,。;])同上(?!所述)/,
25
- /(?:^|[\s,。;])见上(?!游)/,
26
- /\b(?:待定|未定)\b/i,
27
- /(?<![无未不否])\bTBD\b/i,
28
- /\d+\s*[或\\/]\s*\d+\s*(?:均可|都行)/,
29
- ];
30
- export function findForbiddenMatch(content) {
31
- for (const pattern of FORBIDDEN_STUB_PATTERNS) {
32
- if (pattern.test(content)) {
33
- return pattern;
34
- }
35
- }
36
- return null;
37
- }
38
- export function findInconsistencyHints(content) {
39
- return INCONSISTENCY_HINT_PATTERNS.filter((p) => p.test(content));
40
- }