@gordon.gan/specflow 1.8.2-beta → 1.8.4-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.
@@ -717,12 +717,15 @@ function buildSynthRules(dirs) {
717
717
  return [
718
718
  `## 跨仓合成规则(必须遵守)`,
719
719
  `1. **统一大纲**:一次生成一份 outline,章节覆盖所有仓,不按仓分章(跨仓对比/汇总放进对应章节)。`,
720
- `2. **契约实体命名空间**:跨仓实体 id 用 \`<repo>_<id>\` 前缀(如 \`repo1_I1\`、\`repo2_T1\`),禁止不同仓共用裸 id。`,
721
- `3. **每章必须分仓小节(质量门禁,validate 强制)**:每个必选章节的叙述按仓分节,用 \`### <repo>\` 小节标题(可带后缀如 \`### repo1 · 目标\`),且每个仓(${repoList})都必须出现。禁止把多仓内容混写成一段(会导致细节互相稀释)。`,
722
- `4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection 对比各仓技术方案;api-design/data-model 分仓列契约;closed-loop 跨仓统一闭环检查;implementability 跨仓统一可实施性评估。其他场景章节(如 reproduce/root-cause/fix、requirement/test-strategy、research/poc-demo/benchmark)同样按仓分节并给出跨仓结论。`,
720
+ `2. **契约单一定义**:同一物理端点(method+path)只定义一个契约实体,用 \`side\`(console/worker/internal)与 \`consumers\`/\`producers\`(仓名)标注面与消费方/生产方;\`<repo>_<id>\` 前缀仅作为追溯别名保留,禁止为不同仓重复定义同一接口(validate 会以 \`duplicate_contract\` 拦截)。`,
721
+ `3. **整体性组织(替代按仓分节)**:每章按主题写成一段整体叙述,仓归属用内联 \`[${repoList.split('、').join('] / [')}]\` 标注或表格「归属」列;**禁止**为无内容的仓写占位小节(如「某仓无 UI」);若确需 \`### <repo>\` 小节,小节必须有实质内容(validate 会以 \`repo_section_stub\` 拦截空壳)。`,
722
+ `4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection 对比各仓技术方案;**接口与数据合并为「接口与数据设计」章**(接口总览 → 逐接口明细 → 数据模型与契约-表映射 → 存量回滚,DDL 就近引用附录),含契约-表映射表(每接口 ↔ 读写表/列);closed-loop 跨仓统一闭环检查;implementability 跨仓统一可实施性评估。`,
723
723
  `5. **跨仓依赖/风险**:涉及仓间依赖(接口调用/数据共享/发布顺序)时显式标注「跨仓依赖」。`,
724
724
  `6. **按需加载规约(重要)**:写某个仓的小节时,**只读取该仓**的规约/IDE rules/guidance(该仓的 \`docs/\`、\`.cursor\`、\`.claude\`、\`.agents\` 等),**禁止一次性读取全部仓的规约**;只有写跨仓内容(接口对接/数据共享/发布顺序)时才读取**涉及仓**的规约。未读取的仓规约不得臆测,必要时标注「未读取该仓规约,待确认」。`,
725
725
  `7. **输入归属**:每段输入带「来源仓」标记,段 id 携带仓前缀(如 \`seg-repo1-proposal-01\`);引用原文时按段 id 精确取回对应仓的片段,不整篇重读。`,
726
+ `8. **完整性骨架**:合成文档必须含术语与约定表、非目标汇总、风险与开放问题、决策记录(ADR,正文用 \`[D#]\` 引用,决策先定义后引用,validate 以 \`forward_decision_ref\` 提示前向引用)。`,
727
+ `9. **示例与字段表**:接口实体必须填 \`request_example\`/\`success_response_example\`/\`failure_response_example\`(真实 JSON body,无请求体方法显式标注「无请求体」);请求字段与响应字段分列填写;错误表按 \`backend\`(gRPC→HTTP 映射)/ \`guard\`(前端守卫行为)/ \`internal\`(Worker 内部语义)分类。`,
728
+ `10. **主流程必须含图**:主业务流程章输出整体流程图(含失败分支)+ 时序图 + 失败路径决策图,图必须有文字说明(validate 以 diagram 门禁拦截缺图)。`,
726
729
  ].join('\n');
727
730
  }
728
731
  /**
@@ -17,7 +17,7 @@ import { renderDocument, renderHtml } from './render.js';
17
17
  import { runGates } from './gates.js';
18
18
  import { resolvePaths, reposPath, chapterPath } from './paths.js';
19
19
  import { parseEntitiesLenient } from './schemas.js';
20
- import { lintNarrative, checkRepoSectionCoverage } from './lint.js';
20
+ import { lintNarrative, checkRepoCoverage, checkRepoSectionStub, checkDuplicateContract, checkExamplePresence, checkForwardDecisionRef, checkUnverifiedNumber, checkDataContractMapping } from './lint.js';
21
21
  import { checkStructuredCoverage } from './coverage.js';
22
22
  import { resolveProjectConventionPaths } from '../project-conventions.js';
23
23
  import { extractContracts, EMPTY_CONTRACTS } from './extract.js';
@@ -227,8 +227,18 @@ export async function runDocument(options) {
227
227
  await writeReviewResult(workRoot, review);
228
228
  }
229
229
  // --- Pass 4: render (T9) ---
230
+ // Multi-repo synthesize marker: repos.json (written by document synthesize) — 提前读取,
231
+ // 供渲染文档头元信息与 runGates 整体性门禁共用(Agent 模式 validateWork 亦同)。
232
+ let synthRepos = [];
233
+ try {
234
+ const raw = JSON.parse(await fs.readFile(reposPath(workRoot), 'utf-8'));
235
+ synthRepos = Array.isArray(raw) ? raw.filter((r) => typeof r === 'string') : [];
236
+ }
237
+ catch {
238
+ // no repos.json → not a synthesize workRoot → gate skipped (bundle/run unaffected)
239
+ }
230
240
  const appendixAntiAI = await readAppendix(workRoot, 'anti-ai');
231
- const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI });
241
+ const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI, repos: synthRepos.length > 0 ? synthRepos : undefined });
232
242
  if (persist)
233
243
  await fs.writeFile(paths.documentMd, documentMd, 'utf-8');
234
244
  let documentHtml;
@@ -238,16 +248,6 @@ export async function runDocument(options) {
238
248
  await fs.writeFile(paths.documentHtml, documentHtml, 'utf-8');
239
249
  }
240
250
  // --- Gates (T10) ---
241
- // Multi-repo synthesize marker: repos.json (written by document synthesize) → runGates enforces
242
- // the per-repo section gate in the library pipeline too (not only Agent-mode validateWork).
243
- let synthRepos = [];
244
- try {
245
- const raw = JSON.parse(await fs.readFile(reposPath(workRoot), 'utf-8'));
246
- synthRepos = Array.isArray(raw) ? raw.filter((r) => typeof r === 'string') : [];
247
- }
248
- catch {
249
- // no repos.json → not a synthesize workRoot → gate skipped (bundle/run unaffected)
250
- }
251
251
  const gates = await runGates({
252
252
  outline,
253
253
  chapterNarratives: narrativeMap,
@@ -675,8 +675,8 @@ export async function validateWork(options) {
675
675
  }
676
676
  }
677
677
  // Multi-repo synthesize quality gate: if repos.json exists (document synthesize wrote it),
678
- // every required chapter must have a per-repo section (`### <repo>`) for ALL repos.
679
- // This prevents "伪统一" — cross-repo content diluting each other into vague prose.
678
+ // 整体性组织门禁——全文级仓覆盖(防某仓被概括掉)+ 空壳小节拦截(禁「某仓无 X」占位),
679
+ // 替代旧「每章必须 ### <repo> 分仓小节」;另附契约质量 lint(与 runGates 库模式一致)。
680
680
  let synthRepos = [];
681
681
  try {
682
682
  const raw = JSON.parse(await fs.readFile(reposPath(workRoot), 'utf-8'));
@@ -686,18 +686,34 @@ export async function validateWork(options) {
686
686
  // no repos.json → not a synthesize workRoot → gate skipped (bundle/run unaffected)
687
687
  }
688
688
  if (synthRepos.length >= 2) {
689
- const requiredIds = new Set(profile.required);
690
- for (const ch of outline.chapters) {
691
- if (!requiredIds.has(ch.id))
692
- continue;
693
- const narrative = narrativeMap.get(ch.id);
694
- if (narrative === undefined)
695
- continue; // chapter_missing already reported
696
- for (const d of checkRepoSectionCoverage(ch.id, narrative, synthRepos)) {
697
- diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message, chapter: ch.id });
689
+ for (const d of checkRepoCoverage(synthRepos, narrativeMap)) {
690
+ diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message });
691
+ }
692
+ for (const [chId, narrative] of narrativeMap) {
693
+ for (const d of checkRepoSectionStub(chId, narrative, synthRepos)) {
694
+ diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message, chapter: chId });
698
695
  }
699
696
  }
700
697
  }
698
+ for (const d of checkDuplicateContract(allEntities.interfaces)) {
699
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
700
+ }
701
+ for (const d of checkExamplePresence(allEntities.interfaces)) {
702
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
703
+ }
704
+ for (const d of checkForwardDecisionRef(narrativeMap, allEntities.decisions)) {
705
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
706
+ }
707
+ for (const [chId, narrative] of narrativeMap) {
708
+ for (const d of checkUnverifiedNumber(chId, narrative)) {
709
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
710
+ }
711
+ }
712
+ if (synthRepos.length >= 2) {
713
+ for (const d of checkDataContractMapping(narrativeMap, allEntities)) {
714
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
715
+ }
716
+ }
701
717
  // Structured coverage (entity points must be filled).
702
718
  for (const d of checkStructuredCoverage(outline, entityMap)) {
703
719
  diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
@@ -710,7 +726,7 @@ export async function validateWork(options) {
710
726
  return { ok: false, diagnostics: diags };
711
727
  }
712
728
  // Render (appendices driven by profile.appendices).
713
- const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI });
729
+ const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI, repos: synthRepos.length > 0 ? synthRepos : undefined });
714
730
  await fs.writeFile(paths.documentMd, documentMd, 'utf-8');
715
731
  let documentHtml;
716
732
  if (options.renderHtmlOutput !== false) {
@@ -2,7 +2,7 @@
2
2
  * Document Engine — gate orchestration (T10.4).
3
3
  * Runs lint → structured coverage → review check → budget → zod, collecting diagnostics.
4
4
  */
5
- import { lintNarrative, checkBudget, checkDiagramPresence, checkNumericPresence, checkGoldenSignals, checkRepoSectionCoverage } from './lint.js';
5
+ import { lintNarrative, checkBudget, checkDiagramPresence, checkNumericPresence, checkGoldenSignals, checkRepoCoverage, checkRepoSectionStub, checkDuplicateContract, checkExamplePresence, checkForwardDecisionRef, checkUnverifiedNumber, checkDataContractMapping, } from './lint.js';
6
6
  import { checkStructuredCoverage } from './coverage.js';
7
7
  import { checkReviewResult } from './review.js';
8
8
  export async function runGates(input) {
@@ -48,18 +48,38 @@ export async function runGates(input) {
48
48
  }
49
49
  }
50
50
  }
51
- // Multi-repo synthesize gate (library pipeline): every non-optional chapter must have per-repo
52
- // sections (`### <repo>`) when repos ≥2 — matches validateWork (Agent mode) enforcement.
51
+ // Multi-repo synthesize gate (library pipeline): 全文级仓覆盖 + 空壳小节拦截(整体性组织,
52
+ // 替代旧「每章必须 ### <repo> 分仓小节」)。matches validateWork (Agent mode) enforcement.
53
53
  if (input.repos && input.repos.length >= 2) {
54
- const requiredById = new Map(input.outline.chapters.filter((c) => !c.optional).map((c) => [c.id, c]));
54
+ for (const d of checkRepoCoverage(input.repos, input.chapterNarratives)) {
55
+ diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message });
56
+ }
55
57
  for (const [chId, narrative] of input.chapterNarratives) {
56
- if (!requiredById.has(chId))
57
- continue;
58
- for (const d of checkRepoSectionCoverage(chId, narrative, input.repos)) {
58
+ for (const d of checkRepoSectionStub(chId, narrative, input.repos)) {
59
59
  diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message, chapter: chId });
60
60
  }
61
61
  }
62
62
  }
63
+ // 契约质量 lint:契约单一定义 / 示例带 body(空壳拦截)/ 前向决策引用 / 未验证数字 / 契约-表映射。
64
+ for (const d of checkDuplicateContract(input.entities.interfaces)) {
65
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
66
+ }
67
+ for (const d of checkExamplePresence(input.entities.interfaces)) {
68
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
69
+ }
70
+ for (const d of checkForwardDecisionRef(input.chapterNarratives, input.entities.decisions)) {
71
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
72
+ }
73
+ for (const [chId, narrative] of input.chapterNarratives) {
74
+ for (const d of checkUnverifiedNumber(chId, narrative)) {
75
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
76
+ }
77
+ }
78
+ if (input.repos && input.repos.length >= 2) {
79
+ for (const d of checkDataContractMapping(input.chapterNarratives, input.entities)) {
80
+ diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
81
+ }
82
+ }
63
83
  // structured coverage
64
84
  for (const d of checkStructuredCoverage(input.outline, input.chapterEntities)) {
65
85
  diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
@@ -36,4 +36,30 @@ export interface RepoSectionDiagnostic {
36
36
  message: string;
37
37
  chapter?: string;
38
38
  }
39
- export declare function checkRepoSectionCoverage(chapterId: string, narrative: string, repos: readonly string[]): RepoSectionDiagnostic[];
39
+ export declare function countRepoMentions(narrative: string, repo: string): number;
40
+ export declare function checkRepoCoverage(repos: readonly string[], narratives: ReadonlyMap<string, string>, threshold?: number): RepoSectionDiagnostic[];
41
+ export declare function checkRepoSectionStub(chapterId: string, narrative: string, repos: readonly string[], minChars?: number): RepoSectionDiagnostic[];
42
+ /** 同一物理端点(method+path 归一化)不得存在多个接口实体。 */
43
+ export declare function checkDuplicateContract(interfaces: ReadonlyArray<{
44
+ method: string;
45
+ path: string;
46
+ id: string;
47
+ }>): RepoSectionDiagnostic[];
48
+ /** 空壳示例拦截:body 方法必须给 request_example(或标注「无请求体」);有响应字段必须有成功响应示例。 */
49
+ export declare function checkExamplePresence(interfaces: ReadonlyArray<{
50
+ id: string;
51
+ method: string;
52
+ request_example?: string;
53
+ response_fields?: unknown[];
54
+ success_response_example?: string;
55
+ }>): RepoSectionDiagnostic[];
56
+ /** 前向决策引用:叙述引用 [D#] 但该决策未在 entities.decisions 定义 → warning(P6 决策位置前移辅助检查)。 */
57
+ export declare function checkForwardDecisionRef(narratives: ReadonlyMap<string, string>, decisions: ReadonlyArray<{
58
+ id: string;
59
+ }>): RepoSectionDiagnostic[];
60
+ /** 性能章目标数字必须标注来源/待实测(P10)。 */
61
+ export declare function checkUnverifiedNumber(chapterId: string, narrative: string): RepoSectionDiagnostic[];
62
+ /** 接口与数据整合(P11):synth 模式下有表实体时,接口章必须含契约-表映射表。 */
63
+ export declare function checkDataContractMapping(narratives: ReadonlyMap<string, string>, entities: {
64
+ tables?: unknown[];
65
+ }): RepoSectionDiagnostic[];
@@ -124,24 +124,143 @@ export function checkGoldenSignals(chapterId, narrative, gates = []) {
124
124
  message: `Chapter "${chapterId}" gates require SRE 四金信号 monitoring but missing: ${missing.map((m) => m.key).join('、')}(监控必须覆盖延迟/流量/错误/饱和度)`,
125
125
  }];
126
126
  }
127
- export function checkRepoSectionCoverage(chapterId, narrative, repos) {
127
+ export function countRepoMentions(narrative, repo) {
128
+ const esc = escapeRegExp(repo);
129
+ const re = new RegExp(`\\[${esc}\\]|^###\\s+${esc}(?:\\s|:|:|·|$)|${esc}_`, 'gm');
130
+ return (narrative.match(re) ?? []).length;
131
+ }
132
+ export function checkRepoCoverage(repos, narratives, threshold = 3) {
128
133
  if (repos.length < 2)
129
134
  return []; // 非 synthesize 或单仓 → 不校验
135
+ const joined = [...narratives.values()].join('\n');
130
136
  const diags = [];
131
137
  for (const repo of repos) {
132
- // 匹配 `### <repo>` 小节标题(行首,可带后缀如 `### repo1 · 目标`)
133
- const sectionRe = new RegExp(`^###\\s+${escapeRegExp(repo)}(?:\\s|:|:|·|$)`, 'm');
134
- if (!sectionRe.test(narrative)) {
138
+ if (countRepoMentions(joined, repo) < threshold) {
135
139
  diags.push({
136
- code: 'repo_section_missing',
140
+ code: 'repo_coverage_missing',
141
+ severity: 'error',
142
+ message: `合成文档中仓 ${repo} 全文累计出现 < ${threshold} 次(应含 [${repo}] 标注、\`### ${repo}\` 小节或 ${repo}_ 前缀实体),疑似被概括掉`,
143
+ });
144
+ }
145
+ }
146
+ return diags;
147
+ }
148
+ export function checkRepoSectionStub(chapterId, narrative, repos, minChars = 40) {
149
+ const diags = [];
150
+ for (const repo of repos) {
151
+ const re = new RegExp(`^###\\s+${escapeRegExp(repo)}(?:\\s|:|:|·|$)[\\s\\S]*?(?=^###|$)`, 'm');
152
+ const m = re.exec(narrative);
153
+ if (!m)
154
+ continue;
155
+ const sectionBody = m[0].replace(/^###[^\n]*\n/, '').trim();
156
+ if (sectionBody.length < minChars) {
157
+ diags.push({
158
+ code: 'repo_section_stub',
137
159
  severity: 'error',
138
160
  chapter: chapterId,
139
- message: `Chapter "${chapterId}" 缺少仓 ${repo} 的分仓小节(应含 \`### ${repo}\`,合成文档每章必须按仓分节,防止跨仓内容互相稀释)`,
161
+ message: `Chapter "${chapterId}" 的仓 ${repo} 小节内容过短(< ${minChars} 字符,疑似「无 X」占位),请并入整体叙述或补足实质内容`,
162
+ });
163
+ }
164
+ }
165
+ return diags;
166
+ }
167
+ // ============= 契约质量 lint(P2/P3/P4/P6:契约单一定义 / 示例带 body / 前向决策引用) =============
168
+ /** 同一物理端点(method+path 归一化)不得存在多个接口实体。 */
169
+ export function checkDuplicateContract(interfaces) {
170
+ const diags = [];
171
+ const seen = new Map();
172
+ for (const it of interfaces) {
173
+ const key = `${it.method} ${it.path.trim().toLowerCase()}`;
174
+ const prev = seen.get(key);
175
+ if (prev) {
176
+ diags.push({
177
+ code: 'duplicate_contract',
178
+ severity: 'error',
179
+ message: `接口 ${it.id} 与 ${prev} 为同一物理端点(${it.method} ${it.path}),契约应单一定义并标注消费方/生产方(<repo>_<id> 仅作追溯别名)`,
180
+ });
181
+ }
182
+ else {
183
+ seen.set(key, it.id);
184
+ }
185
+ }
186
+ return diags;
187
+ }
188
+ const BODY_METHODS = new Set(['POST', 'PUT', 'PATCH']);
189
+ const NO_BODY_MARK = /无请求体/;
190
+ /** 空壳示例拦截:body 方法必须给 request_example(或标注「无请求体」);有响应字段必须有成功响应示例。 */
191
+ export function checkExamplePresence(interfaces) {
192
+ const diags = [];
193
+ for (const it of interfaces) {
194
+ const example = (it.request_example ?? '').trim();
195
+ const isNoBodyMark = NO_BODY_MARK.test(example);
196
+ // 空壳拦截:body 方法既无真实 JSON 请求体、也未显式标注「无请求体」→ error。
197
+ if (BODY_METHODS.has(it.method) && !isNoBodyMark && example.length === 0) {
198
+ diags.push({
199
+ code: 'request_example_missing',
200
+ severity: 'error',
201
+ message: `接口 ${it.id}(${it.method})缺少 request_example(真实 JSON 请求体),空壳「成功示例」不合格;确无请求体请显式标注「无请求体」`,
202
+ });
203
+ }
204
+ if ((it.response_fields?.length ?? 0) > 0 && !it.success_response_example) {
205
+ diags.push({
206
+ code: 'response_example_missing',
207
+ severity: 'error',
208
+ message: `接口 ${it.id} 声明了响应字段但缺少 success_response_example(真实 JSON 成功响应示例)`,
140
209
  });
141
210
  }
142
211
  }
143
212
  return diags;
144
213
  }
214
+ /** 前向决策引用:叙述引用 [D#] 但该决策未在 entities.decisions 定义 → warning(P6 决策位置前移辅助检查)。 */
215
+ export function checkForwardDecisionRef(narratives, decisions) {
216
+ const defined = new Set(decisions.map((d) => d.id));
217
+ const diags = [];
218
+ const re = /\[(D\d+)\]/g;
219
+ for (const [chId, narrative] of narratives) {
220
+ let m;
221
+ while ((m = re.exec(narrative)) !== null) {
222
+ if (!defined.has(m[1])) {
223
+ diags.push({
224
+ code: 'forward_decision_ref',
225
+ severity: 'warning',
226
+ chapter: chId,
227
+ message: `Chapter "${chId}" 引用了未定义的决策 ${m[1]}(决策应在 ADR 章节先行定义,正文用 [D#] 引用)`,
228
+ });
229
+ }
230
+ }
231
+ }
232
+ return diags;
233
+ }
234
+ /** 性能章目标数字必须标注来源/待实测(P10)。 */
235
+ export function checkUnverifiedNumber(chapterId, narrative) {
236
+ if (chapterId !== 'performance')
237
+ return [];
238
+ if (!/\d/.test(narrative))
239
+ return [];
240
+ if (!/待实测|门闩|测量|实测|基准/.test(narrative)) {
241
+ return [{
242
+ code: 'unverified_number',
243
+ severity: 'warning',
244
+ chapter: chapterId,
245
+ message: `Chapter "${chapterId}" 含目标数字但未标注「待实测/门闩/测量方法」,数字可信度不足(性能数字必须给来源或测量方式)`,
246
+ }];
247
+ }
248
+ return [];
249
+ }
250
+ /** 接口与数据整合(P11):synth 模式下有表实体时,接口章必须含契约-表映射表。 */
251
+ export function checkDataContractMapping(narratives, entities) {
252
+ if ((entities.tables?.length ?? 0) === 0)
253
+ return [];
254
+ const narrative = narratives.get('api-design') ?? '';
255
+ if (!/契约-表映射|映射表|接口.*表.*映射|表.*接口.*映射/.test(narrative)) {
256
+ return [{
257
+ code: 'data_contract_mapping_missing',
258
+ severity: 'warning',
259
+ message: '接口与数据应整合呈现:api-design 章缺少「契约-表映射」(每接口 ↔ 读写表/列),接口约定与数据库仍游离',
260
+ }];
261
+ }
262
+ return [];
263
+ }
145
264
  function escapeRegExp(s) {
146
265
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
147
266
  }
@@ -19,7 +19,10 @@ export declare function renderTable(t: TableEntity): string;
19
19
  export declare function renderDecisions(decisions: Array<{
20
20
  id: string;
21
21
  text: string;
22
+ background?: string;
23
+ alternative?: string;
22
24
  }>): string;
25
+ /** 契约实体按「面」分组渲染(Console 面 / 内部与 Worker 面),替代平铺。 */
23
26
  export declare function renderEntities(entities: Entities): string;
24
27
  export interface RenderDocumentInput {
25
28
  outline: Outline;
@@ -32,6 +35,10 @@ export interface RenderDocumentInput {
32
35
  };
33
36
  /** LLM-written anti-AI self-check items (from appendices/anti-ai.md, optional). */
34
37
  appendixAntiAI?: string;
38
+ /** 参与仓清单(synth 模式):渲染文档头元信息(P7 整体性)。 */
39
+ repos?: string[];
40
+ /** 是否渲染产物溯源附录(P8:默认关闭,调试/--signoff 时开启)。 */
41
+ includeProvenance?: boolean;
35
42
  }
36
43
  /** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
37
44
  * stays in `chapters/<id>.md` and `review-result.json`; the deliverable document must not show
@@ -53,6 +60,17 @@ export declare function stripEntityJsonBlocks(text: string): string;
53
60
  * 仅当首行非空是 H1 且其内容与章节标题 trim 后一致时才剥离,避免误删正文中的真实内容。
54
61
  */
55
62
  export declare function stripLeadingTitle(text: string, chapterTitle: string): string;
63
+ /**
64
+ * 章节叙述标题整体降级 n 级(默认 1)。原因:renderDocument 已为每章输出 `## <章节标题>`(H2),
65
+ * 而 Agent 生成的叙述内小节标题习惯用 `##`(H2)、分仓小节用 `###`(H3)——与章节头同级,
66
+ * 造成最终文档目录层级乱(`## 需求` 与 `## 跨仓总目标` 平级)。渲染时降级:`##`→`###`、`###`→`####`…,
67
+ * 使章节头 H2 → 叙述小节 H3 → 分仓 H4,层级正确。
68
+ *
69
+ * 只处理行首的 markdown 标题(`#{1,6}\s`),并跳过围栏代码块(mermaid/``` 内部行不动),
70
+ * 避免把代码内容里的 `#` 误判成标题。H6 封顶不再升。质量门禁 `### <repo>` 检查的是原始章节
71
+ * 文件(chapters/*.md),不在此处降级影响范围——仅最终渲染的 document.md 生效。
72
+ */
73
+ export declare function demoteHeadings(text: string, levels?: number): string;
56
74
  export declare function renderDocument(input: RenderDocumentInput): string;
57
75
  /** 附录 B · 去AI味自检(frontend-dev-guide §十一 → README §1.6 五维表)。 */
58
76
  export declare function renderAppendixAntiAI(content?: string): string;
@@ -31,32 +31,45 @@ export function escapeInline(s) {
31
31
  }
32
32
  // ============= Contract entity → Markdown (T9.1) =============
33
33
  export function renderInterface(i) {
34
- const rows = [
34
+ const meta = [
35
35
  `##### ${i.id} · ${escapeInline(i.name)}(${i.method} ${i.path})`,
36
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
- '',
37
+ ...(i.side || (i.consumers?.length ?? 0) > 0 || (i.producers?.length ?? 0) > 0
38
+ ? [`**面**:${escapeInline(i.side ?? '')}${(i.consumers?.length ?? 0) > 0 ? ` | **消费方**:${(i.consumers ?? []).map(escapeInline).join(', ')}` : ''}${(i.producers?.length ?? 0) > 0 ? ` | **生产方**:${(i.producers ?? []).map(escapeInline).join(', ')}` : ''}`, '']
39
+ : []),
40
+ ];
41
+ const fieldRows = (fields) => fields.map((f) => `| ${escapeTableCell(f.name)} | ${escapeTableCell(f.type)} | ${f.required ? '是' : '否'} | ${escapeTableCell(f.desc ?? '')} |`);
42
+ const fieldTables = [];
43
+ if ((i.request_fields ?? []).length > 0) {
44
+ fieldTables.push('**请求字段**', '', '| 字段 | 类型 | 必填 | 说明 |', '|------|------|------|------|', ...fieldRows(i.request_fields ?? []));
45
+ }
46
+ if ((i.response_fields ?? []).length > 0) {
47
+ fieldTables.push('**响应字段**', '', '| 字段 | 类型 | 必填 | 说明 |', '|------|------|------|------|', ...fieldRows(i.response_fields ?? []));
48
+ }
49
+ const examples = [];
50
+ if (i.request_example) {
51
+ examples.push('**请求示例**', '', '```json', escapeFenceContent(i.request_example), '```');
52
+ }
53
+ if (i.success_response_example) {
54
+ examples.push('**成功响应示例**', '', '```json', escapeFenceContent(i.success_response_example), '```');
55
+ }
56
+ if ((i.failure_examples?.length ?? 0) > 0 || i.failure_response_example) {
57
+ examples.push('**失败示例**', '');
58
+ if ((i.failure_examples?.length ?? 0) > 0) {
59
+ examples.push(...(i.failure_examples ?? []).map((ex) => `- ${escapeInline(ex)}`));
60
+ }
61
+ if (i.failure_response_example) {
62
+ examples.push('', '```json', escapeFenceContent(i.failure_response_example), '```');
63
+ }
64
+ }
65
+ const errors = [
53
66
  '**错误**',
54
67
  '',
55
- '| 条件 | 状态 | 说明 |',
56
- '|------|------|------|',
57
- ...i.errors.map((e) => `| ${escapeTableCell(e.condition)} | ${escapeTableCell(e.status)} | ${escapeTableCell(e.note ?? '')} |`),
68
+ '| 条件 | 状态 | 类别 | 说明 |',
69
+ '|------|------|------|------|',
70
+ ...i.errors.map((e) => `| ${escapeTableCell(e.condition)} | ${escapeTableCell(e.status)} | ${escapeTableCell(e.kind ?? 'backend')} | ${escapeTableCell(e.note ?? '')} |`),
58
71
  ];
59
- return rows.join('\n');
72
+ return [...meta, ...fieldTables, ...examples, ...errors].join('\n');
60
73
  }
61
74
  export function renderTable(t) {
62
75
  return [
@@ -74,22 +87,37 @@ export function renderDecisions(decisions) {
74
87
  if (decisions.length === 0)
75
88
  return '';
76
89
  return [
77
- '**决策记录**',
90
+ '## 决策记录(ADR)',
78
91
  '',
79
- '| 决策 | 结论 |',
80
- '|------|------|',
81
- ...decisions.map((d) => `| ${escapeTableCell(d.id)} | ${escapeTableCell(d.text)} |`),
92
+ '| 决策 | 背景 | 结论 | 备选(已否决) |',
93
+ '|------|------|------|------|',
94
+ ...decisions.map((d) => `| ${escapeTableCell(d.id)} | ${escapeTableCell(d.background ?? '')} | ${escapeTableCell(d.text)} | ${escapeTableCell(d.alternative ?? '')} |`),
82
95
  ].join('\n');
83
96
  }
97
+ /** 契约实体按「面」分组渲染(Console 面 / 内部与 Worker 面),替代平铺。 */
84
98
  export function renderEntities(entities) {
85
99
  const parts = [];
86
- for (const i of entities.interfaces)
87
- parts.push(renderInterface(i));
100
+ const groups = new Map();
101
+ for (const i of entities.interfaces) {
102
+ const key = i.side ?? 'other';
103
+ if (!groups.has(key))
104
+ groups.set(key, []);
105
+ groups.get(key).push(i);
106
+ }
107
+ const sideTitle = {
108
+ console: 'Console 面契约',
109
+ internal: '内部 / Worker 面契约',
110
+ worker: 'Worker 面契约',
111
+ other: '契约',
112
+ };
113
+ for (const key of ['console', 'internal', 'worker', 'other']) {
114
+ const list = groups.get(key);
115
+ if (!list || list.length === 0)
116
+ continue;
117
+ parts.push(`### ${sideTitle[key]}`, '', list.map(renderInterface).join('\n\n'));
118
+ }
88
119
  for (const t of entities.tables)
89
120
  parts.push(renderTable(t));
90
- const decisionsMd = renderDecisions(entities.decisions);
91
- if (decisionsMd)
92
- parts.push(decisionsMd);
93
121
  return parts.join('\n\n');
94
122
  }
95
123
  /** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
@@ -146,30 +174,97 @@ export function stripLeadingTitle(text, chapterTitle) {
146
174
  const rest = trimmed.slice(firstLine.length).replace(/^\n+/, '');
147
175
  return rest;
148
176
  }
177
+ /**
178
+ * 章节叙述标题整体降级 n 级(默认 1)。原因:renderDocument 已为每章输出 `## <章节标题>`(H2),
179
+ * 而 Agent 生成的叙述内小节标题习惯用 `##`(H2)、分仓小节用 `###`(H3)——与章节头同级,
180
+ * 造成最终文档目录层级乱(`## 需求` 与 `## 跨仓总目标` 平级)。渲染时降级:`##`→`###`、`###`→`####`…,
181
+ * 使章节头 H2 → 叙述小节 H3 → 分仓 H4,层级正确。
182
+ *
183
+ * 只处理行首的 markdown 标题(`#{1,6}\s`),并跳过围栏代码块(mermaid/``` 内部行不动),
184
+ * 避免把代码内容里的 `#` 误判成标题。H6 封顶不再升。质量门禁 `### <repo>` 检查的是原始章节
185
+ * 文件(chapters/*.md),不在此处降级影响范围——仅最终渲染的 document.md 生效。
186
+ */
187
+ export function demoteHeadings(text, levels = 1) {
188
+ if (levels <= 0)
189
+ return text;
190
+ const lines = text.split('\n');
191
+ let inFence = false;
192
+ const out = [];
193
+ for (const line of lines) {
194
+ const trimmed = line.trimStart();
195
+ if (trimmed.startsWith('```'))
196
+ inFence = !inFence;
197
+ if (!inFence) {
198
+ const m = /^(#{1,6})(\s.*)$/.exec(line);
199
+ if (m) {
200
+ const hashes = Math.min(m[1].length + levels, 6);
201
+ out.push('#'.repeat(hashes) + m[2]);
202
+ continue;
203
+ }
204
+ }
205
+ out.push(line);
206
+ }
207
+ return out.join('\n');
208
+ }
149
209
  export function renderDocument(input) {
150
- const { outline, entities, narratives, profile, appendixAntiAI } = input;
210
+ const { outline, entities, narratives, profile, appendixAntiAI, repos, includeProvenance } = input;
151
211
  const body = [`# 方案文档:${escapeInline(outline.profile)}`, ''];
152
- for (const ch of outline.chapters) {
212
+ // 文档头元信息(P7):确定性渲染,不依赖 LLM。
213
+ const meta = [];
214
+ if (repos && repos.length > 0)
215
+ meta.push(`| 参与仓 | ${repos.map(escapeTableCell).join('、')} |`);
216
+ if (meta.length > 0) {
217
+ body.push('| 项 | 值 |', '|------|------|', ...meta, '');
218
+ }
219
+ // 目录(P7):按大纲章节序确定性生成;接口与数据合并时 data-model 不单列。
220
+ const apiDesignIdx = outline.chapters.findIndex((c) => c.id === 'api-design');
221
+ const dataModelIdx = outline.chapters.findIndex((c) => c.id === 'data-model');
222
+ const mergeDataModel = apiDesignIdx >= 0 && dataModelIdx > apiDesignIdx;
223
+ body.push('## 目录', '');
224
+ outline.chapters.forEach((ch, idx) => {
225
+ if (mergeDataModel && ch.id === 'data-model')
226
+ return;
227
+ body.push(`${idx + 1}. ${escapeInline(ch.title)}`);
228
+ });
229
+ body.push('');
230
+ for (const [idx, ch] of outline.chapters.entries()) {
231
+ if (mergeDataModel && ch.id === 'data-model')
232
+ continue; // 已并入「接口与数据设计」章
153
233
  body.push(`## ${escapeInline(ch.title)}`, '');
154
- // 依次净化:剥内嵌契约 JSON 块(机器中间格式)→ 剥与章节标题重复的首行 H1 → 剥引擎修复注记。
155
- const narrative = stripLeadingTitle(stripEntityJsonBlocks(stripReviewNotes(narratives.get(ch.id) ?? '')), ch.title);
234
+ // 依次净化:剥内嵌契约 JSON 块(机器中间格式)→ 剥与章节标题重复的首行 H1 → 剥引擎修复注记,
235
+ // 再把叙述内标题降级一级(章节头已占 H2,叙述小节 H2→H3),保证目录层级正确。
236
+ const narrative = demoteHeadings(stripLeadingTitle(stripEntityJsonBlocks(stripReviewNotes(narratives.get(ch.id) ?? '')), ch.title));
156
237
  if (narrative.trim())
157
238
  body.push(narrative.trim(), '');
239
+ // 接口与数据整合(P11):data-model 紧随 api-design 时并入其下为小节,不再单独成章。
240
+ if (mergeDataModel && ch.id === 'api-design') {
241
+ const dm = outline.chapters[dataModelIdx];
242
+ const dmNarrative = demoteHeadings(stripLeadingTitle(stripEntityJsonBlocks(stripReviewNotes(narratives.get(dm.id) ?? '')), dm.title));
243
+ if (dmNarrative.trim())
244
+ body.push('### 数据模型', '', dmNarrative.trim(), '');
245
+ }
246
+ }
247
+ // 决策记录(ADR)独立成节(P6):不再埋在契约实体末尾。
248
+ const decisionsMd = renderDecisions(entities.decisions);
249
+ if (decisionsMd.trim()) {
250
+ body.push('---', '', decisionsMd, '');
158
251
  }
159
252
  // Contract entities: append to the chapters that reference them (simplified: append in a
160
- // final "契约" section, since entities are cross-chapter).
253
+ // final "附录 A" section, since entities are cross-chapter).
161
254
  const contractMd = renderEntities(entities);
162
255
  if (contractMd.trim()) {
163
- body.push('---', '', '## 契约实体', '', contractMd, '');
256
+ body.push('---', '', '## 附录 A:契约实体明细', '', contractMd, '');
164
257
  }
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 生成 + 引擎校验 |`);
258
+ // 产物溯源附录(P8):默认不渲染(synth 下每章来源雷同,属噪声);--signoff/调试时开启。
259
+ if (includeProvenance) {
260
+ body.push('---', '', '## 附录:产物溯源', '');
261
+ body.push('| 章节 | 数据来源 | 处理方式 |');
262
+ body.push('|------|---------|---------|');
263
+ for (const ch of outline.chapters) {
264
+ body.push(`| ${escapeTableCell(ch.title)} | 输入素材 / 项目规约 / 大纲要点 | LLM 生成 + 引擎校验 |`);
265
+ }
266
+ body.push('', '');
171
267
  }
172
- body.push('', '');
173
268
  // 附录 B/C: rendered from profile.appendices (decision: deterministic templates, §7.3).
174
269
  const appendices = profile?.appendices ?? [];
175
270
  if (appendices.includes('anti-ai')) {
@@ -34,13 +34,17 @@ export declare const ErrorSpecSchema: z.ZodObject<{
34
34
  condition: z.ZodString;
35
35
  status: z.ZodString;
36
36
  note: z.ZodOptional<z.ZodString>;
37
+ /** 错误类别:backend=gRPC→HTTP 映射;guard=前端守卫行为;internal=Worker 内部语义(P5 拆分)。 */
38
+ kind: z.ZodOptional<z.ZodEnum<["backend", "guard", "internal"]>>;
37
39
  }, "strip", z.ZodTypeAny, {
38
40
  status: string;
39
41
  condition: string;
42
+ kind?: "backend" | "guard" | "internal" | undefined;
40
43
  note?: string | undefined;
41
44
  }, {
42
45
  status: string;
43
46
  condition: string;
47
+ kind?: "backend" | "guard" | "internal" | undefined;
44
48
  note?: string | undefined;
45
49
  }>;
46
50
  export declare const InterfaceEntitySchema: z.ZodObject<{
@@ -90,16 +94,26 @@ export declare const InterfaceEntitySchema: z.ZodObject<{
90
94
  condition: z.ZodString;
91
95
  status: z.ZodString;
92
96
  note: z.ZodOptional<z.ZodString>;
97
+ /** 错误类别:backend=gRPC→HTTP 映射;guard=前端守卫行为;internal=Worker 内部语义(P5 拆分)。 */
98
+ kind: z.ZodOptional<z.ZodEnum<["backend", "guard", "internal"]>>;
93
99
  }, "strip", z.ZodTypeAny, {
94
100
  status: string;
95
101
  condition: string;
102
+ kind?: "backend" | "guard" | "internal" | undefined;
96
103
  note?: string | undefined;
97
104
  }, {
98
105
  status: string;
99
106
  condition: string;
107
+ kind?: "backend" | "guard" | "internal" | undefined;
100
108
  note?: string | undefined;
101
109
  }>, "many">;
102
110
  failure_examples: z.ZodArray<z.ZodString, "many">;
111
+ request_example: z.ZodOptional<z.ZodString>;
112
+ success_response_example: z.ZodOptional<z.ZodString>;
113
+ failure_response_example: z.ZodOptional<z.ZodString>;
114
+ side: z.ZodOptional<z.ZodEnum<["console", "worker", "internal"]>>;
115
+ consumers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
116
+ producers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
103
117
  }, "strip", z.ZodTypeAny, {
104
118
  path: string;
105
119
  id: string;
@@ -107,6 +121,7 @@ export declare const InterfaceEntitySchema: z.ZodObject<{
107
121
  errors: {
108
122
  status: string;
109
123
  condition: string;
124
+ kind?: "backend" | "guard" | "internal" | undefined;
110
125
  note?: string | undefined;
111
126
  }[];
112
127
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -125,6 +140,12 @@ export declare const InterfaceEntitySchema: z.ZodObject<{
125
140
  default?: string | undefined;
126
141
  desc?: string | undefined;
127
142
  }[] | undefined;
143
+ request_example?: string | undefined;
144
+ success_response_example?: string | undefined;
145
+ failure_response_example?: string | undefined;
146
+ side?: "internal" | "console" | "worker" | undefined;
147
+ consumers?: string[] | undefined;
148
+ producers?: string[] | undefined;
128
149
  }, {
129
150
  path: string;
130
151
  id: string;
@@ -132,6 +153,7 @@ export declare const InterfaceEntitySchema: z.ZodObject<{
132
153
  errors: {
133
154
  status: string;
134
155
  condition: string;
156
+ kind?: "backend" | "guard" | "internal" | undefined;
135
157
  note?: string | undefined;
136
158
  }[];
137
159
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -150,6 +172,12 @@ export declare const InterfaceEntitySchema: z.ZodObject<{
150
172
  required?: boolean | undefined;
151
173
  desc?: string | undefined;
152
174
  }[] | undefined;
175
+ request_example?: string | undefined;
176
+ success_response_example?: string | undefined;
177
+ failure_response_example?: string | undefined;
178
+ side?: "internal" | "console" | "worker" | undefined;
179
+ consumers?: string[] | undefined;
180
+ producers?: string[] | undefined;
153
181
  }>;
154
182
  export type InterfaceEntity = z.infer<typeof InterfaceEntitySchema>;
155
183
  export declare const TableEntitySchema: z.ZodObject<{
@@ -208,12 +236,18 @@ export type TableEntity = z.infer<typeof TableEntitySchema>;
208
236
  export declare const DecisionEntitySchema: z.ZodObject<{
209
237
  id: z.ZodString;
210
238
  text: z.ZodString;
239
+ background: z.ZodOptional<z.ZodString>;
240
+ alternative: z.ZodOptional<z.ZodString>;
211
241
  }, "strip", z.ZodTypeAny, {
212
242
  id: string;
213
243
  text: string;
244
+ background?: string | undefined;
245
+ alternative?: string | undefined;
214
246
  }, {
215
247
  id: string;
216
248
  text: string;
249
+ background?: string | undefined;
250
+ alternative?: string | undefined;
217
251
  }>;
218
252
  export type DecisionEntity = z.infer<typeof DecisionEntitySchema>;
219
253
  export declare const EntitiesSchema: z.ZodObject<{
@@ -264,16 +298,26 @@ export declare const EntitiesSchema: z.ZodObject<{
264
298
  condition: z.ZodString;
265
299
  status: z.ZodString;
266
300
  note: z.ZodOptional<z.ZodString>;
301
+ /** 错误类别:backend=gRPC→HTTP 映射;guard=前端守卫行为;internal=Worker 内部语义(P5 拆分)。 */
302
+ kind: z.ZodOptional<z.ZodEnum<["backend", "guard", "internal"]>>;
267
303
  }, "strip", z.ZodTypeAny, {
268
304
  status: string;
269
305
  condition: string;
306
+ kind?: "backend" | "guard" | "internal" | undefined;
270
307
  note?: string | undefined;
271
308
  }, {
272
309
  status: string;
273
310
  condition: string;
311
+ kind?: "backend" | "guard" | "internal" | undefined;
274
312
  note?: string | undefined;
275
313
  }>, "many">;
276
314
  failure_examples: z.ZodArray<z.ZodString, "many">;
315
+ request_example: z.ZodOptional<z.ZodString>;
316
+ success_response_example: z.ZodOptional<z.ZodString>;
317
+ failure_response_example: z.ZodOptional<z.ZodString>;
318
+ side: z.ZodOptional<z.ZodEnum<["console", "worker", "internal"]>>;
319
+ consumers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
320
+ producers: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
277
321
  }, "strip", z.ZodTypeAny, {
278
322
  path: string;
279
323
  id: string;
@@ -281,6 +325,7 @@ export declare const EntitiesSchema: z.ZodObject<{
281
325
  errors: {
282
326
  status: string;
283
327
  condition: string;
328
+ kind?: "backend" | "guard" | "internal" | undefined;
284
329
  note?: string | undefined;
285
330
  }[];
286
331
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -299,6 +344,12 @@ export declare const EntitiesSchema: z.ZodObject<{
299
344
  default?: string | undefined;
300
345
  desc?: string | undefined;
301
346
  }[] | undefined;
347
+ request_example?: string | undefined;
348
+ success_response_example?: string | undefined;
349
+ failure_response_example?: string | undefined;
350
+ side?: "internal" | "console" | "worker" | undefined;
351
+ consumers?: string[] | undefined;
352
+ producers?: string[] | undefined;
302
353
  }, {
303
354
  path: string;
304
355
  id: string;
@@ -306,6 +357,7 @@ export declare const EntitiesSchema: z.ZodObject<{
306
357
  errors: {
307
358
  status: string;
308
359
  condition: string;
360
+ kind?: "backend" | "guard" | "internal" | undefined;
309
361
  note?: string | undefined;
310
362
  }[];
311
363
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -324,6 +376,12 @@ export declare const EntitiesSchema: z.ZodObject<{
324
376
  required?: boolean | undefined;
325
377
  desc?: string | undefined;
326
378
  }[] | undefined;
379
+ request_example?: string | undefined;
380
+ success_response_example?: string | undefined;
381
+ failure_response_example?: string | undefined;
382
+ side?: "internal" | "console" | "worker" | undefined;
383
+ consumers?: string[] | undefined;
384
+ producers?: string[] | undefined;
327
385
  }>, "many">>, {
328
386
  path: string;
329
387
  id: string;
@@ -331,6 +389,7 @@ export declare const EntitiesSchema: z.ZodObject<{
331
389
  errors: {
332
390
  status: string;
333
391
  condition: string;
392
+ kind?: "backend" | "guard" | "internal" | undefined;
334
393
  note?: string | undefined;
335
394
  }[];
336
395
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -349,6 +408,12 @@ export declare const EntitiesSchema: z.ZodObject<{
349
408
  default?: string | undefined;
350
409
  desc?: string | undefined;
351
410
  }[] | undefined;
411
+ request_example?: string | undefined;
412
+ success_response_example?: string | undefined;
413
+ failure_response_example?: string | undefined;
414
+ side?: "internal" | "console" | "worker" | undefined;
415
+ consumers?: string[] | undefined;
416
+ producers?: string[] | undefined;
352
417
  }[], {
353
418
  path: string;
354
419
  id: string;
@@ -356,6 +421,7 @@ export declare const EntitiesSchema: z.ZodObject<{
356
421
  errors: {
357
422
  status: string;
358
423
  condition: string;
424
+ kind?: "backend" | "guard" | "internal" | undefined;
359
425
  note?: string | undefined;
360
426
  }[];
361
427
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -374,6 +440,12 @@ export declare const EntitiesSchema: z.ZodObject<{
374
440
  required?: boolean | undefined;
375
441
  desc?: string | undefined;
376
442
  }[] | undefined;
443
+ request_example?: string | undefined;
444
+ success_response_example?: string | undefined;
445
+ failure_response_example?: string | undefined;
446
+ side?: "internal" | "console" | "worker" | undefined;
447
+ consumers?: string[] | undefined;
448
+ producers?: string[] | undefined;
377
449
  }[] | undefined>;
378
450
  tables: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodObject<{
379
451
  id: z.ZodString;
@@ -456,18 +528,28 @@ export declare const EntitiesSchema: z.ZodObject<{
456
528
  decisions: z.ZodEffects<z.ZodDefault<z.ZodArray<z.ZodObject<{
457
529
  id: z.ZodString;
458
530
  text: z.ZodString;
531
+ background: z.ZodOptional<z.ZodString>;
532
+ alternative: z.ZodOptional<z.ZodString>;
459
533
  }, "strip", z.ZodTypeAny, {
460
534
  id: string;
461
535
  text: string;
536
+ background?: string | undefined;
537
+ alternative?: string | undefined;
462
538
  }, {
463
539
  id: string;
464
540
  text: string;
541
+ background?: string | undefined;
542
+ alternative?: string | undefined;
465
543
  }>, "many">>, {
466
544
  id: string;
467
545
  text: string;
546
+ background?: string | undefined;
547
+ alternative?: string | undefined;
468
548
  }[], {
469
549
  id: string;
470
550
  text: string;
551
+ background?: string | undefined;
552
+ alternative?: string | undefined;
471
553
  }[] | undefined>;
472
554
  }, "strip", z.ZodTypeAny, {
473
555
  interfaces: {
@@ -477,6 +559,7 @@ export declare const EntitiesSchema: z.ZodObject<{
477
559
  errors: {
478
560
  status: string;
479
561
  condition: string;
562
+ kind?: "backend" | "guard" | "internal" | undefined;
480
563
  note?: string | undefined;
481
564
  }[];
482
565
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -495,6 +578,12 @@ export declare const EntitiesSchema: z.ZodObject<{
495
578
  default?: string | undefined;
496
579
  desc?: string | undefined;
497
580
  }[] | undefined;
581
+ request_example?: string | undefined;
582
+ success_response_example?: string | undefined;
583
+ failure_response_example?: string | undefined;
584
+ side?: "internal" | "console" | "worker" | undefined;
585
+ consumers?: string[] | undefined;
586
+ producers?: string[] | undefined;
498
587
  }[];
499
588
  tables: {
500
589
  id: string;
@@ -513,6 +602,8 @@ export declare const EntitiesSchema: z.ZodObject<{
513
602
  decisions: {
514
603
  id: string;
515
604
  text: string;
605
+ background?: string | undefined;
606
+ alternative?: string | undefined;
516
607
  }[];
517
608
  }, {
518
609
  interfaces?: {
@@ -522,6 +613,7 @@ export declare const EntitiesSchema: z.ZodObject<{
522
613
  errors: {
523
614
  status: string;
524
615
  condition: string;
616
+ kind?: "backend" | "guard" | "internal" | undefined;
525
617
  note?: string | undefined;
526
618
  }[];
527
619
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -540,6 +632,12 @@ export declare const EntitiesSchema: z.ZodObject<{
540
632
  required?: boolean | undefined;
541
633
  desc?: string | undefined;
542
634
  }[] | undefined;
635
+ request_example?: string | undefined;
636
+ success_response_example?: string | undefined;
637
+ failure_response_example?: string | undefined;
638
+ side?: "internal" | "console" | "worker" | undefined;
639
+ consumers?: string[] | undefined;
640
+ producers?: string[] | undefined;
543
641
  }[] | undefined;
544
642
  tables?: {
545
643
  id: string;
@@ -558,6 +656,8 @@ export declare const EntitiesSchema: z.ZodObject<{
558
656
  decisions?: {
559
657
  id: string;
560
658
  text: string;
659
+ background?: string | undefined;
660
+ alternative?: string | undefined;
561
661
  }[] | undefined;
562
662
  }>;
563
663
  export type Entities = z.infer<typeof EntitiesSchema>;
@@ -25,6 +25,8 @@ export const ErrorSpecSchema = z.object({
25
25
  condition: z.string().min(1),
26
26
  status: z.string().min(1),
27
27
  note: z.string().optional(),
28
+ /** 错误类别:backend=gRPC→HTTP 映射;guard=前端守卫行为;internal=Worker 内部语义(P5 拆分)。 */
29
+ kind: z.enum(['backend', 'guard', 'internal']).optional(),
28
30
  });
29
31
  // ============= Contract entities =============
30
32
  export const InterfaceEntitySchema = z.object({
@@ -36,6 +38,13 @@ export const InterfaceEntitySchema = z.object({
36
38
  response_fields: z.array(FieldSchema).optional(),
37
39
  errors: z.array(ErrorSpecSchema).min(1), // G2: errors required
38
40
  failure_examples: z.array(z.string()).min(1), // G2 hard gate
41
+ // 整体性文档扩展(P3/P4/P11):真实示例 body + 契约面与消费方/生产方(契约单一定义)。
42
+ request_example: z.string().optional(), // JSON body(无请求体方法省略或标注"无请求体")
43
+ success_response_example: z.string().optional(),
44
+ failure_response_example: z.string().optional(),
45
+ side: z.enum(['console', 'worker', 'internal']).optional(),
46
+ consumers: z.array(z.string()).optional(), // 消费方仓
47
+ producers: z.array(z.string()).optional(), // 生产方仓
39
48
  });
40
49
  export const TableEntitySchema = z.object({
41
50
  id: z.string().trim().min(1), // stable id, e.g. "T1"
@@ -47,7 +56,9 @@ export const TableEntitySchema = z.object({
47
56
  });
48
57
  export const DecisionEntitySchema = z.object({
49
58
  id: z.string().trim().min(1), // e.g. "D1"
50
- text: z.string().min(1),
59
+ text: z.string().min(1), // 结论
60
+ background: z.string().optional(), // ADR 背景(P6:决策前移为独立章节)
61
+ alternative: z.string().optional(), // 备选(已否决)
51
62
  });
52
63
  export const EntitiesSchema = z.object({
53
64
  interfaces: nullish(z.array(InterfaceEntitySchema).default([])),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gordon.gan/specflow",
3
- "version": "1.8.2-beta",
3
+ "version": "1.8.4-beta",
4
4
  "type": "module",
5
5
  "description": "SpecFlow — unified spec-driven development: OpenSpec planning + Superpowers execution in one CLI and cross-IDE workflow",
6
6
  "keywords": [
@@ -1,4 +1,4 @@
1
- # 章节填充:api-design(接口设计 · 契约 + 前端对接)
1
+ # 章节填充:api-design(接口与数据设计 · 契约 + 数据模型 + 前端对接)
2
2
 
3
3
  你是方案文档撰写员。按大纲要点"填空题"式展开本章,逐要点填充,不自由发挥。
4
4
  - kind=entity|mixed 的要点 → 产出结构化契约实体(JSON 块 ```json {"interfaces": [...], "tables": [...], "decisions": [...]} ```)
@@ -7,20 +7,36 @@
7
7
  - 禁止 stub(TODO/待补充/此处省略);禁止含糊词
8
8
  - **事实从工程数据源提取,意图向用户询问,推理由你完成**(见 `prompts/document/shared/grounding.md`):本章涉及的接口字段/表列/组件名/依赖版本必须来自工程,缺失的关键信息列入缺口清单一次性询问用户,禁止编造
9
9
 
10
+ ## 本章定位:接口与数据整合呈现(P11)
11
+
12
+ 本章同时承载**接口契约**与**数据模型**,二者不得游离:
13
+
14
+ 1. 固定顺序:接口总览(含面/消费方/生产方)→ 请求字段表 → 请求示例 → 响应字段表 → 响应示例 → 失败示例 → 错误表(按类别)→ **数据流总览与契约-表映射**(每接口 ↔ 读写表/列)→ 表结构与 DDL → 存量填充(G3)→ 回滚兼容(G4)。
15
+ 2. **契约-表映射表(强制)**:每个接口一行,列 = 契约 / 读写 / 表·列 / 关键说明。例:`step-results → 写 scenario_step_results(幂等键 execution_job_uid+step_order)`。契约字段与表列必须对得上,禁止接口讲接口、表讲表。
16
+ 3. DDL 就近:表结构在本章给出说明,完整 CREATE TABLE 可放附录并交叉引用(零 DDL 迭代必须显式声明「零迁移」,并展示现网 DDL 参考,禁止假装不涉及数据库)。
17
+ 4. **跨仓合成(≥2 仓)**:接口**单一定义**——同一 method+path 只定义一个实体,用 `side`(console/worker/internal)与 `consumers`/`producers`(仓名)标注;`<repo>_<id>` 仅作追溯别名,禁止按仓重复。仓归属用 `[repo]` 内联标注或表格「归属」列。
18
+
10
19
  ## 接口章节硬规则(借鉴 approval api-guidance + F1/F2 前端对接合并)
11
20
 
12
21
  1. **分层契约,禁止混层**:每个契约面对应一个独立 `In`(L2 Worker HTTP / L3 RPC+HTTP / L4 客户端 RPC),禁止「内部经 I7 一行代替 L4 详设」。
13
22
  2. **RPC / 服务名冻结**:禁止「暂定 / 如 Xxx / 实现时命名」;给出冻结的 RPC 名 + proto 字段号 + http_path。
14
23
  3. **proto 最小集**:新接口尽量给出可生成的 Proto 草案(rpc 名 / message / field 编号 / google.api.http)。
15
24
  4. **G2 失败示例(强制)**:清单中每个接口(含「不变」)除成功示例外,必须 ≥1 组失败示例(参数校验失败/租约过期/未认证),附完整 HTTP 或等价示例。只有错误码表不合格。
16
- 5. **固定顺序**:元信息请求体字段(或路径/Query/CLI flags)→ 请求示例成功响应字段响应示例(成功)响应示例(失败)(G2)错误表 →(可选)处理顺序。
25
+ 5. **固定顺序**:元信息(面/消费方/生产方)→ **请求字段表** **请求示例(真实 JSON body,禁止空壳;无请求体方法显式标注「无请求体」)****响应字段表****成功响应示例(真实 JSON)** **失败示例(G2,可附失败响应 body)** **错误表(backend/guard/internal 分类)**。
17
26
  6. **接口清单稳定编号**:`In` 稳定,供页面引用与跨章交叉引用。
27
+ 7. **契约-表映射**:见上文「本章定位」第 2 条,接口清单后紧跟映射表。
28
+
29
+ ## 数据模型硬规则(G3/G4)
30
+
31
+ 8. 每张表必须有完整 CREATE TABLE(含 ENGINE/CHARSET)与字段说明。
32
+ 9. JSON/新列必须有存量填充策略(G3);必须有回滚数据兼容说明(G4)。
33
+ 10. 零 DDL 迭代仍须展示现网 DDL(禁止假装不涉及数据库),并显式声明「零迁移」。
18
34
 
19
35
  ## 前端对接维度(O5–O7,F1 §2.2 合并「契约+对接」)
20
36
 
21
- 7. **前端请求封装**:统一请求层(拦截器:token 注入/统一错误处理/超时/重试策略/缓存),DTO → VO 映射层。禁止「每个页面自己 fetch + 自己处理错误」。
22
- 8. **前端错误处理统一**:后端错误码 → 前端统一错误态映射(Loading-Empty-Error),禁止前后端各写一套错误码约定。
23
- 9. **接口 Mock 与契约先行**:前端并行开发用 Mock,Mock 的数据结构必须与冻结契约一致(R5);联调时切换真实接口不改变前端代码结构。
24
- 10. **优先级**:项目约定 + 现网 OpenAPI/proto > SpecFlow 骨架 > LLM。
37
+ 11. **前端请求封装**:统一请求层(拦截器:token 注入/统一错误处理/超时/重试策略/缓存),DTO → VO 映射层。禁止「每个页面自己 fetch + 自己处理错误」。
38
+ 12. **前端错误处理统一**:后端错误码 → 前端统一错误态映射(Loading-Empty-Error),禁止前后端各写一套错误码约定。
39
+ 13. **接口 Mock 与契约先行**:前端并行开发用 Mock,Mock 的数据结构必须与冻结契约一致(R5);联调时切换真实接口不改变前端代码结构。
40
+ 14. **优先级**:项目约定 + 现网 OpenAPI/proto > SpecFlow 骨架 > LLM。
25
41
 
26
42
  > 前后端契约一致性是本章最高门禁:字段名、错误码、DTO 结构前后端必须对齐,禁止各写各的。
@@ -27,4 +27,6 @@
27
27
 
28
28
  6. **禁臆造组件树(README §1.1)**:组件树必须能追溯到页面/路由清单与设计输入;不存在于输入或推断来源的页面树禁止臆造。
29
29
 
30
+ 7. **只列真实存在的组件(跨仓整体性,P1)**:只列出实际存在的组件/模块(前端组件树、后端逻辑模块、Worker 模块),每个标注归属仓(如 `[web]`/`[talos]`);**某仓在某维度没有内容就不出现**(禁止「某仓无 UI」「某仓无组件」类占位小节)。
31
+
30
32
  > 优先级:项目约定 > SpecFlow guidance > LLM。项目禁令不得被通用规则覆盖。
@@ -8,7 +8,10 @@
8
8
 
9
9
  ## 时序图硬规则(README §1.7 架构图规范 + seqdiagram 精神)
10
10
 
11
- 1. **必须有图**:主链路时序图用 Mermaid `sequenceDiagram` 源码块(diagram as code,可进 git/diff)。禁止用文字描述代替图,也禁止贴图片。
11
+ 1. **必须有图(≥2 张,P12)**:
12
+ - **整体流程图**:Mermaid `flowchart` 源码块,覆盖「编排 → 编译 → 入队 → 执行 → 上报 → 展示」全链路,**含失败分支**(编译失败不写库 / 执行失败 / 上报失败 abort)。
13
+ - **主链路时序图**:Mermaid `sequenceDiagram` 源码块(diagram as code,可进 git/diff)。禁止用文字描述代替图,也禁止贴图片。
14
+ - **失败路径决策图**:执行失败 vs 上报失败的分支单独成图(可并入整体流程图,但分支必须可见)。
12
15
 
13
16
  2. **必须有文字说明图(图 + 文成对)**:
14
17
  - 图前写一段「本图说明」:这条链路在讲什么、参与者(lifeline)分别是谁。
@@ -1,5 +1,7 @@
1
1
  # 章节填充:data-model(数据结构/数据模型变更)
2
2
 
3
+ > **整合说明(P11)**:跨仓合成(≥2 仓)时,本章内容并入「接口与数据设计」章(api-design)——接口、数据模型与契约-表映射同章呈现,不单独成章。单仓文档可继续独立使用本章。无论哪种形态,**契约-表映射**(每接口 ↔ 读写表/列)都必须给出,禁止接口讲接口、表讲表。
4
+
3
5
  你是方案文档撰写员。按大纲要点"填空题"式展开本章,逐要点填充,不自由发挥。
4
6
  - kind=entity|mixed 的要点 → 产出结构化契约实体(JSON 块 ```json {"interfaces": [...], "tables": [...], "decisions": [...]} ```)
5
7
  - kind=narrative|mixed 的要点 → 产出叙述 Markdown
@@ -15,4 +17,5 @@
15
17
  4. **G3 存量填充**:JSON 形状变更或新增列时必须写存量默认值填充策略(回填 SQL/读时默认值/禁止空读/上线顺序)。
16
18
  5. **G4 回滚兼容**:回滚后旧版本能否安全跳过/忽略新数据?写明机制(omitempty/忽略未知键/version 分派)。禁止只写「回滚应用」。
17
19
  6. **零 DDL 迭代**:仍须展示现网 DDL,禁止假装「不涉及数据库」。
18
- 7. **优先级**:项目约定 > SpecFlow guidance > LLM。项目禁令(如「本迭代禁止迁移」)不得被通用规则覆盖。
20
+ 7. **契约-表映射(强制)**:本章(或并入的接口与数据设计章)必须给出契约-表映射表——每个相关接口一行(契约 / 读写 / 表·列 / 关键说明),字段与列对齐。
21
+ 8. **优先级**:项目约定 > SpecFlow guidance > LLM。项目禁令(如「本迭代禁止迁移」)不得被通用规则覆盖。
@@ -44,3 +44,9 @@
44
44
  5. **source_segments**:要点尽量标注来源段 id(来自全局摘要的段 id)。
45
45
  6. **每要点引用 ≤3 个实体**。
46
46
  7. **要点数量**:每章 2-5 个要点,覆盖该章必须覆盖的维度。
47
+ 8. **接口与数据整合(P11)**:当 `api-design` 与 `data-model` 同时被选中时,合并为单一「接口与数据设计」章(id 用 `api-design`),数据模型作为该章小节(数据流总览 → 契约-表映射 → 表结构/DDL → 存量填充 G3 → 回滚 G4),不再单列 `data-model` 章;DDL 可放附录并交叉引用。
48
+ 9. **完整性骨架(P7)**:涉及 ≥2 仓的合成文档必须含 `术语与约定`、`非目标汇总`、`风险与开放问题`、`决策记录(ADR)` 章节位(决策在正文用 `[D#]` 引用,先定义后引用)。
49
+ 10. **实体注册表增强**:接口实体注册时记录 `side`(console/worker/internal)、`consumers`、`producers`(仓名);同一 method+path 只注册一个实体,`<repo>_<id>` 前缀仅作追溯别名(防 `duplicate_contract`)。
50
+ 11. **主流程含图(P12)**:`core-flow` 章必须输出整体流程图(含失败分支)+ 时序图 + 失败路径决策图,图必须有文字说明。
51
+ 12. **文档头与目录**:最终文档由引擎确定性渲染元信息头(标题/参与仓/生成时间)与目录;大纲只决定章节序与要点,不写文档头。
52
+ 13. **契约质量**:接口要点要求示例带真实 JSON body(`request_example`/`success_response_example`/`failure_response_example`)、请求/响应字段分列、错误表按 backend/guard/internal 分类。
@@ -82,3 +82,10 @@ OpenAPI(`openapi.yaml`/`swagger.json` 等)、proto3(`*.proto`)、SQL DDL
82
82
  - 组件/页面:组件名、路由路径、状态 store 名应来自工程;页面清单可与用户核对。
83
83
  - 性能:基准数值必须来自实测(或标注"待实测"),禁止编造 LCP/包体数字。
84
84
  - 依赖升级:当前版本/目标版本来自 package.json;升级理由与收益向用户确认。
85
+
86
+ ## 七、整体性文档写作约定(P9/P11,所有章节通用)
87
+
88
+ 1. **首次定义、后续引用**:术语、决策(`[D#]`)、关键事实(幂等键、零 DDL、禁直连等)只在首次出现处定义,后续一律引用,禁止跨章重复整段(防 P9 重复)。
89
+ 2. **接口与数据同源**:接口字段与表列来自同一工程数据源;写契约-表映射时,字段↔列必须一一对齐,禁止接口讲接口、表讲表(P11)。
90
+ 3. **主流程必有图**:涉及多步流程的章节(core-flow 等)必须给出 mermaid 图(整体流程图/时序图),图必须有文字说明(P12)。
91
+ 4. **某仓无内容则不出现**:跨仓写作时,某仓在某维度无内容(如后端无 UI 组件)就**不写该仓的占位小节**,只写真实存在的内容(防"无 X"占位)。
@@ -47,18 +47,18 @@ Cursor: `specflow:techdoc-synth run ...`; Codex: `$specflow-techdoc-synth run ..
47
47
  - 无 `--text` → **回退 `approve`**(原合成默认,不打断流程,保持向后兼容)。
48
48
  5. 可选:`specflow techdoc detect --text "..."` 查看确定性关键词识别的结果作为参考。
49
49
 
50
- > 场景识别决定合成文档的**章节结构**(如 bugfix → reproduce/root-cause/fix;feature → requirement/test-strategy;poc → research/poc-demo/benchmark/tech-selection;migration → compat-migration/migration-guide)。跨仓合成规则(统一大纲/命名空间/分仓小节/全局视角/跨仓依赖)对任何 profile 都适用。
50
+ > 场景识别决定合成文档的**章节结构**(如 bugfix → reproduce/root-cause/fix;feature → requirement/test-strategy;poc → research/poc-demo/benchmark/tech-selection;migration → compat-migration/migration-guide)。跨仓合成规则(统一大纲/契约单一定义/整体性组织/全局视角/跨仓依赖)对任何 profile 都适用。
51
51
 
52
52
  ## 与 `techdoc approve --bundle` 的区别(为什么用 synth)
53
53
 
54
54
  | 维度 | `approve --bundle`(拼接) | `techdoc-synth`(合成) |
55
55
  |---|---|---|
56
56
  | 大纲 | 每仓独立 | **一次统一大纲**,章节覆盖所有仓 |
57
- | 契约实体 | 每仓独立 I1/T1,跨仓冲突 | **命名空间隔离**(`<repo>_<id>`) |
58
- | 章节内容 | 各仓各写各的 | **每章按仓分节**(`### <repo>`),细节不互相稀释 |
57
+ | 契约实体 | 每仓独立 I1/T1,跨仓冲突 | **单一定义 + 追溯别名**(`<repo>_<id>`),同一端点不重复 |
58
+ | 章节内容 | 各仓各写各的 | **整体性叙述**(仓归属用 `[repo]` 标注),某仓无内容则不出现 |
59
59
  | 全局视角 | 无 | goal 写多仓总目标;closed-loop/implementability 跨仓统一结论 |
60
60
  | 跨仓依赖 | 不识别 | 显式标注「跨仓依赖」(接口调用/数据共享/发布顺序) |
61
- | 质量门禁 | 无 | **分仓小节门禁**(validate 强制:每必选章含所有仓的 `### <repo>` 小节) |
61
+ | 质量门禁 | 无 | **整体性门禁**(validate 强制:全文仓覆盖 `repo_coverage_missing` + 空壳小节 `repo_section_stub` + 契约质量 lint) |
62
62
 
63
63
  > 原有多仓多产物(`techdoc approve --workspace-root ... --bundle` 各仓独立文档 + 主仓合订)**保持不变**;`techdoc-synth` 是新增的"真正合成"路径,两者并存。
64
64
 
@@ -96,17 +96,20 @@ run [--workspace-root <root>]
96
96
  ## 跨仓合成规则(必须遵守,validate 强制)
97
97
 
98
98
  1. **统一大纲**:一次生成一份 outline,章节覆盖所有仓,**不按仓分章**(跨仓对比/汇总放进对应章节)。
99
- 2. **契约实体命名空间**:跨仓实体 id 用 `<repo>_<id>` 前缀(如 `repo1_I1`、`repo2_T1`),**禁止不同仓共用裸 id**。
100
- 3. **每章必须分仓小节(质量门禁)**:每个必选章节的叙述按仓分节,用 `### <repo>` 小节标题(可带后缀如 `### repo1 · 目标`),且**每个仓都必须出现**。禁止把多仓内容混写成一段(会导致细节互相稀释)。
101
- 4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection 对比各仓技术方案;api-design/data-model 分仓列契约;closed-loop 跨仓统一闭环检查;implementability 跨仓统一可实施性评估。其他场景章节(如 reproduce/root-cause/fix、requirement/test-strategy、research/poc-demo/benchmark)同样按仓分节并给出跨仓结论。
99
+ 2. **契约单一定义**:同一物理端点(method+path)只定义一个实体,用 `side`(console/worker/internal)与 `consumers`/`producers`(仓名)标注面与消费方/生产方;`<repo>_<id>` 前缀仅作**追溯别名**,禁止不同仓重复定义同一接口(validate `duplicate_contract` 拦截)。
100
+ 3. **整体性组织(替代按仓分节)**:每章按主题写成一段整体叙述,仓归属用内联 `[repo]` 标注或表格「归属」列;**禁止**为无内容的仓写占位小节(如「某仓无 UI」,validate `repo_section_stub` 拦截);validate 以全文级 `repo_coverage_missing` 防某仓被概括掉。
101
+ 4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection 对比各仓技术方案;**接口与数据合并为「接口与数据设计」章**(含契约-表映射表:每接口 ↔ 读写表/列);closed-loop 跨仓统一闭环检查;implementability 跨仓统一可实施性评估。其他场景章节(如 reproduce/root-cause/fix、requirement/test-strategy、research/poc-demo/benchmark)同样给出跨仓结论。
102
102
  5. **跨仓依赖/风险**:涉及仓间依赖(接口调用/数据共享/发布顺序)时显式标注「跨仓依赖」。
103
+ 6. **完整性骨架**:合成文档必须含术语与约定表、非目标汇总、风险与开放问题、决策记录(ADR,正文用 `[D#]` 引用且先定义后引用)。
104
+ 7. **契约质量**:接口示例带真实 JSON body(空壳示例拦截)、请求/响应字段分列、错误表按 backend/guard/internal 分类。
105
+ 8. **主流程必须含图**:主业务流程章输出整体流程图(含失败分支)+ 时序图 + 失败路径决策图。
103
106
 
104
- > 门禁是确定性的:`synthesize` 在 workRoot 写入 `repos.json`,`document validate` 读取后对每个必选章节检查 `### <repo>` 小节,缺失 error(`repo_section_missing`)。非 synthesize 工作目录(`document run` / `approve --bundle`)不写 repos.json,门禁不触发。
107
+ > 门禁是确定性的:`synthesize` 在 workRoot 写入 `repos.json`,`document validate` 读取后执行**整体性组织门禁**(全文级仓覆盖 `repo_coverage_missing` + 空壳小节 `repo_section_stub`)与契约质量 lint(`duplicate_contract`/`request_example_missing`/`response_example_missing`/`data_contract_mapping_missing`/`forward_decision_ref`)。非 synthesize 工作目录(`document run` / `approve --bundle`)不写 repos.json,跨仓门禁不触发。
105
108
 
106
109
  ## 产物
107
110
 
108
111
  - `<workspaceRoot>/.specflow/document-synthesized/document.md`(可用 `--work-root` 指定输出目录)
109
- - `repos.json`:参与合成的仓清单(门禁依据,自动写入)
112
+ - `repos.json`:参与合成的仓清单(整体性门禁依据,自动写入)
110
113
 
111
114
  ## 说明
112
115
 
@@ -1,29 +1,50 @@
1
- # 接口设计(候选;依赖架构;0→1 全栈/功能场景合并「契约+前端对接」)
1
+ # 接口与数据设计(候选;依赖架构)
2
+ # 整合呈现:接口契约(分层契约 + 前端对接)与数据模型(DDL/G3/G4)同章,含契约-表映射表。
3
+ # 跨仓合成时遵循 buildSynthRules:接口单一定义(side/consumers/producers)、字段表请求/响应分列、
4
+ # 示例带真实 JSON body、错误表按 backend/guard/internal 分类。
2
5
  id: api-design
3
- title: 接口设计
4
- when: input.containsApiChange
6
+ title: 接口与数据设计
7
+ when: input.containsApiChange || input.hasPersistence
5
8
  depends_on: [architecture]
6
9
  outline_points:
7
- - { id: O1, text: 接口总览与调用方/鉴权, required: true, kind: narrative }
8
- - { id: O2, text: 逐接口字段表 + 成功示例, required: true, kind: mixed }
10
+ - { id: O1, text: 接口总览与调用方/鉴权(含面/消费方/生产方), required: true, kind: narrative }
11
+ - { id: O2, text: 逐接口请求/响应字段表分列 + 请求/成功响应示例(真实 JSON body), required: true, kind: mixed }
9
12
  - { id: O3, text: 每个接口 ≥1 失败示例(G2), required: true, kind: entity }
10
- - { id: O4, text: 错误码映射, required: true, kind: entity }
13
+ - { id: O4, text: 错误码映射(backend/guard/internal 分类), required: true, kind: entity }
11
14
  - { id: O5, text: 前端请求封装(拦截器/重试/缓存/DTO→VO), required: false, kind: narrative }
12
15
  - { id: O6, text: 前端错误处理(错误码统一/Loading-Empty-Error 状态), required: false, kind: narrative }
13
16
  - { id: O7, text: 接口 Mock 与联调(Mock 策略/契约先行,前端可并行开发), required: false, kind: narrative }
17
+ - { id: DM1, text: 数据流总览与契约-表映射(每接口 ↔ 读写表/列), required: true, kind: mixed }
18
+ - { id: DM2, text: 表结构与 DDL(既有表参考标注), required: true, kind: entity }
19
+ - { id: DM3, text: 字段说明 + 本迭代用法, required: true, kind: entity }
20
+ - { id: DM4, text: 存量填充策略(G3), required: true, kind: narrative }
21
+ - { id: DM5, text: 回滚数据兼容(G4), required: true, kind: narrative }
14
22
  entities:
15
23
  interfaces:
16
24
  required: [id, name, method, path, errors]
17
25
  each:
18
26
  failure_example: required
19
27
  errors_table: required
28
+ request_example: required
29
+ success_response_example: required
30
+ tables:
31
+ required: [id, name, ddl]
32
+ each:
33
+ migration: required
34
+ rollback: required
20
35
  narratives:
21
36
  design_notes: required
22
37
  gates:
23
38
  - 禁止「暂定/如/实现时」接口名
24
39
  - 每个接口必须 ≥1 失败示例
25
40
  - 接口编号稳定可交叉引用
41
+ - 请求字段与响应字段分列(禁止混成一张表)
42
+ - POST/PUT/PATCH 必须给真实 JSON 请求示例(无请求体显式标注「无请求体」);有响应字段必须有成功响应示例
43
+ - 错误表按 backend(gRPC→HTTP 映射)/ guard(前端守卫行为)/ internal(Worker 内部语义)分类
26
44
  - 涉及前端对接时,请求封装与错误码统一方案必须与契约对齐(R5,字段名/错误码禁各写各的)
27
45
  - 前端接口 Mock 必须与契约一致(禁 Mock 一套、真实接口另一套)
46
+ - 有表实体时必须给出「契约-表映射」(每接口 ↔ 读写表/列)
47
+ - 每张表必须有完整 CREATE TABLE(含 ENGINE/CHARSET);零 DDL 迭代仍须展示现网 DDL(禁止假装不涉及数据库)
48
+ - JSON/新列必须有存量填充策略(G3);必须有回滚数据兼容说明(G4)
28
49
  map_prompt: prompts/document/map/api-design.md
29
- output_budget_tokens: 8000
50
+ output_budget_tokens: 12000
@@ -18,6 +18,8 @@ narratives:
18
18
  core_flow: required
19
19
  gates:
20
20
  - 主链路时序图必须 Mermaid 源码(```mermaid sequenceDiagram)+ 可渲染(diagram as code)
21
+ - 必须含整体流程图(```mermaid flowchart,覆盖编排→编译→入队→执行→上报→展示,含失败分支)
22
+ - 失败路径/决策部分必须单独成图(执行失败 vs 上报失败分支)
21
23
  - 时序图必须有文字说明(逐消息/逐分支),禁止只有图无文字
22
24
  - 每条主链路必须画异常/失败分支(禁只画 happy path)
23
25
  - 有写操作的链路必须给幂等/并发结论(禁「前端按钮防抖」当幂等方案)