@gordon.gan/specflow 1.8.3-beta → 1.8.5-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.
- package/dist/cli/commands/document-run.js +6 -3
- package/dist/core/document/engine.js +46 -24
- package/dist/core/document/gates.js +33 -7
- package/dist/core/document/lint.d.ts +37 -1
- package/dist/core/document/lint.js +201 -6
- package/dist/core/document/render.d.ts +7 -4
- package/dist/core/document/render.js +100 -53
- package/dist/core/document/schemas.d.ts +100 -0
- package/dist/core/document/schemas.js +12 -1
- package/package.json +1 -1
- package/prompts/document/map/api-design.md +11 -3
- package/prompts/document/map/architecture.md +1 -1
- package/prompts/document/map/component-design.md +2 -0
- package/prompts/document/map/core-flow.md +4 -1
- package/prompts/document/map/core-logic.md +10 -3
- package/prompts/document/map/data-model.md +5 -2
- package/prompts/document/map/requirement.md +6 -1
- package/prompts/document/map/test-strategy.md +1 -1
- package/prompts/document/outline/general.md +9 -0
- package/prompts/document/shared/grounding.md +7 -0
- package/prompts/shared/artifact-language.md +9 -0
- package/skills/specflow-techdoc-synth/SKILL.md +12 -9
- package/templates/document/chapters/api-design.yaml +14 -5
- package/templates/document/chapters/architecture.yaml +1 -2
- package/templates/document/chapters/core-flow.yaml +2 -0
- package/templates/document/chapters/core-logic.yaml +8 -6
- package/templates/document/chapters/data-model.yaml +9 -5
- package/templates/document/chapters/mvp-boundary.yaml +0 -3
- package/templates/document/chapters/requirement.yaml +5 -7
- package/templates/document/chapters/tech-selection.yaml +0 -3
- package/templates/document/chapters/test-strategy.yaml +2 -5
- package/templates/document/chapters/ui-design.yaml +0 -3
- package/templates/document/profiles/approve.yaml +3 -2
- package/templates/document/profiles/feature.yaml +1 -1
|
@@ -717,12 +717,15 @@ function buildSynthRules(dirs) {
|
|
|
717
717
|
return [
|
|
718
718
|
`## 跨仓合成规则(必须遵守)`,
|
|
719
719
|
`1. **统一大纲**:一次生成一份 outline,章节覆盖所有仓,不按仓分章(跨仓对比/汇总放进对应章节)。`,
|
|
720
|
-
`2.
|
|
721
|
-
`3.
|
|
722
|
-
`4. **全局视角**(若所选 profile 包含对应章节):goal 写多仓总目标与边界;tech-selection
|
|
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/存量 G3/回滚 G4),契约-表映射写数据模型章,两章按「接口与数据设计 → 数据模型」顺序并列;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#]\` 引用**——决策理由沉淀在技术选型(否决/备选列)与风险章。`,
|
|
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,
|
|
20
|
+
import { lintNarrative, checkRepoCoverage, checkRepoSectionStub, checkDuplicateContract, checkExamplePresence, checkForwardDecisionRef, checkUnverifiedNumber, checkDataContractMapping, checkBareIdentifiers, checkTextHeavyNoDiagram } 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
|
-
//
|
|
679
|
-
//
|
|
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,40 @@ 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
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
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
|
+
for (const d of checkBareIdentifiers(chId, narrative)) {
|
|
712
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
713
|
+
}
|
|
714
|
+
for (const d of checkTextHeavyNoDiagram(chId, narrative)) {
|
|
715
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (synthRepos.length >= 2) {
|
|
719
|
+
for (const d of checkDataContractMapping(narrativeMap, allEntities)) {
|
|
720
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
|
|
721
|
+
}
|
|
722
|
+
}
|
|
701
723
|
// Structured coverage (entity points must be filled).
|
|
702
724
|
for (const d of checkStructuredCoverage(outline, entityMap)) {
|
|
703
725
|
diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
|
|
@@ -710,7 +732,7 @@ export async function validateWork(options) {
|
|
|
710
732
|
return { ok: false, diagnostics: diags };
|
|
711
733
|
}
|
|
712
734
|
// Render (appendices driven by profile.appendices).
|
|
713
|
-
const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI });
|
|
735
|
+
const documentMd = renderDocument({ outline, entities: allEntities, narratives: narrativeMap, profile, appendixAntiAI, repos: synthRepos.length > 0 ? synthRepos : undefined });
|
|
714
736
|
await fs.writeFile(paths.documentMd, documentMd, 'utf-8');
|
|
715
737
|
let documentHtml;
|
|
716
738
|
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,
|
|
5
|
+
import { lintNarrative, checkBudget, checkDiagramPresence, checkNumericPresence, checkGoldenSignals, checkRepoCoverage, checkRepoSectionStub, checkDuplicateContract, checkExamplePresence, checkForwardDecisionRef, checkUnverifiedNumber, checkDataContractMapping, checkBareIdentifiers, checkTextHeavyNoDiagram, } 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,44 @@ export async function runGates(input) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
// Multi-repo synthesize gate (library pipeline):
|
|
52
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
+
for (const d of checkBareIdentifiers(chId, narrative)) {
|
|
78
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
79
|
+
}
|
|
80
|
+
for (const d of checkTextHeavyNoDiagram(chId, narrative)) {
|
|
81
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (input.repos && input.repos.length >= 2) {
|
|
85
|
+
for (const d of checkDataContractMapping(input.chapterNarratives, input.entities)) {
|
|
86
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
63
89
|
// structured coverage
|
|
64
90
|
for (const d of checkStructuredCoverage(input.outline, input.chapterEntities)) {
|
|
65
91
|
diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
|
|
@@ -36,4 +36,40 @@ export interface RepoSectionDiagnostic {
|
|
|
36
36
|
message: string;
|
|
37
37
|
chapter?: string;
|
|
38
38
|
}
|
|
39
|
-
export declare function
|
|
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):有表实体时,数据模型章(或接口章)必须含契约-表映射表。 */
|
|
63
|
+
export declare function checkDataContractMapping(narratives: ReadonlyMap<string, string>, entities: {
|
|
64
|
+
tables?: unknown[];
|
|
65
|
+
}): RepoSectionDiagnostic[];
|
|
66
|
+
/**
|
|
67
|
+
* 正文中文化(warning):叙述行(非代码块/非表格/非标题)出现裸代码标识符时提示用中文业务术语替代。
|
|
68
|
+
* 术语与约定章(glossary)放行——那里本就登记标识符。
|
|
69
|
+
*/
|
|
70
|
+
export declare function checkBareIdentifiers(chapterId: string, narrative: string): RepoSectionDiagnostic[];
|
|
71
|
+
/**
|
|
72
|
+
* 图文结合(warning):叙述正文(非表格/非标题/非代码块)超过阈值行数但整章无 Mermaid 图 →
|
|
73
|
+
* 提示补充图(流程/状态/结构类内容必须配图 + 文字说明)。术语表放行。
|
|
74
|
+
*/
|
|
75
|
+
export declare function checkTextHeavyNoDiagram(chapterId: string, narrative: string, minProseLines?: number): RepoSectionDiagnostic[];
|
|
@@ -124,24 +124,219 @@ 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
|
|
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');
|
|
136
|
+
const diags = [];
|
|
137
|
+
for (const repo of repos) {
|
|
138
|
+
if (countRepoMentions(joined, repo) < threshold) {
|
|
139
|
+
diags.push({
|
|
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) {
|
|
130
149
|
const diags = [];
|
|
131
150
|
for (const repo of repos) {
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
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) {
|
|
135
157
|
diags.push({
|
|
136
|
-
code: '
|
|
158
|
+
code: 'repo_section_stub',
|
|
137
159
|
severity: 'error',
|
|
138
160
|
chapter: chapterId,
|
|
139
|
-
message: `Chapter "${chapterId}"
|
|
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> 仅作追溯别名)`,
|
|
140
180
|
});
|
|
141
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 成功响应示例)`,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return diags;
|
|
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
|
+
}
|
|
142
231
|
}
|
|
143
232
|
return diags;
|
|
144
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):有表实体时,数据模型章(或接口章)必须含契约-表映射表。 */
|
|
251
|
+
export function checkDataContractMapping(narratives, entities) {
|
|
252
|
+
if ((entities.tables?.length ?? 0) === 0)
|
|
253
|
+
return [];
|
|
254
|
+
const narrative = narratives.get('data-model') ?? narratives.get('api-design') ?? '';
|
|
255
|
+
if (!/契约-表映射|映射表|接口.*表.*映射|表.*接口.*映射/.test(narrative)) {
|
|
256
|
+
return [{
|
|
257
|
+
code: 'data_contract_mapping_missing',
|
|
258
|
+
severity: 'warning',
|
|
259
|
+
message: '接口与数据应通过契约-表映射对齐:数据模型章缺少「契约-表映射」(每接口 ↔ 读写表/列),接口约定与数据库仍游离',
|
|
260
|
+
}];
|
|
261
|
+
}
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
// ============= 正文中文化 lint(可读性整改:代码标识符只允许出现在术语表/字段表/示例) =============
|
|
265
|
+
/** 正文叙述中禁止裸用代码标识符的清单(术语表/字段表/JSON 示例/代码块除外)。 */
|
|
266
|
+
const BARE_IDENTIFIERS = [
|
|
267
|
+
'runtime_payload', 'execution_jobs', 'scenario_cases', 'scenario_steps', 'scenario_step_results',
|
|
268
|
+
'param_overrides', 'lease_epoch', 'ref_type', 'step_count', 'job_type', 'execution_mode',
|
|
269
|
+
'step_uid', 'step_order', 'schema_version', 'request_id', 'EnsureCaseResult', 'CompileScenario',
|
|
270
|
+
'ExecuteCase', 'StartDebugExecution', 'SubmitStepResult', 'ListScenarioStepResults', 'ReplaceScenarioSteps',
|
|
271
|
+
'pollJobUntilTerminal', 'case_result_uid', 'execution_job_uid',
|
|
272
|
+
];
|
|
273
|
+
/**
|
|
274
|
+
* 正文中文化(warning):叙述行(非代码块/非表格/非标题)出现裸代码标识符时提示用中文业务术语替代。
|
|
275
|
+
* 术语与约定章(glossary)放行——那里本就登记标识符。
|
|
276
|
+
*/
|
|
277
|
+
export function checkBareIdentifiers(chapterId, narrative) {
|
|
278
|
+
if (/term|glossary|约定|术语/i.test(chapterId))
|
|
279
|
+
return [];
|
|
280
|
+
const diags = [];
|
|
281
|
+
let inFence = false;
|
|
282
|
+
for (const raw of narrative.split('\n')) {
|
|
283
|
+
const line = raw.trimStart();
|
|
284
|
+
if (line.startsWith('```')) {
|
|
285
|
+
inFence = !inFence;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (inFence)
|
|
289
|
+
continue;
|
|
290
|
+
if (line.startsWith('|'))
|
|
291
|
+
continue; // 表格行(字段表)保留原名
|
|
292
|
+
if (/^#{1,6}\s/.test(line))
|
|
293
|
+
continue; // 标题
|
|
294
|
+
for (const id of BARE_IDENTIFIERS) {
|
|
295
|
+
if (new RegExp(`\\b${id}\\b`).test(line)) {
|
|
296
|
+
diags.push({
|
|
297
|
+
code: 'bare_identifier',
|
|
298
|
+
severity: 'warning',
|
|
299
|
+
chapter: chapterId,
|
|
300
|
+
message: `Chapter "${chapterId}" 正文出现裸代码标识符「${id}」——正文应用中文业务术语(如「运行时信封」替代 runtime_payload);标识符只允许在术语表、字段表、JSON/HTTP 示例、代码块中出现`,
|
|
301
|
+
});
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return diags;
|
|
307
|
+
}
|
|
308
|
+
// ============= 图文结合 lint(可读性整改:叙述多的章节必须有图) =============
|
|
309
|
+
/**
|
|
310
|
+
* 图文结合(warning):叙述正文(非表格/非标题/非代码块)超过阈值行数但整章无 Mermaid 图 →
|
|
311
|
+
* 提示补充图(流程/状态/结构类内容必须配图 + 文字说明)。术语表放行。
|
|
312
|
+
*/
|
|
313
|
+
export function checkTextHeavyNoDiagram(chapterId, narrative, minProseLines = 30) {
|
|
314
|
+
if (/term|glossary|约定|术语/i.test(chapterId))
|
|
315
|
+
return [];
|
|
316
|
+
let prose = 0;
|
|
317
|
+
let inFence = false;
|
|
318
|
+
for (const raw of narrative.split('\n')) {
|
|
319
|
+
const line = raw.trimStart();
|
|
320
|
+
if (line.startsWith('```')) {
|
|
321
|
+
inFence = !inFence;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
if (inFence)
|
|
325
|
+
continue;
|
|
326
|
+
if (line.startsWith('|') || /^#{1,6}\s/.test(line) || line.trim() === '')
|
|
327
|
+
continue;
|
|
328
|
+
prose += 1;
|
|
329
|
+
}
|
|
330
|
+
if (prose >= minProseLines && !/```mermaid/.test(narrative)) {
|
|
331
|
+
return [{
|
|
332
|
+
code: 'text_heavy_no_diagram',
|
|
333
|
+
severity: 'warning',
|
|
334
|
+
chapter: chapterId,
|
|
335
|
+
message: `Chapter "${chapterId}" 叙述较多(${prose} 行)但无图——建议图文结合:流程/状态流转/结构/决策分支用 Mermaid 图(flowchart/sequenceDiagram/stateDiagram-v2)+ 文字说明,叙述分条输出`,
|
|
336
|
+
}];
|
|
337
|
+
}
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
145
340
|
function escapeRegExp(s) {
|
|
146
341
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
147
342
|
}
|
|
@@ -16,11 +16,10 @@ export declare function escapeFenceContent(s: string): string;
|
|
|
16
16
|
export declare function escapeInline(s: string): string;
|
|
17
17
|
export declare function renderInterface(i: InterfaceEntity): string;
|
|
18
18
|
export declare function renderTable(t: TableEntity): string;
|
|
19
|
-
export declare function renderDecisions(decisions: Array<{
|
|
20
|
-
id: string;
|
|
21
|
-
text: string;
|
|
22
|
-
}>): string;
|
|
23
19
|
export declare function renderEntities(entities: Entities): string;
|
|
20
|
+
/** 契约实体按「面」分组渲染(Console 面 / 内部与 Worker 面),供接口与数据设计章就地展开。 */
|
|
21
|
+
export declare function renderInterfaces(interfaces: InterfaceEntity[]): string;
|
|
22
|
+
export declare function renderTables(tables: TableEntity[]): string;
|
|
24
23
|
export interface RenderDocumentInput {
|
|
25
24
|
outline: Outline;
|
|
26
25
|
entities: Entities;
|
|
@@ -32,6 +31,10 @@ export interface RenderDocumentInput {
|
|
|
32
31
|
};
|
|
33
32
|
/** LLM-written anti-AI self-check items (from appendices/anti-ai.md, optional). */
|
|
34
33
|
appendixAntiAI?: string;
|
|
34
|
+
/** 参与仓清单(synth 模式):渲染文档头元信息(P7 整体性)。 */
|
|
35
|
+
repos?: string[];
|
|
36
|
+
/** 是否渲染产物溯源附录(P8:默认关闭,调试/--signoff 时开启)。 */
|
|
37
|
+
includeProvenance?: boolean;
|
|
35
38
|
}
|
|
36
39
|
/** Strip engine repair notes (`<!-- review-fix ... -->`) from the FINAL render. The audit trail
|
|
37
40
|
* stays in `chapters/<id>.md` and `review-result.json`; the deliverable document must not show
|