@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.
- package/README.md +7 -5
- package/dist/cli/commands/document-run.d.ts +98 -0
- package/dist/cli/commands/document-run.js +1064 -0
- package/dist/cli/index.js +2 -2
- package/dist/core/artifact-language.js +11 -1
- package/dist/core/document/asset-paths.d.ts +15 -0
- package/dist/core/document/asset-paths.js +38 -0
- package/dist/core/document/chapters.d.ts +6 -0
- package/dist/core/document/chapters.js +38 -0
- package/dist/core/document/coverage.d.ts +25 -0
- package/dist/core/document/coverage.js +49 -0
- package/dist/core/document/digests.d.ts +40 -0
- package/dist/core/document/digests.js +144 -0
- package/dist/core/document/engine.d.ts +101 -0
- package/dist/core/document/engine.js +722 -0
- package/dist/core/document/extract.d.ts +61 -0
- package/dist/core/document/extract.js +437 -0
- package/dist/core/{approval → document}/forbidden-patterns.d.ts +4 -2
- package/dist/core/document/forbidden-patterns.js +36 -0
- package/dist/core/document/gates.d.ts +39 -0
- package/dist/core/document/gates.js +75 -0
- package/dist/core/document/input-digest.d.ts +63 -0
- package/dist/core/document/input-digest.js +200 -0
- package/dist/core/document/input-features.d.ts +43 -0
- package/dist/core/document/input-features.js +161 -0
- package/dist/core/document/lint.d.ts +39 -0
- package/dist/core/document/lint.js +147 -0
- package/dist/core/document/llm.d.ts +28 -0
- package/dist/core/document/llm.js +22 -0
- package/dist/core/document/map.d.ts +52 -0
- package/dist/core/document/map.js +296 -0
- package/dist/core/document/outline.d.ts +29 -0
- package/dist/core/document/outline.js +171 -0
- package/dist/core/document/paths.d.ts +23 -0
- package/dist/core/document/paths.js +36 -0
- package/dist/core/document/profile-validator.d.ts +23 -0
- package/dist/core/document/profile-validator.js +155 -0
- package/dist/core/document/profiles.d.ts +23 -0
- package/dist/core/document/profiles.js +76 -0
- package/dist/core/document/render.d.ts +61 -0
- package/dist/core/document/render.js +243 -0
- package/dist/core/document/review.d.ts +57 -0
- package/dist/core/document/review.js +202 -0
- package/dist/core/document/scene-detect.d.ts +32 -0
- package/dist/core/document/scene-detect.js +237 -0
- package/dist/core/document/schemas.d.ts +1111 -0
- package/dist/core/document/schemas.js +191 -0
- package/dist/integrations/shared/capability-evidence.js +4 -9
- package/dist/integrations/shared/command-catalog.js +2 -1
- package/dist/integrations/shared/parity-manifest.js +4 -9
- package/package.json +2 -1
- package/prompts/apply/phase-a-plan.md +1 -1
- package/prompts/document/map/acceptance.md +9 -0
- package/prompts/document/map/anti-ai.md +29 -0
- package/prompts/document/map/api-design.md +26 -0
- package/prompts/document/map/architecture.md +27 -0
- package/prompts/document/map/benchmark.md +26 -0
- package/prompts/document/map/closed-loop.md +9 -0
- package/prompts/document/map/compat-migration.md +31 -0
- package/prompts/document/map/component-design.md +30 -0
- package/prompts/document/map/config-runtime.md +9 -0
- package/prompts/document/map/core-flow.md +62 -0
- package/prompts/document/map/core-logic.md +9 -0
- package/prompts/document/map/data-model.md +18 -0
- package/prompts/document/map/deploy.md +26 -0
- package/prompts/document/map/fix.md +8 -0
- package/prompts/document/map/frontend-architecture.md +35 -0
- package/prompts/document/map/goal.md +8 -0
- package/prompts/document/map/impact.md +8 -0
- package/prompts/document/map/implementability.md +9 -0
- package/prompts/document/map/migration-guide.md +36 -0
- package/prompts/document/map/mvp-boundary.md +8 -0
- package/prompts/document/map/non-goals.md +8 -0
- package/prompts/document/map/ops.md +33 -0
- package/prompts/document/map/performance.md +32 -0
- package/prompts/document/map/poc-demo.md +25 -0
- package/prompts/document/map/regression.md +8 -0
- package/prompts/document/map/reproduce.md +8 -0
- package/prompts/document/map/requirement.md +8 -0
- package/prompts/document/map/research.md +25 -0
- package/prompts/document/map/root-cause.md +8 -0
- package/prompts/document/map/signoff.md +9 -0
- package/prompts/document/map/state-management.md +23 -0
- package/prompts/document/map/tech-selection.md +29 -0
- package/prompts/document/map/test-strategy.md +24 -0
- package/prompts/document/map/ui-design.md +21 -0
- package/prompts/document/outline/general.md +46 -0
- package/prompts/document/review/ai-review.md +57 -0
- package/prompts/document/shared/grounding.md +84 -0
- package/prompts/explore/explore-session.md +1 -1
- package/prompts/propose/design-draft.md +1 -1
- package/prompts/propose/proposal.md +1 -1
- package/prompts/propose/specs.md +1 -1
- package/prompts/propose/tasks-draft.md +1 -1
- package/prompts/refine/design-output.md +1 -1
- package/prompts/refine/update-artifacts.md +2 -0
- package/prompts/shared/artifact-language.md +13 -0
- package/skills/specflow-techdoc/SKILL.md +143 -0
- package/skills/specflow-techdoc-synth/SKILL.md +115 -0
- package/templates/document/chapters/acceptance.yaml +16 -0
- package/templates/document/chapters/api-design.yaml +29 -0
- package/templates/document/chapters/architecture.yaml +21 -0
- package/templates/document/chapters/benchmark.yaml +20 -0
- package/templates/document/chapters/closed-loop.yaml +22 -0
- package/templates/document/chapters/compat-migration.yaml +21 -0
- package/templates/document/chapters/component-design.yaml +22 -0
- package/templates/document/chapters/config-runtime.yaml +16 -0
- package/templates/document/chapters/core-flow.yaml +27 -0
- package/templates/document/chapters/core-logic.yaml +16 -0
- package/templates/document/chapters/data-model.yaml +25 -0
- package/templates/document/chapters/deploy.yaml +22 -0
- package/templates/document/chapters/fix.yaml +18 -0
- package/templates/document/chapters/frontend-architecture.yaml +22 -0
- package/templates/document/chapters/goal.yaml +17 -0
- package/templates/document/chapters/impact.yaml +16 -0
- package/templates/document/chapters/implementability.yaml +22 -0
- package/templates/document/chapters/migration-guide.yaml +21 -0
- package/templates/document/chapters/mvp-boundary.yaml +20 -0
- package/templates/document/chapters/non-goals.yaml +17 -0
- package/templates/document/chapters/ops.yaml +25 -0
- package/templates/document/chapters/performance.yaml +21 -0
- package/templates/document/chapters/poc-demo.yaml +22 -0
- package/templates/document/chapters/regression.yaml +16 -0
- package/templates/document/chapters/reproduce.yaml +16 -0
- package/templates/document/chapters/requirement.yaml +18 -0
- package/templates/document/chapters/research.yaml +22 -0
- package/templates/document/chapters/root-cause.yaml +18 -0
- package/templates/document/chapters/signoff.yaml +15 -0
- package/templates/document/chapters/state-management.yaml +22 -0
- package/templates/document/chapters/tech-selection.yaml +23 -0
- package/templates/document/chapters/test-strategy.yaml +23 -0
- package/templates/document/chapters/ui-design.yaml +28 -0
- package/templates/document/profiles/0to1.yaml +44 -0
- package/templates/document/profiles/approve.yaml +22 -0
- package/templates/document/profiles/bugfix.yaml +15 -0
- package/templates/document/profiles/feature.yaml +23 -0
- package/templates/document/profiles/frontend-0to1.yaml +47 -0
- package/templates/document/profiles/migration.yaml +42 -0
- package/templates/document/profiles/poc.yaml +46 -0
- package/dist/cli/commands/approval-assemble.d.ts +0 -64
- package/dist/cli/commands/approval-assemble.js +0 -408
- package/dist/core/approval/assemble.d.ts +0 -10
- package/dist/core/approval/assemble.js +0 -384
- package/dist/core/approval/bundle.d.ts +0 -9
- package/dist/core/approval/bundle.js +0 -172
- package/dist/core/approval/forbidden-patterns.js +0 -40
- package/dist/core/approval/index-schema.d.ts +0 -676
- package/dist/core/approval/index-schema.js +0 -193
- package/dist/core/approval/index.d.ts +0 -12
- package/dist/core/approval/index.js +0 -9
- package/dist/core/approval/lint.d.ts +0 -10
- package/dist/core/approval/lint.js +0 -302
- package/dist/core/approval/paths.d.ts +0 -13
- package/dist/core/approval/paths.js +0 -43
- package/dist/core/approval/pipeline.d.ts +0 -28
- package/dist/core/approval/pipeline.js +0 -146
- package/dist/core/approval/playbook-schema.d.ts +0 -182
- package/dist/core/approval/playbook-schema.js +0 -51
- package/dist/core/approval/render.d.ts +0 -20
- package/dist/core/approval/render.js +0 -210
- package/dist/core/approval/review-pack.d.ts +0 -26
- package/dist/core/approval/review-pack.js +0 -205
- package/dist/core/approval/types.d.ts +0 -223
- package/dist/core/approval/types.js +0 -1
- package/prompts/approval/acp-pipeline.md +0 -106
- package/prompts/approval/ai-review.md +0 -145
- package/prompts/approval/api-guidance.md +0 -179
- package/prompts/approval/database-guidance.md +0 -105
- package/prompts/approval/frontend-guidance.md +0 -249
- package/prompts/approval/generate.md +0 -1596
- package/prompts/approval/multi-repo-guidance.md +0 -263
- package/prompts/approval/multi-repo-spoke-subagent.md +0 -94
- package/prompts/approval/project-conventions-guidance.md +0 -171
- package/prompts/approval/runtime-guidance.md +0 -64
- package/prompts/approval/segmented-generation.md +0 -157
- package/skills/specflow-approval/SKILL.md +0 -778
- package/templates/approval-index.yaml +0 -93
- package/templates/approval-part.md +0 -15
- package/templates/approval-playbook-talos-scenario-job-compile.yaml +0 -29
- package/templates/approval-playbook.yaml +0 -28
- package/templates/approval.md +0 -261
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — Pass 0.5 input compression + source segmentation (decision 8·P0-A, decision 9).
|
|
3
|
+
*
|
|
4
|
+
* Long inputs are split into stable segments (seg id → original file + line range),
|
|
5
|
+
* each segment is compressed in a bounded-context LLM call, then segment digests are
|
|
6
|
+
* merged into a global input digest (~1–2k tokens). Pass 1 generates the outline from the
|
|
7
|
+
* global digest; Pass 2 pulls original detail by `source_segments` from the segment index.
|
|
8
|
+
*/
|
|
9
|
+
import type { DocumentLLM } from './llm.js';
|
|
10
|
+
export interface InputSegment {
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
label: string;
|
|
14
|
+
source: string;
|
|
15
|
+
startLine: number;
|
|
16
|
+
endLine: number;
|
|
17
|
+
content: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SegmentDigest {
|
|
20
|
+
id: string;
|
|
21
|
+
type: string;
|
|
22
|
+
label: string;
|
|
23
|
+
summary: string;
|
|
24
|
+
entity_candidates: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface GlobalInputDigest {
|
|
27
|
+
segments: SegmentDigest[];
|
|
28
|
+
summary: string;
|
|
29
|
+
}
|
|
30
|
+
export interface SegmentIndex {
|
|
31
|
+
schema: 'specflow.document.segments/v1';
|
|
32
|
+
segments: Array<{
|
|
33
|
+
id: string;
|
|
34
|
+
type: string;
|
|
35
|
+
label: string;
|
|
36
|
+
source: string;
|
|
37
|
+
startLine: number;
|
|
38
|
+
endLine: number;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
export interface SegmentableInput {
|
|
42
|
+
type: string;
|
|
43
|
+
source: string;
|
|
44
|
+
content: string;
|
|
45
|
+
/** Multi-repo source repo name (synthesize). When set, segment ids carry `<repo>` so outline
|
|
46
|
+
* points can reference "repo1 的提案段" and Pass 2 fetches only the involved repo's原文. */
|
|
47
|
+
repo?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 按实际累计字符数切分(P2):此前按「20 行/段」切分,单条超长行会击穿 4000 字符预算,
|
|
51
|
+
* 中文短行则浪费预算。现在累计字符达到预算即开新段;单条超长行强制单独成段。
|
|
52
|
+
* 同时记录行号区间(startLine/endLine)供 Pass 2 fetchSegments 取原文。
|
|
53
|
+
*/
|
|
54
|
+
export declare function segmentInputs(inputs: SegmentableInput[]): InputSegment[];
|
|
55
|
+
export declare function compressSegment(seg: InputSegment, llm: DocumentLLM): Promise<SegmentDigest>;
|
|
56
|
+
/** Heuristic entity-candidate extraction: backtick tokens + PascalCase/snake tokens. */
|
|
57
|
+
export declare function extractEntityCandidates(text: string): string[];
|
|
58
|
+
export declare function mergeSegmentDigests(digests: SegmentDigest[], llm: DocumentLLM): Promise<GlobalInputDigest>;
|
|
59
|
+
export declare function writeSegmentIndex(workRoot: string, segments: InputSegment[]): Promise<string>;
|
|
60
|
+
export declare function readSegmentIndex(workRoot: string): Promise<SegmentIndex>;
|
|
61
|
+
/** Fetch original text for a set of source_segments (bounded, decision 9). */
|
|
62
|
+
export declare function fetchSegments(workRoot: string, segmentIds: string[]): Promise<string>;
|
|
63
|
+
export declare function compressInputs(workRoot: string, inputs: SegmentableInput[], llm: DocumentLLM): Promise<GlobalInputDigest>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — Pass 0.5 input compression + source segmentation (decision 8·P0-A, decision 9).
|
|
3
|
+
*
|
|
4
|
+
* Long inputs are split into stable segments (seg id → original file + line range),
|
|
5
|
+
* each segment is compressed in a bounded-context LLM call, then segment digests are
|
|
6
|
+
* merged into a global input digest (~1–2k tokens). Pass 1 generates the outline from the
|
|
7
|
+
* global digest; Pass 2 pulls original detail by `source_segments` from the segment index.
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { segmentIndexPath } from './paths.js';
|
|
12
|
+
const SEGMENT_CHAR_BUDGET = 4000; // ~1-2k tokens per segment, bounded context
|
|
13
|
+
/**
|
|
14
|
+
* 按实际累计字符数切分(P2):此前按「20 行/段」切分,单条超长行会击穿 4000 字符预算,
|
|
15
|
+
* 中文短行则浪费预算。现在累计字符达到预算即开新段;单条超长行强制单独成段。
|
|
16
|
+
* 同时记录行号区间(startLine/endLine)供 Pass 2 fetchSegments 取原文。
|
|
17
|
+
*/
|
|
18
|
+
export function segmentInputs(inputs) {
|
|
19
|
+
const segments = [];
|
|
20
|
+
for (const input of inputs) {
|
|
21
|
+
const lines = input.content.split('\n');
|
|
22
|
+
const chunks = [];
|
|
23
|
+
let current = [];
|
|
24
|
+
let currentChars = 0;
|
|
25
|
+
let currentStart = 1;
|
|
26
|
+
for (let li = 0; li < lines.length; li++) {
|
|
27
|
+
const line = lines[li];
|
|
28
|
+
// 单条超长行强制按字符切分(P2):否则一条 10000 字符行永远单独成段击穿预算。
|
|
29
|
+
if (current.length === 0 && line.length + 1 > SEGMENT_CHAR_BUDGET) {
|
|
30
|
+
for (let ci = 0; ci < line.length; ci += SEGMENT_CHAR_BUDGET) {
|
|
31
|
+
chunks.push({ lines: [line.slice(ci, ci + SEGMENT_CHAR_BUDGET)], startLine: li + 1 });
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (current.length > 0 && currentChars + line.length + 1 > SEGMENT_CHAR_BUDGET) {
|
|
36
|
+
chunks.push({ lines: current, startLine: currentStart });
|
|
37
|
+
current = [];
|
|
38
|
+
currentChars = 0;
|
|
39
|
+
currentStart = li + 1;
|
|
40
|
+
}
|
|
41
|
+
current.push(line);
|
|
42
|
+
currentChars += line.length + 1;
|
|
43
|
+
if (currentChars >= SEGMENT_CHAR_BUDGET) {
|
|
44
|
+
chunks.push({ lines: current, startLine: currentStart });
|
|
45
|
+
current = [];
|
|
46
|
+
currentChars = 0;
|
|
47
|
+
currentStart = li + 2;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (current.length > 0)
|
|
51
|
+
chunks.push({ lines: current, startLine: currentStart });
|
|
52
|
+
if (chunks.length === 0)
|
|
53
|
+
chunks.push({ lines: [''], startLine: 1 });
|
|
54
|
+
for (const chunk of chunks) {
|
|
55
|
+
const idx = segments.length + 1;
|
|
56
|
+
// Segment id carries the repo prefix (e.g. `seg-repo1-proposal-01`) so multi-repo
|
|
57
|
+
// synthesis can trace which repo a segment belongs to across Pass 0.5 → Pass 2.
|
|
58
|
+
const repoPrefix = input.repo ? `${input.repo}-` : '';
|
|
59
|
+
segments.push({
|
|
60
|
+
id: `seg-${repoPrefix}${input.type}-${String(idx).padStart(2, '0')}`,
|
|
61
|
+
type: input.type,
|
|
62
|
+
label: `${input.repo ? `${input.repo} · ` : ''}${input.type} #${idx}`,
|
|
63
|
+
source: input.source,
|
|
64
|
+
startLine: chunk.startLine,
|
|
65
|
+
endLine: chunk.startLine + chunk.lines.length - 1,
|
|
66
|
+
content: chunk.lines.join('\n'),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return segments;
|
|
71
|
+
}
|
|
72
|
+
// ============= Compression (T4.2) =============
|
|
73
|
+
const COMPRESS_SYSTEM = '你是长文档输入压缩器。将给定原文段压缩为紧凑摘要,保留:关键实体名(接口/表/页面)、决策线索、约束、来源仓标记(如「来源仓:repo1」)。不要复述细节,只要提炼。';
|
|
74
|
+
export async function compressSegment(seg, llm) {
|
|
75
|
+
const input = {
|
|
76
|
+
system: COMPRESS_SYSTEM,
|
|
77
|
+
messages: [{ role: 'user', content: `段 ${seg.id}(${seg.label}):\n${seg.content}` }],
|
|
78
|
+
};
|
|
79
|
+
const summary = (await llm.complete(input)).content.trim();
|
|
80
|
+
return {
|
|
81
|
+
id: seg.id,
|
|
82
|
+
type: seg.type,
|
|
83
|
+
label: seg.label,
|
|
84
|
+
summary,
|
|
85
|
+
entity_candidates: extractEntityCandidates(summary),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Heuristic entity-candidate extraction: backtick tokens + PascalCase/snake tokens. */
|
|
89
|
+
export function extractEntityCandidates(text) {
|
|
90
|
+
const out = new Set();
|
|
91
|
+
for (const m of text.matchAll(/`([^`]+)`/g))
|
|
92
|
+
out.add(m[1].trim());
|
|
93
|
+
for (const m of text.matchAll(/\b([A-Z][A-Za-z0-9]{2,})\b/g))
|
|
94
|
+
out.add(m[1]);
|
|
95
|
+
return [...out];
|
|
96
|
+
}
|
|
97
|
+
// ============= Merge (T4.3) =============
|
|
98
|
+
const MERGE_SYSTEM = '你是长文档输入压缩合并器。将多段摘要合并为一份全局摘要(~1-2k token),保留各段 id、实体候选与关键约束,不丢失任何段。';
|
|
99
|
+
export async function mergeSegmentDigests(digests, llm) {
|
|
100
|
+
const payload = digests.map((d) => `[${d.id}] ${d.summary}`).join('\n\n');
|
|
101
|
+
const input = {
|
|
102
|
+
system: MERGE_SYSTEM,
|
|
103
|
+
messages: [{ role: 'user', content: payload }],
|
|
104
|
+
};
|
|
105
|
+
const summary = (await llm.complete(input)).content.trim();
|
|
106
|
+
return { segments: digests, summary };
|
|
107
|
+
}
|
|
108
|
+
// ============= Segment index (T4.4) =============
|
|
109
|
+
export async function writeSegmentIndex(workRoot, segments) {
|
|
110
|
+
const index = {
|
|
111
|
+
schema: 'specflow.document.segments/v1',
|
|
112
|
+
segments: segments.map((s) => ({
|
|
113
|
+
id: s.id,
|
|
114
|
+
type: s.type,
|
|
115
|
+
label: s.label,
|
|
116
|
+
source: s.source,
|
|
117
|
+
startLine: s.startLine,
|
|
118
|
+
endLine: s.endLine,
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
const path = segmentIndexPath(workRoot);
|
|
122
|
+
await fs.mkdir(join(workRoot, 'segments'), { recursive: true });
|
|
123
|
+
await fs.writeFile(path, JSON.stringify(index, null, 2), 'utf-8');
|
|
124
|
+
return path;
|
|
125
|
+
}
|
|
126
|
+
export async function readSegmentIndex(workRoot) {
|
|
127
|
+
const raw = JSON.parse(await fs.readFile(segmentIndexPath(workRoot), 'utf-8'));
|
|
128
|
+
// 运行时 schema 校验(P2):防止截断/被篡改的索引产生裸 TypeError 或注入任意路径。
|
|
129
|
+
if (!raw || typeof raw !== 'object' || raw.schema !== 'specflow.document.segments/v1' || !Array.isArray(raw.segments)) {
|
|
130
|
+
throw new Error(`Invalid segment index at ${segmentIndexPath(workRoot)}: schema or segments missing`);
|
|
131
|
+
}
|
|
132
|
+
return raw;
|
|
133
|
+
}
|
|
134
|
+
/** Fetch original text for a set of source_segments (bounded, decision 9). */
|
|
135
|
+
export async function fetchSegments(workRoot, segmentIds) {
|
|
136
|
+
const index = await readSegmentIndex(workRoot);
|
|
137
|
+
const wanted = new Set(segmentIds);
|
|
138
|
+
const matched = index.segments.filter((s) => wanted.has(s.id));
|
|
139
|
+
const parts = [];
|
|
140
|
+
for (const s of matched) {
|
|
141
|
+
parts.push(`[${s.id} · ${s.label}] ${s.source} ${s.startLine}-${s.endLine}`);
|
|
142
|
+
// source 来自引擎写入的段索引(由显式输入文件构成)。读取前做基础校验:
|
|
143
|
+
// 拒绝空串与明显越界的相对路径;绝对路径(--code 显式输入)放行。
|
|
144
|
+
if (!s.source || s.source.includes('\0')) {
|
|
145
|
+
parts.push('(原文路径非法,使用段摘要)');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const text = await fs.readFile(s.source, 'utf-8');
|
|
150
|
+
const lines = text.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
|
|
151
|
+
parts.push(lines.join('\n'));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
parts.push('(原文不可读,使用段摘要)');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return parts.join('\n\n');
|
|
158
|
+
}
|
|
159
|
+
// ============= Top-level orchestration (T4.x entry) =============
|
|
160
|
+
const MAX_CONCURRENT_COMPRESS = 4; // 并发压缩上限(P2)
|
|
161
|
+
/** 段级压缩:失败回退「原文摘要」并记录诊断(P2),不中断整个生成。 */
|
|
162
|
+
async function compressSegmentSafe(seg, llm) {
|
|
163
|
+
try {
|
|
164
|
+
return await compressSegment(seg, llm);
|
|
165
|
+
}
|
|
166
|
+
catch (e) {
|
|
167
|
+
// 降级:压缩失败时用原文截断作摘要(保留事实,避免全盘失败)。
|
|
168
|
+
return {
|
|
169
|
+
id: seg.id,
|
|
170
|
+
type: seg.type,
|
|
171
|
+
label: seg.label,
|
|
172
|
+
summary: `(压缩失败,使用原文摘要)${seg.content.slice(0, 800)}`,
|
|
173
|
+
entity_candidates: extractEntityCandidates(seg.content),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export async function compressInputs(workRoot, inputs, llm) {
|
|
178
|
+
const segments = segmentInputs(inputs);
|
|
179
|
+
await writeSegmentIndex(workRoot, segments);
|
|
180
|
+
// 并发压缩(P2):段与段无依赖,串行是主耗时点;带上限并行避免打爆限流。
|
|
181
|
+
const digests = [];
|
|
182
|
+
for (let i = 0; i < segments.length; i += MAX_CONCURRENT_COMPRESS) {
|
|
183
|
+
const batch = segments.slice(i, i + MAX_CONCURRENT_COMPRESS);
|
|
184
|
+
const batchResults = await Promise.all(batch.map((seg) => compressSegmentSafe(seg, llm)));
|
|
185
|
+
digests.push(...batchResults);
|
|
186
|
+
}
|
|
187
|
+
if (digests.length === 0) {
|
|
188
|
+
return { segments: [], summary: '' };
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
return await mergeSegmentDigests(digests, llm);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// 合并失败:回退为拼接摘要(不中断)。
|
|
195
|
+
return {
|
|
196
|
+
segments: digests,
|
|
197
|
+
summary: digests.map((d) => `[${d.id}] ${d.summary}`).join('\n\n'),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — input feature registry + `when` DSL + content-based feature detection.
|
|
3
|
+
*
|
|
4
|
+
* `when` expression grammar (design §3.1, decision 9):
|
|
5
|
+
* term := "input." <feature> | "(" expr ")" | "!" term
|
|
6
|
+
* and-or := term (("&&" | "||") term)*
|
|
7
|
+
* Built-in features: containsApiChange, hasPersistence, uiInScope, hasLogic, hasBackend, includeSignoff.
|
|
8
|
+
* Unknown features surface as validation warnings (T2.3).
|
|
9
|
+
*
|
|
10
|
+
* Feature VALUES are derived by `detectInputFeatures` (deterministic keyword scoring over the
|
|
11
|
+
* input text + file-type hints), NOT hardcoded booleans — so e.g. `uiInScope` can actually
|
|
12
|
+
* become true when the input mentions UI, and `document plan/run` selects chapters such as
|
|
13
|
+
* `ui-design` accordingly.
|
|
14
|
+
*/
|
|
15
|
+
import type { SegmentableInput } from './input-digest.js';
|
|
16
|
+
export declare const INPUT_FEATURES: readonly ["containsApiChange", "hasPersistence", "uiInScope", "hasLogic", "hasBackend", "includeSignoff"];
|
|
17
|
+
export type InputFeatureMap = Record<string, boolean>;
|
|
18
|
+
export declare function isKnownFeature(name: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Detect input features deterministically from the input content (+ file-type hints).
|
|
21
|
+
*
|
|
22
|
+
* - `containsApiChange` / `hasPersistence`: file-type hints (design/specs typically carry API
|
|
23
|
+
* surface / persistence) OR content keywords.
|
|
24
|
+
* - `uiInScope`: content only — the file type cannot reveal UI scope (was previously hardcoded
|
|
25
|
+
* `false`, which made `ui-design` unreachable).
|
|
26
|
+
* - `hasBackend`: content keywords (后端/接口/数据库/服务端…) — distinguishes fullstack from
|
|
27
|
+
* pure-frontend variants (F1 §2.4).
|
|
28
|
+
* - `hasLogic`: content keywords, with a lenient default of true for any non-empty input —
|
|
29
|
+
* a false negative would silently drop core optional chapters (architecture / test-strategy)
|
|
30
|
+
* whose `when: input.hasLogic`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function detectInputFeatures(inputs: SegmentableInput[]): InputFeatureMap;
|
|
33
|
+
/** Tokenize a when expression into a simple AST via recursive descent. */
|
|
34
|
+
export interface WhenExpr {
|
|
35
|
+
op: 'and' | 'or' | 'not' | 'feature';
|
|
36
|
+
feature?: string;
|
|
37
|
+
left?: WhenExpr;
|
|
38
|
+
right?: WhenExpr;
|
|
39
|
+
}
|
|
40
|
+
export declare function parseWhen(expr: string): WhenExpr;
|
|
41
|
+
export declare function evaluateWhen(expr: string, features: InputFeatureMap): boolean;
|
|
42
|
+
/** Collect feature names referenced in a when expression (for T2.3 validation). */
|
|
43
|
+
export declare function featuresInWhen(expr: string): string[];
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — input feature registry + `when` DSL + content-based feature detection.
|
|
3
|
+
*
|
|
4
|
+
* `when` expression grammar (design §3.1, decision 9):
|
|
5
|
+
* term := "input." <feature> | "(" expr ")" | "!" term
|
|
6
|
+
* and-or := term (("&&" | "||") term)*
|
|
7
|
+
* Built-in features: containsApiChange, hasPersistence, uiInScope, hasLogic, hasBackend, includeSignoff.
|
|
8
|
+
* Unknown features surface as validation warnings (T2.3).
|
|
9
|
+
*
|
|
10
|
+
* Feature VALUES are derived by `detectInputFeatures` (deterministic keyword scoring over the
|
|
11
|
+
* input text + file-type hints), NOT hardcoded booleans — so e.g. `uiInScope` can actually
|
|
12
|
+
* become true when the input mentions UI, and `document plan/run` selects chapters such as
|
|
13
|
+
* `ui-design` accordingly.
|
|
14
|
+
*/
|
|
15
|
+
export const INPUT_FEATURES = [
|
|
16
|
+
'containsApiChange',
|
|
17
|
+
'hasPersistence',
|
|
18
|
+
'uiInScope',
|
|
19
|
+
'hasLogic',
|
|
20
|
+
'hasBackend',
|
|
21
|
+
'includeSignoff',
|
|
22
|
+
];
|
|
23
|
+
export function isKnownFeature(name) {
|
|
24
|
+
return INPUT_FEATURES.includes(name);
|
|
25
|
+
}
|
|
26
|
+
// ============= Content-based feature detection (decision 8 · P1-J) =============
|
|
27
|
+
/** Deterministic keyword groups per feature. Short ASCII keywords are matched at word
|
|
28
|
+
* boundaries (`\bapp\b` does not match "approach"); CJK keywords are plain substring. */
|
|
29
|
+
const FEATURE_KEYWORDS = {
|
|
30
|
+
containsApiChange: ['接口', 'api', 'endpoint', 'rest', 'http', 'proto', '请求', '响应', '鉴权', '错误码', '路由'],
|
|
31
|
+
hasPersistence: ['数据库', 'database', '存储', '持久化', '数据模型', '表结构', 'ddl', 'mysql', 'postgres', 'sqlite', 'redis', 'mongodb', '缓存'],
|
|
32
|
+
uiInScope: ['前端', 'ui', '页面', '界面', '交互', '按钮', '表单', '弹窗', '样式', '小程序', 'h5', '移动端', 'dashboard', 'frontend', '可视化'],
|
|
33
|
+
hasLogic: ['逻辑', '算法', '规则', '计算', '流程', '校验', '判断', '状态机', 'algorithm'],
|
|
34
|
+
hasBackend: ['后端', '服务端', '服务器', '接口', 'api', 'rest', '数据库', '微服务', 'backend', 'server', 'daemon', '后台', '网关'],
|
|
35
|
+
};
|
|
36
|
+
function hasAnyKeyword(text, keywords) {
|
|
37
|
+
return keywords.some((kw) => {
|
|
38
|
+
if (/^[A-Za-z0-9_.-]+$/.test(kw)) {
|
|
39
|
+
// Word-boundary match for Latin keywords ("app" ≠ "approach", "ui" ≠ "require").
|
|
40
|
+
return new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(text);
|
|
41
|
+
}
|
|
42
|
+
return text.includes(kw);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Detect input features deterministically from the input content (+ file-type hints).
|
|
47
|
+
*
|
|
48
|
+
* - `containsApiChange` / `hasPersistence`: file-type hints (design/specs typically carry API
|
|
49
|
+
* surface / persistence) OR content keywords.
|
|
50
|
+
* - `uiInScope`: content only — the file type cannot reveal UI scope (was previously hardcoded
|
|
51
|
+
* `false`, which made `ui-design` unreachable).
|
|
52
|
+
* - `hasBackend`: content keywords (后端/接口/数据库/服务端…) — distinguishes fullstack from
|
|
53
|
+
* pure-frontend variants (F1 §2.4).
|
|
54
|
+
* - `hasLogic`: content keywords, with a lenient default of true for any non-empty input —
|
|
55
|
+
* a false negative would silently drop core optional chapters (architecture / test-strategy)
|
|
56
|
+
* whose `when: input.hasLogic`.
|
|
57
|
+
*/
|
|
58
|
+
export function detectInputFeatures(inputs) {
|
|
59
|
+
const text = inputs.map((i) => i.content).join('\n').toLowerCase();
|
|
60
|
+
const types = new Set(inputs.map((i) => i.type));
|
|
61
|
+
return {
|
|
62
|
+
containsApiChange: types.has('design') || types.has('specs') || hasAnyKeyword(text, FEATURE_KEYWORDS.containsApiChange),
|
|
63
|
+
hasPersistence: types.has('specs') || hasAnyKeyword(text, FEATURE_KEYWORDS.hasPersistence),
|
|
64
|
+
uiInScope: hasAnyKeyword(text, FEATURE_KEYWORDS.uiInScope),
|
|
65
|
+
hasBackend: hasAnyKeyword(text, FEATURE_KEYWORDS.hasBackend),
|
|
66
|
+
hasLogic: hasAnyKeyword(text, FEATURE_KEYWORDS.hasLogic) || inputs.length > 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
class WhenParser {
|
|
70
|
+
src;
|
|
71
|
+
pos = 0;
|
|
72
|
+
constructor(src) {
|
|
73
|
+
this.src = src;
|
|
74
|
+
this.src = src.replace(/\s+/g, '');
|
|
75
|
+
}
|
|
76
|
+
parse() {
|
|
77
|
+
const expr = this.parseOr();
|
|
78
|
+
if (this.pos < this.src.length) {
|
|
79
|
+
throw new Error(`WhenParser: unexpected token at ${this.pos}: "${this.src.slice(this.pos)}"`);
|
|
80
|
+
}
|
|
81
|
+
return expr;
|
|
82
|
+
}
|
|
83
|
+
parseOr() {
|
|
84
|
+
let left = this.parseAnd();
|
|
85
|
+
while (this.peek('||')) {
|
|
86
|
+
this.pos += 2;
|
|
87
|
+
left = { op: 'or', left, right: this.parseAnd() };
|
|
88
|
+
}
|
|
89
|
+
return left;
|
|
90
|
+
}
|
|
91
|
+
parseAnd() {
|
|
92
|
+
let left = this.parseNot();
|
|
93
|
+
while (this.peek('&&')) {
|
|
94
|
+
this.pos += 2;
|
|
95
|
+
left = { op: 'and', left, right: this.parseNot() };
|
|
96
|
+
}
|
|
97
|
+
return left;
|
|
98
|
+
}
|
|
99
|
+
parseNot() {
|
|
100
|
+
if (this.peek('!')) {
|
|
101
|
+
this.pos += 1;
|
|
102
|
+
return { op: 'not', right: this.parseNot() };
|
|
103
|
+
}
|
|
104
|
+
if (this.peek('(')) {
|
|
105
|
+
this.pos += 1;
|
|
106
|
+
const inner = this.parseOr();
|
|
107
|
+
if (!this.peek(')'))
|
|
108
|
+
throw new Error('WhenParser: missing ")"');
|
|
109
|
+
this.pos += 1;
|
|
110
|
+
return inner;
|
|
111
|
+
}
|
|
112
|
+
return this.parseFeature();
|
|
113
|
+
}
|
|
114
|
+
parseFeature() {
|
|
115
|
+
const m = this.src.slice(this.pos).match(/^input\.[A-Za-z_][A-Za-z0-9_]*/);
|
|
116
|
+
if (!m)
|
|
117
|
+
throw new Error(`WhenParser: expected input.<feature> at "${this.src.slice(this.pos)}"`);
|
|
118
|
+
this.pos += m[0].length;
|
|
119
|
+
return { op: 'feature', feature: m[0].slice('input.'.length) };
|
|
120
|
+
}
|
|
121
|
+
peek(token) {
|
|
122
|
+
return this.src.startsWith(token, this.pos);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function parseWhen(expr) {
|
|
126
|
+
if (!expr || !expr.trim())
|
|
127
|
+
throw new Error('WhenParser: empty expression');
|
|
128
|
+
return new WhenParser(expr).parse();
|
|
129
|
+
}
|
|
130
|
+
export function evaluateWhen(expr, features) {
|
|
131
|
+
const ast = parseWhen(expr);
|
|
132
|
+
return evalNode(ast, features);
|
|
133
|
+
}
|
|
134
|
+
function evalNode(node, features) {
|
|
135
|
+
switch (node.op) {
|
|
136
|
+
case 'feature':
|
|
137
|
+
return features[node.feature] === true;
|
|
138
|
+
case 'not':
|
|
139
|
+
return !evalNode(node.right, features);
|
|
140
|
+
case 'and':
|
|
141
|
+
return evalNode(node.left, features) && evalNode(node.right, features);
|
|
142
|
+
case 'or':
|
|
143
|
+
return evalNode(node.left, features) || evalNode(node.right, features);
|
|
144
|
+
default:
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Collect feature names referenced in a when expression (for T2.3 validation). */
|
|
149
|
+
export function featuresInWhen(expr) {
|
|
150
|
+
const out = [];
|
|
151
|
+
walk(parseWhen(expr), out);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
function walk(node, acc) {
|
|
155
|
+
if (node.op === 'feature')
|
|
156
|
+
acc.push(node.feature);
|
|
157
|
+
if (node.left)
|
|
158
|
+
walk(node.left, acc);
|
|
159
|
+
if (node.right)
|
|
160
|
+
walk(node.right, acc);
|
|
161
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — lint: reference checks, stub detection, budget checks (decision 6).
|
|
3
|
+
*/
|
|
4
|
+
export interface LintDiagnostic {
|
|
5
|
+
code: string;
|
|
6
|
+
severity: 'error' | 'warning';
|
|
7
|
+
message: string;
|
|
8
|
+
chapter?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LintOptions {
|
|
11
|
+
/** Minimum meaningful content length for narrative (default 20). */
|
|
12
|
+
minNarrativeChars?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function lintNarrative(chapterId: string, narrative: string, options?: LintOptions): LintDiagnostic[];
|
|
15
|
+
export interface BudgetDiagnostic {
|
|
16
|
+
code: string;
|
|
17
|
+
severity: 'error' | 'warning';
|
|
18
|
+
message: string;
|
|
19
|
+
chapter?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Check chapter output against per-chapter budget (decision 6/P6). */
|
|
22
|
+
export declare function checkBudget(chapterId: string, tokens: number, budgetTokens?: number): BudgetDiagnostic[];
|
|
23
|
+
export declare function checkNumericPresence(chapterId: string, narrative: string, gates?: readonly string[]): DiagramLintDiagnostic[];
|
|
24
|
+
export interface DiagramLintDiagnostic {
|
|
25
|
+
code: string;
|
|
26
|
+
severity: 'error' | 'warning';
|
|
27
|
+
message: string;
|
|
28
|
+
chapter?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Check that a chapter's narrative satisfies its diagram gates (README §1.7). */
|
|
31
|
+
export declare function checkDiagramPresence(chapterId: string, narrative: string, gates?: readonly string[]): DiagramLintDiagnostic[];
|
|
32
|
+
export declare function checkGoldenSignals(chapterId: string, narrative: string, gates?: readonly string[]): DiagramLintDiagnostic[];
|
|
33
|
+
export interface RepoSectionDiagnostic {
|
|
34
|
+
code: string;
|
|
35
|
+
severity: 'error' | 'warning';
|
|
36
|
+
message: string;
|
|
37
|
+
chapter?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function checkRepoSectionCoverage(chapterId: string, narrative: string, repos: readonly string[]): RepoSectionDiagnostic[];
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — lint: reference checks, stub detection, budget checks (decision 6).
|
|
3
|
+
*/
|
|
4
|
+
import { findForbiddenMatch, findInconsistencyHints } from './forbidden-patterns.js';
|
|
5
|
+
export function lintNarrative(chapterId, narrative, options = {}) {
|
|
6
|
+
const diags = [];
|
|
7
|
+
const min = options.minNarrativeChars ?? 20;
|
|
8
|
+
const trimmed = narrative.trim();
|
|
9
|
+
if (trimmed.length < min) {
|
|
10
|
+
diags.push({ code: 'narrative_too_short', severity: 'error', chapter: chapterId, message: `Chapter "${chapterId}" narrative too short (< ${min} chars)` });
|
|
11
|
+
}
|
|
12
|
+
const forbidden = findForbiddenMatch(trimmed);
|
|
13
|
+
if (forbidden) {
|
|
14
|
+
diags.push({ code: 'forbidden_pattern', severity: 'error', chapter: chapterId, message: `Chapter "${chapterId}": forbidden stub pattern (${forbidden.source})` });
|
|
15
|
+
}
|
|
16
|
+
for (const hint of findInconsistencyHints(trimmed)) {
|
|
17
|
+
diags.push({ code: 'inconsistency_hint', severity: 'warning', chapter: chapterId, message: `Chapter "${chapterId}": possible inconsistency (${hint.source})` });
|
|
18
|
+
}
|
|
19
|
+
return diags;
|
|
20
|
+
}
|
|
21
|
+
/** Check chapter output against per-chapter budget (decision 6/P6). */
|
|
22
|
+
export function checkBudget(chapterId, tokens, budgetTokens) {
|
|
23
|
+
if (budgetTokens === undefined)
|
|
24
|
+
return [];
|
|
25
|
+
if (tokens > budgetTokens) {
|
|
26
|
+
return [{ code: 'budget_exceeded', severity: 'error', chapter: chapterId, message: `Chapter "${chapterId}" ${tokens} tokens exceeds budget ${budgetTokens}` }];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
// ============= Numeric-presence lint (反AI决策 #2 · 数字门禁确定性化) =============
|
|
31
|
+
//
|
|
32
|
+
// Chapters whose `gates` demand concrete numbers (原始测量值 / 给数字 — e.g. F5 benchmark
|
|
33
|
+
// 与 research) must contain at least one digit in the narrative. Turns the prompt-level
|
|
34
|
+
// "数字门禁" into a validate-able rule for the most number-dependent chapters.
|
|
35
|
+
const DIGIT_RE = /\d/;
|
|
36
|
+
export function checkNumericPresence(chapterId, narrative, gates = []) {
|
|
37
|
+
const joinedGates = gates.join(' ');
|
|
38
|
+
const wantsNumbers = /原始测量值|给数字/.test(joinedGates);
|
|
39
|
+
if (!wantsNumbers)
|
|
40
|
+
return [];
|
|
41
|
+
if (!DIGIT_RE.test(narrative)) {
|
|
42
|
+
return [{
|
|
43
|
+
code: 'number_gate_missing',
|
|
44
|
+
severity: 'error',
|
|
45
|
+
chapter: chapterId,
|
|
46
|
+
message: `Chapter "${chapterId}" gates require concrete numbers (原始测量值/给数字) but narrative contains no digits`,
|
|
47
|
+
}];
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Fresh non-global copies of the diagram regexes. The module-level `/g` regexes are
|
|
53
|
+
* stateful (`.test()` advances `lastIndex`), so calling them for consecutive chapters with
|
|
54
|
+
* diagrams (e.g. frontend-architecture graph then core-flow sequence) could miss matches.
|
|
55
|
+
* `runGates` iterates chapters in order, so a stale `lastIndex` from a previous chapter's
|
|
56
|
+
* mermaid graph would leak into the next chapter's check. Use per-call copies to stay
|
|
57
|
+
* deterministic.
|
|
58
|
+
*/
|
|
59
|
+
function freshMermaidRE() { return /```mermaid\s*[\s\S]*?```/gi; }
|
|
60
|
+
// 允许 `%%` 注释 / `%%{init:{...}}%%` 指令出现在 sequenceDiagram 之前(P2:合法的 mermaid 时序图
|
|
61
|
+
// 常以 init 指令开头,之前会被误报 sequence_diagram_missing)。
|
|
62
|
+
function freshSequenceRE() { return /```mermaid\s*(?:%%[^\n]*\n|\s)*sequenceDiagram[\s\S]*?```/gi; }
|
|
63
|
+
/** Check that a chapter's narrative satisfies its diagram gates (README §1.7). */
|
|
64
|
+
export function checkDiagramPresence(chapterId, narrative, gates = []) {
|
|
65
|
+
const diags = [];
|
|
66
|
+
const joinedGates = gates.join(' ');
|
|
67
|
+
const wantsMermaid = /mermaid/i.test(joinedGates);
|
|
68
|
+
const wantsSequence = /时序图|sequence/i.test(joinedGates);
|
|
69
|
+
if (!wantsMermaid && !wantsSequence)
|
|
70
|
+
return diags; // no diagram gate → no check
|
|
71
|
+
const hasMermaidBlock = freshMermaidRE().test(narrative);
|
|
72
|
+
if (wantsMermaid && !hasMermaidBlock) {
|
|
73
|
+
diags.push({
|
|
74
|
+
code: 'diagram_missing',
|
|
75
|
+
severity: 'error',
|
|
76
|
+
chapter: chapterId,
|
|
77
|
+
message: `Chapter "${chapterId}" gates require a Mermaid diagram but none found (missing \`\`\`mermaid block)`,
|
|
78
|
+
});
|
|
79
|
+
return diags;
|
|
80
|
+
}
|
|
81
|
+
if (wantsSequence && !freshSequenceRE().test(narrative)) {
|
|
82
|
+
diags.push({
|
|
83
|
+
code: 'sequence_diagram_missing',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
chapter: chapterId,
|
|
86
|
+
message: `Chapter "${chapterId}" gates require a sequence diagram (sequenceDiagram) but none found`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// "图必须有文字说明": the narrative must contain text OUTSIDE the mermaid block(s).
|
|
90
|
+
const withoutMermaid = narrative.replace(freshMermaidRE(), '').trim();
|
|
91
|
+
if (hasMermaidBlock && withoutMermaid.length === 0) {
|
|
92
|
+
diags.push({
|
|
93
|
+
code: 'diagram_without_text',
|
|
94
|
+
severity: 'error',
|
|
95
|
+
chapter: chapterId,
|
|
96
|
+
message: `Chapter "${chapterId}" contains only a diagram with no textual explanation (图必须有文字说明)`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return diags;
|
|
100
|
+
}
|
|
101
|
+
// ============= Ops golden-signals presence lint (SRE 四金信号,f1-0to1.md §2.6.3) =============
|
|
102
|
+
//
|
|
103
|
+
// A chapter whose `gates` reference 四金信号 must name all four golden signals in the narrative
|
|
104
|
+
// (延迟/流量/错误/饱和度); a missing one surfaces as a warning (per f1 §2.6.3: 缺一报 warning).
|
|
105
|
+
const GOLDEN_SIGNALS = [
|
|
106
|
+
{ key: '延迟', aliases: ['延迟', 'latency', '耗时', '响应时间', 'p95', 'lcp'] },
|
|
107
|
+
{ key: '流量', aliases: ['流量', 'traffic', 'qps', '请求量', '吞吐', 'rps'] },
|
|
108
|
+
{ key: '错误', aliases: ['错误率', 'errors', 'error rate', '5xx', '4xx'] },
|
|
109
|
+
// 饱和度用强信号(P2):cpu/内存/连接数 作为独立提及常出现于排障段,不代表监控覆盖。
|
|
110
|
+
{ key: '饱和度', aliases: ['饱和度', 'saturation', '资源使用率'] },
|
|
111
|
+
];
|
|
112
|
+
export function checkGoldenSignals(chapterId, narrative, gates = []) {
|
|
113
|
+
const joinedGates = gates.join(' ');
|
|
114
|
+
if (!/四金信号|golden signals/i.test(joinedGates))
|
|
115
|
+
return [];
|
|
116
|
+
const lower = narrative.toLowerCase();
|
|
117
|
+
const missing = GOLDEN_SIGNALS.filter((s) => !s.aliases.some((a) => lower.includes(a)));
|
|
118
|
+
if (missing.length === 0)
|
|
119
|
+
return [];
|
|
120
|
+
return [{
|
|
121
|
+
code: 'golden_signal_missing',
|
|
122
|
+
severity: 'warning',
|
|
123
|
+
chapter: chapterId,
|
|
124
|
+
message: `Chapter "${chapterId}" gates require SRE 四金信号 monitoring but missing: ${missing.map((m) => m.key).join('、')}(监控必须覆盖延迟/流量/错误/饱和度)`,
|
|
125
|
+
}];
|
|
126
|
+
}
|
|
127
|
+
export function checkRepoSectionCoverage(chapterId, narrative, repos) {
|
|
128
|
+
if (repos.length < 2)
|
|
129
|
+
return []; // 非 synthesize 或单仓 → 不校验
|
|
130
|
+
const diags = [];
|
|
131
|
+
for (const repo of repos) {
|
|
132
|
+
// 匹配 `### <repo>` 小节标题(行首,可带后缀如 `### repo1 · 目标`)
|
|
133
|
+
const sectionRe = new RegExp(`^###\\s+${escapeRegExp(repo)}(?:\\s|:|:|·|$)`, 'm');
|
|
134
|
+
if (!sectionRe.test(narrative)) {
|
|
135
|
+
diags.push({
|
|
136
|
+
code: 'repo_section_missing',
|
|
137
|
+
severity: 'error',
|
|
138
|
+
chapter: chapterId,
|
|
139
|
+
message: `Chapter "${chapterId}" 缺少仓 ${repo} 的分仓小节(应含 \`### ${repo}\`,合成文档每章必须按仓分节,防止跨仓内容互相稀释)`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return diags;
|
|
144
|
+
}
|
|
145
|
+
function escapeRegExp(s) {
|
|
146
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
147
|
+
}
|