@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,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — deterministic contract extraction (grounding, engine-level).
|
|
3
|
+
*
|
|
4
|
+
* 从工程数据源**确定性**提取契约实体(接口/表),绕过 LLM 编造:
|
|
5
|
+
* - OpenAPI 3.x (JSON/YAML) → 接口(method/path/operationId/请求响应字段/错误码)
|
|
6
|
+
* - proto3 → 接口(rpc 名 + 服务名 + message 字段)
|
|
7
|
+
* - SQL DDL (CREATE TABLE) → 表(表名/字段/类型/DDL 原文)
|
|
8
|
+
*
|
|
9
|
+
* 提取结果作为「工程契约候选」注入 outline 生成(Pass 1 冻结实体名)与章节填充
|
|
10
|
+
* (Pass 2 按 references 注入真实字段),让「接口字段少一个/表结构差一列」不再发生。
|
|
11
|
+
*
|
|
12
|
+
* 设计约束:
|
|
13
|
+
* - **纯确定性**:不调 LLM,解析失败/文件缺失 → 静默跳过(提取是增强,不阻塞流程)。
|
|
14
|
+
* - **输入源**:显式传入的 `--code` 文件 + 常见路径(openapi/proto/ddl 文件)。
|
|
15
|
+
* - **按需加载**:只解析命中的文件,不递归全仓扫描(token/性能可控)。
|
|
16
|
+
*/
|
|
17
|
+
import type { SegmentableInput } from './input-digest.js';
|
|
18
|
+
export interface ExtractedField {
|
|
19
|
+
name: string;
|
|
20
|
+
type: string;
|
|
21
|
+
required?: boolean;
|
|
22
|
+
desc?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ExtractedInterface {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
method: string;
|
|
28
|
+
path: string;
|
|
29
|
+
request_fields?: ExtractedField[];
|
|
30
|
+
response_fields?: ExtractedField[];
|
|
31
|
+
errors?: Array<{
|
|
32
|
+
condition: string;
|
|
33
|
+
status: string;
|
|
34
|
+
}>;
|
|
35
|
+
source: string;
|
|
36
|
+
}
|
|
37
|
+
export interface ExtractedTable {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
ddl: string;
|
|
41
|
+
fields?: ExtractedField[];
|
|
42
|
+
source: string;
|
|
43
|
+
}
|
|
44
|
+
export interface ExtractedContracts {
|
|
45
|
+
interfaces: ExtractedInterface[];
|
|
46
|
+
tables: ExtractedTable[];
|
|
47
|
+
}
|
|
48
|
+
export declare const EMPTY_CONTRACTS: ExtractedContracts;
|
|
49
|
+
type SourceKind = 'openapi' | 'proto' | 'ddl';
|
|
50
|
+
/** 候选文件:显式输入(--code/--design/--specs)+ 常见路径。只返回存在且可分类的文件。 */
|
|
51
|
+
export declare function discoverContractFiles(inputs: SegmentableInput[], projectRoot?: string): Promise<Array<{
|
|
52
|
+
path: string;
|
|
53
|
+
kind: SourceKind;
|
|
54
|
+
}>>;
|
|
55
|
+
/**
|
|
56
|
+
* 从输入文件 + 工程常见路径确定性提取契约实体。
|
|
57
|
+
* - 解析失败/文件缺失 → 静默跳过(提取是增强,不阻塞流程)。
|
|
58
|
+
* - 返回稳定 id(I1…/T1…),供 outline references 使用。
|
|
59
|
+
*/
|
|
60
|
+
export declare function extractContracts(inputs: SegmentableInput[], projectRoot?: string): Promise<ExtractedContracts>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — deterministic contract extraction (grounding, engine-level).
|
|
3
|
+
*
|
|
4
|
+
* 从工程数据源**确定性**提取契约实体(接口/表),绕过 LLM 编造:
|
|
5
|
+
* - OpenAPI 3.x (JSON/YAML) → 接口(method/path/operationId/请求响应字段/错误码)
|
|
6
|
+
* - proto3 → 接口(rpc 名 + 服务名 + message 字段)
|
|
7
|
+
* - SQL DDL (CREATE TABLE) → 表(表名/字段/类型/DDL 原文)
|
|
8
|
+
*
|
|
9
|
+
* 提取结果作为「工程契约候选」注入 outline 生成(Pass 1 冻结实体名)与章节填充
|
|
10
|
+
* (Pass 2 按 references 注入真实字段),让「接口字段少一个/表结构差一列」不再发生。
|
|
11
|
+
*
|
|
12
|
+
* 设计约束:
|
|
13
|
+
* - **纯确定性**:不调 LLM,解析失败/文件缺失 → 静默跳过(提取是增强,不阻塞流程)。
|
|
14
|
+
* - **输入源**:显式传入的 `--code` 文件 + 常见路径(openapi/proto/ddl 文件)。
|
|
15
|
+
* - **按需加载**:只解析命中的文件,不递归全仓扫描(token/性能可控)。
|
|
16
|
+
*/
|
|
17
|
+
import { promises as fs } from 'node:fs';
|
|
18
|
+
import { join, basename, extname } from 'node:path';
|
|
19
|
+
import yaml from 'js-yaml';
|
|
20
|
+
export const EMPTY_CONTRACTS = { interfaces: [], tables: [] };
|
|
21
|
+
// ============= 文件发现 =============
|
|
22
|
+
const OPENAPI_PATTERNS = [/openapi/i, /swagger/i];
|
|
23
|
+
const PROTO_PATTERNS = [/(^|[\\/])[^\\/]*\.proto$/, /\.proto$/];
|
|
24
|
+
const DDL_PATTERNS = [/ddl/i, /schema/i, /migration/i, /create\.sql/i];
|
|
25
|
+
function classifySource(path) {
|
|
26
|
+
const base = basename(path);
|
|
27
|
+
const ext = extname(base).toLowerCase();
|
|
28
|
+
if (ext === '.json' || ext === '.yaml' || ext === '.yml') {
|
|
29
|
+
if (OPENAPI_PATTERNS.some((re) => re.test(base)))
|
|
30
|
+
return 'openapi';
|
|
31
|
+
}
|
|
32
|
+
if (ext === '.proto')
|
|
33
|
+
return 'proto';
|
|
34
|
+
if (ext === '.sql') {
|
|
35
|
+
if (DDL_PATTERNS.some((re) => re.test(base)))
|
|
36
|
+
return 'ddl';
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
/** 候选文件:显式输入(--code/--design/--specs)+ 常见路径。只返回存在且可分类的文件。 */
|
|
41
|
+
export async function discoverContractFiles(inputs, projectRoot) {
|
|
42
|
+
const candidates = [];
|
|
43
|
+
for (const input of inputs) {
|
|
44
|
+
if (input.type === 'code' || input.type === 'design' || input.type === 'specs') {
|
|
45
|
+
candidates.push(input.source);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (projectRoot) {
|
|
49
|
+
for (const rel of [
|
|
50
|
+
'openapi.yaml', 'openapi.yml', 'openapi.json', 'swagger.json', 'swagger.yaml',
|
|
51
|
+
'api/openapi.yaml', 'api/openapi.json', 'api/swagger.json',
|
|
52
|
+
'schema.sql', 'ddl.sql', 'migrations', 'proto', 'api/proto',
|
|
53
|
+
]) {
|
|
54
|
+
candidates.push(join(projectRoot, rel));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const found = [];
|
|
58
|
+
const seen = new Set();
|
|
59
|
+
for (const candidate of candidates) {
|
|
60
|
+
if (seen.has(candidate))
|
|
61
|
+
continue;
|
|
62
|
+
seen.add(candidate);
|
|
63
|
+
const kind = classifySource(candidate);
|
|
64
|
+
if (!kind)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
const stat = await fs.stat(candidate);
|
|
68
|
+
if (stat.isFile())
|
|
69
|
+
found.push({ path: candidate, kind });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// missing → skip (only explicit code/design/specs files that don't exist fall here)
|
|
73
|
+
if (candidate === inputs.find((i) => i.source === candidate)?.source) {
|
|
74
|
+
// explicit input file missing: keep silent, extraction is best-effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
// ============= OpenAPI 解析 =============
|
|
81
|
+
/** 展开 $ref → 内联对象(仅支持同文件 ref,跨文件 ref 跳过)。 */
|
|
82
|
+
function resolveRef(ref, doc) {
|
|
83
|
+
if (!ref.startsWith('#/'))
|
|
84
|
+
return null;
|
|
85
|
+
const parts = ref.slice(2).split('/').filter(Boolean);
|
|
86
|
+
let cur = doc;
|
|
87
|
+
for (const p of parts) {
|
|
88
|
+
if (cur && typeof cur === 'object' && p in cur) {
|
|
89
|
+
cur = cur[p];
|
|
90
|
+
}
|
|
91
|
+
else
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return cur && typeof cur === 'object' ? cur : null;
|
|
95
|
+
}
|
|
96
|
+
function schemaToFields(schema, doc, depth = 0) {
|
|
97
|
+
if (depth > 3)
|
|
98
|
+
return undefined;
|
|
99
|
+
if (!schema || typeof schema !== 'object')
|
|
100
|
+
return undefined;
|
|
101
|
+
const s = schema;
|
|
102
|
+
if (s.$ref) {
|
|
103
|
+
const resolved = resolveRef(String(s.$ref), doc);
|
|
104
|
+
return resolved ? schemaToFields(resolved, doc, depth + 1) : undefined;
|
|
105
|
+
}
|
|
106
|
+
const props = s.properties;
|
|
107
|
+
if (!props || typeof props !== 'object')
|
|
108
|
+
return undefined;
|
|
109
|
+
const fields = [];
|
|
110
|
+
const requiredList = Array.isArray(s.required) ? s.required : [];
|
|
111
|
+
for (const [name, raw] of Object.entries(props)) {
|
|
112
|
+
const f = (raw ?? {});
|
|
113
|
+
const resolved = f.$ref ? resolveRef(String(f.$ref), doc) : f;
|
|
114
|
+
fields.push({
|
|
115
|
+
name,
|
|
116
|
+
type: String(resolved?.type ?? (resolved?.$ref ? String(resolved.$ref) : 'object')),
|
|
117
|
+
required: requiredList.includes(name),
|
|
118
|
+
desc: typeof resolved?.description === 'string' ? resolved.description : undefined,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return fields;
|
|
122
|
+
}
|
|
123
|
+
function parseOpenApiDoc(raw, source, doc) {
|
|
124
|
+
const out = { interfaces: [], tables: [] };
|
|
125
|
+
const paths = doc.paths;
|
|
126
|
+
if (!paths || typeof paths !== 'object')
|
|
127
|
+
return out;
|
|
128
|
+
const seenIds = new Set();
|
|
129
|
+
let n = 0;
|
|
130
|
+
for (const [path, pathItemRaw] of Object.entries(paths)) {
|
|
131
|
+
const pathItem = (pathItemRaw ?? {});
|
|
132
|
+
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
|
133
|
+
const op = pathItem[method];
|
|
134
|
+
if (!op || typeof op !== 'object')
|
|
135
|
+
continue;
|
|
136
|
+
const opObj = op;
|
|
137
|
+
n += 1;
|
|
138
|
+
const id = `I${n}`;
|
|
139
|
+
const name = typeof opObj.operationId === 'string'
|
|
140
|
+
? opObj.operationId
|
|
141
|
+
: `${method.toUpperCase()} ${path}`;
|
|
142
|
+
if (seenIds.has(name))
|
|
143
|
+
continue;
|
|
144
|
+
seenIds.add(name);
|
|
145
|
+
const ext = {
|
|
146
|
+
id,
|
|
147
|
+
name,
|
|
148
|
+
method: method.toUpperCase(),
|
|
149
|
+
path,
|
|
150
|
+
source,
|
|
151
|
+
};
|
|
152
|
+
// 请求体字段
|
|
153
|
+
const rb = opObj.requestBody;
|
|
154
|
+
if (rb && rb.content && typeof rb.content === 'object') {
|
|
155
|
+
const jsonContent = rb.content['application/json'];
|
|
156
|
+
if (jsonContent && typeof jsonContent === 'object') {
|
|
157
|
+
const schema = jsonContent.schema;
|
|
158
|
+
ext.request_fields = schemaToFields(schema, doc);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 响应字段(200/201 的 application/json)
|
|
162
|
+
const responses = opObj.responses;
|
|
163
|
+
if (responses && typeof responses === 'object') {
|
|
164
|
+
const okResp = responses['200'] ?? responses['201'];
|
|
165
|
+
if (okResp && typeof okResp === 'object') {
|
|
166
|
+
const c = okResp.content;
|
|
167
|
+
const jc = c?.['application/json'];
|
|
168
|
+
if (jc?.schema)
|
|
169
|
+
ext.response_fields = schemaToFields(jc.schema, doc);
|
|
170
|
+
}
|
|
171
|
+
// 错误码:非 2xx 响应
|
|
172
|
+
ext.errors = Object.entries(responses)
|
|
173
|
+
.filter(([code]) => !/^2/.test(code) && /^\d{3}$/.test(code))
|
|
174
|
+
.map(([code]) => ({ condition: `${code} 响应`, status: code }));
|
|
175
|
+
}
|
|
176
|
+
out.interfaces.push(ext);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
// ============= proto3 解析(括号配平,支持嵌套 message / service 内 option) =============
|
|
182
|
+
/** 从 `{` 起始位置用括号配平取回闭合的 body(忽略行注释、块注释与引号字符串)。 */
|
|
183
|
+
function balancedBraceBody(raw, openIdx) {
|
|
184
|
+
let depth = 0;
|
|
185
|
+
let inLineComment = false;
|
|
186
|
+
let inBlockComment = false;
|
|
187
|
+
let inString = null;
|
|
188
|
+
for (let i = openIdx; i < raw.length; i++) {
|
|
189
|
+
const c = raw[i];
|
|
190
|
+
const next = raw[i + 1];
|
|
191
|
+
if (inLineComment) {
|
|
192
|
+
if (c === '\n')
|
|
193
|
+
inLineComment = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (inBlockComment) {
|
|
197
|
+
if (c === '*' && next === '/') {
|
|
198
|
+
inBlockComment = false;
|
|
199
|
+
i++;
|
|
200
|
+
}
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (inString) {
|
|
204
|
+
if (c === '\\') {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (c === inString)
|
|
209
|
+
inString = null;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (c === '/' && next === '/') {
|
|
213
|
+
inLineComment = true;
|
|
214
|
+
i++;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (c === '/' && next === '*') {
|
|
218
|
+
inBlockComment = true;
|
|
219
|
+
i++;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (c === '"' || c === "'") {
|
|
223
|
+
inString = c;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (c === '{')
|
|
227
|
+
depth++;
|
|
228
|
+
else if (c === '}') {
|
|
229
|
+
depth--;
|
|
230
|
+
if (depth === 0)
|
|
231
|
+
return raw.slice(openIdx + 1, i);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return null; // 未闭合
|
|
235
|
+
}
|
|
236
|
+
function parseProto(raw, source) {
|
|
237
|
+
const out = { interfaces: [], tables: [] };
|
|
238
|
+
const messages = new Map();
|
|
239
|
+
// 收集 message 字段(嵌套 message 也递归收集,且外层字段不因嵌套截断)
|
|
240
|
+
const messageNameRe = /message\s+(\w+)\s*\{/g;
|
|
241
|
+
let m;
|
|
242
|
+
while ((m = messageNameRe.exec(raw)) !== null) {
|
|
243
|
+
const name = m[1];
|
|
244
|
+
const openIdx = raw.indexOf('{', m.index);
|
|
245
|
+
const body = balancedBraceBody(raw, openIdx);
|
|
246
|
+
if (body === null)
|
|
247
|
+
continue; // 未闭合 → 跳过
|
|
248
|
+
const fields = [];
|
|
249
|
+
for (const line of body.split('\n')) {
|
|
250
|
+
const fm = line.match(/^\s*(repeated\s+)?([\w.]+)\s+(\w+)\s*=\s*(\d+)/);
|
|
251
|
+
if (fm) {
|
|
252
|
+
fields.push({ name: fm[3], type: fm[2], required: !fm[1] });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
messages.set(name, fields);
|
|
256
|
+
}
|
|
257
|
+
// 收集 rpc(service 体用括号配平,内嵌 option/http 不影响)
|
|
258
|
+
const serviceNameRe = /service\s+(\w+)\s*\{/g;
|
|
259
|
+
let n = 0;
|
|
260
|
+
while ((m = serviceNameRe.exec(raw)) !== null) {
|
|
261
|
+
const service = m[1];
|
|
262
|
+
const openIdx = raw.indexOf('{', m.index);
|
|
263
|
+
const body = balancedBraceBody(raw, openIdx);
|
|
264
|
+
if (body === null)
|
|
265
|
+
continue;
|
|
266
|
+
const rpcRe = /rpc\s+(\w+)\s*\(\s*([\w.]+)\s*\)\s*returns\s*\(\s*([\w.]+)\s*\)/g;
|
|
267
|
+
let r;
|
|
268
|
+
while ((r = rpcRe.exec(body)) !== null) {
|
|
269
|
+
n += 1;
|
|
270
|
+
const [, rpcName, reqMsg, respMsg] = r;
|
|
271
|
+
out.interfaces.push({
|
|
272
|
+
id: `I${n}`,
|
|
273
|
+
name: `${service}.${rpcName}`,
|
|
274
|
+
method: 'POST',
|
|
275
|
+
path: `/${service}/${rpcName}`,
|
|
276
|
+
request_fields: messages.get(reqMsg) ?? [],
|
|
277
|
+
response_fields: messages.get(respMsg) ?? [],
|
|
278
|
+
source,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// 无 service 时,message 也能作为表候选(简单映射)
|
|
283
|
+
if (n === 0) {
|
|
284
|
+
let t = 0;
|
|
285
|
+
for (const [name, fields] of messages) {
|
|
286
|
+
t += 1;
|
|
287
|
+
out.tables.push({
|
|
288
|
+
id: `T${t}`,
|
|
289
|
+
name,
|
|
290
|
+
ddl: `-- proto message ${name}(无 SQL DDL,字段见下方)`,
|
|
291
|
+
fields,
|
|
292
|
+
source,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
// ============= SQL DDL 解析(括号配平,支持 DEFAULT/COMMENT 子句) =============
|
|
299
|
+
function parseDdl(raw, source) {
|
|
300
|
+
const out = { interfaces: [], tables: [] };
|
|
301
|
+
const tableNameRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"]?([\w.]+)[`"]?\s*\(/gi;
|
|
302
|
+
let m;
|
|
303
|
+
let n = 0;
|
|
304
|
+
while ((m = tableNameRe.exec(raw)) !== null) {
|
|
305
|
+
const name = m[1];
|
|
306
|
+
const openIdx = raw.indexOf('(', m.index);
|
|
307
|
+
const body = balancedParenBody(raw, openIdx);
|
|
308
|
+
if (body === null)
|
|
309
|
+
continue; // 未闭合 → 跳过
|
|
310
|
+
n += 1;
|
|
311
|
+
const fields = [];
|
|
312
|
+
for (const line of body.split('\n')) {
|
|
313
|
+
const fm = line.match(/^\s*[`"]?(\w+)[`"]?\s+([\w()]+)\s*(?:NOT\s+NULL)?/i);
|
|
314
|
+
if (fm && !/^(PRIMARY|FOREIGN|UNIQUE|KEY|CONSTRAINT|INDEX)/i.test(fm[1])) {
|
|
315
|
+
fields.push({ name: fm[1], type: fm[2], required: /NOT\s+NULL/i.test(line) });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// 取完整 CREATE TABLE ... 语句(到顶层右括号后的第一个 ; 或行尾)
|
|
319
|
+
const closeIdx = findParenClose(raw, openIdx);
|
|
320
|
+
const stmtEnd = closeIdx >= 0 ? raw.indexOf(';', closeIdx) : -1;
|
|
321
|
+
const stmt = raw.slice(m.index, stmtEnd >= 0 ? stmtEnd + 1 : closeIdx >= 0 ? closeIdx + 1 : undefined).trim();
|
|
322
|
+
out.tables.push({
|
|
323
|
+
id: `T${n}`,
|
|
324
|
+
name,
|
|
325
|
+
ddl: stmt || `CREATE TABLE ${name} (...);`,
|
|
326
|
+
fields,
|
|
327
|
+
source,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
/** 从 `(` 起始位置用括号配平取回 body(忽略引号字符串内的括号)。 */
|
|
333
|
+
function balancedParenBody(raw, openIdx) {
|
|
334
|
+
let depth = 0;
|
|
335
|
+
let inString = null;
|
|
336
|
+
for (let i = openIdx; i < raw.length; i++) {
|
|
337
|
+
const c = raw[i];
|
|
338
|
+
if (inString) {
|
|
339
|
+
if (c === '\\') {
|
|
340
|
+
i++;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (c === inString)
|
|
344
|
+
inString = null;
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (c === "'" || c === '"') {
|
|
348
|
+
inString = c;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (c === '(')
|
|
352
|
+
depth++;
|
|
353
|
+
else if (c === ')') {
|
|
354
|
+
depth--;
|
|
355
|
+
if (depth === 0)
|
|
356
|
+
return raw.slice(openIdx + 1, i);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
function findParenClose(raw, openIdx) {
|
|
362
|
+
let depth = 0;
|
|
363
|
+
let inString = null;
|
|
364
|
+
for (let i = openIdx; i < raw.length; i++) {
|
|
365
|
+
const c = raw[i];
|
|
366
|
+
if (inString) {
|
|
367
|
+
if (c === '\\') {
|
|
368
|
+
i++;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (c === inString)
|
|
372
|
+
inString = null;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (c === "'" || c === '"') {
|
|
376
|
+
inString = c;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
if (c === '(')
|
|
380
|
+
depth++;
|
|
381
|
+
else if (c === ')') {
|
|
382
|
+
depth--;
|
|
383
|
+
if (depth === 0)
|
|
384
|
+
return i;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return -1;
|
|
388
|
+
}
|
|
389
|
+
// ============= 主入口 =============
|
|
390
|
+
/**
|
|
391
|
+
* 从输入文件 + 工程常见路径确定性提取契约实体。
|
|
392
|
+
* - 解析失败/文件缺失 → 静默跳过(提取是增强,不阻塞流程)。
|
|
393
|
+
* - 返回稳定 id(I1…/T1…),供 outline references 使用。
|
|
394
|
+
*/
|
|
395
|
+
export async function extractContracts(inputs, projectRoot) {
|
|
396
|
+
const files = await discoverContractFiles(inputs, projectRoot);
|
|
397
|
+
const out = { interfaces: [], tables: [] };
|
|
398
|
+
for (const { path, kind } of files) {
|
|
399
|
+
try {
|
|
400
|
+
const raw = await fs.readFile(path, 'utf-8');
|
|
401
|
+
if (kind === 'openapi') {
|
|
402
|
+
const doc = (await parseYamlOrJson(raw));
|
|
403
|
+
const parsed = parseOpenApiDoc(raw, path, doc);
|
|
404
|
+
out.interfaces.push(...parsed.interfaces);
|
|
405
|
+
out.tables.push(...parsed.tables);
|
|
406
|
+
}
|
|
407
|
+
else if (kind === 'proto') {
|
|
408
|
+
const parsed = parseProto(raw, path);
|
|
409
|
+
out.interfaces.push(...parsed.interfaces);
|
|
410
|
+
out.tables.push(...parsed.tables);
|
|
411
|
+
}
|
|
412
|
+
else if (kind === 'ddl') {
|
|
413
|
+
const parsed = parseDdl(raw, path);
|
|
414
|
+
out.tables.push(...parsed.tables);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
// best-effort: skip unparseable file
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// 稳定全局编号(避免不同文件各自 I1 冲突)
|
|
422
|
+
const merged = { interfaces: [], tables: [] };
|
|
423
|
+
let i = 0;
|
|
424
|
+
for (const it of out.interfaces)
|
|
425
|
+
merged.interfaces.push({ ...it, id: `I${++i}` });
|
|
426
|
+
let t = 0;
|
|
427
|
+
for (const tb of out.tables)
|
|
428
|
+
merged.tables.push({ ...tb, id: `T${++t}` });
|
|
429
|
+
return merged;
|
|
430
|
+
}
|
|
431
|
+
async function parseYamlOrJson(raw) {
|
|
432
|
+
const trimmed = raw.trim();
|
|
433
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
434
|
+
return JSON.parse(raw);
|
|
435
|
+
}
|
|
436
|
+
return yaml.load(raw);
|
|
437
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — anti-lazy patterns (carried over from ACP, reworked).
|
|
3
|
+
* Used by lint.ts to reject stub / lazy content in narratives and digests.
|
|
4
|
+
*/
|
|
2
5
|
export declare const FORBIDDEN_STUB_PATTERNS: readonly RegExp[];
|
|
3
|
-
/** Semantic inconsistency hints (warnings unless strict mode) — long-document generic. */
|
|
4
6
|
export declare const INCONSISTENCY_HINT_PATTERNS: readonly RegExp[];
|
|
5
7
|
export declare function findForbiddenMatch(content: string): RegExp | null;
|
|
6
8
|
export declare function findInconsistencyHints(content: string): readonly RegExp[];
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — anti-lazy patterns (carried over from ACP, reworked).
|
|
3
|
+
* Used by lint.ts to reject stub / lazy content in narratives and digests.
|
|
4
|
+
*/
|
|
5
|
+
export const FORBIDDEN_STUB_PATTERNS = [
|
|
6
|
+
/\bTODO\b/i,
|
|
7
|
+
/待补充/,
|
|
8
|
+
/此处省略/,
|
|
9
|
+
// § 豁免限定在同段落(≤120 字符)内,避免跨行误报(P2)
|
|
10
|
+
/详见\s*(?:上文|下文|附件)(?![\s\S]{0,120}§)/,
|
|
11
|
+
// 「略」必须是独立占位短语,避免「策略/战略/忽略/省略」结尾误报(P2)
|
|
12
|
+
/(?:从略|暂略|略去|其余略|此处略)\s*[。.]?$/,
|
|
13
|
+
/\bTBD\b\s*[。.]?$/,
|
|
14
|
+
/(?:^|\n)\s*(?:同上|同前)\s*[。.]?\s*(?:\n|$)/,
|
|
15
|
+
/实现时(?:命名|对齐)/,
|
|
16
|
+
/(?:RPC|rpc).*暂定/,
|
|
17
|
+
/暂定\s*[`']?\w+[`']?/,
|
|
18
|
+
];
|
|
19
|
+
export const INCONSISTENCY_HINT_PATTERNS = [
|
|
20
|
+
// 去掉 \b(ASCII 语义在中文相邻处永不成立,P2);靠尾部标点约束避免误伤「等级/等待/等于」
|
|
21
|
+
/等\s*[。.)]/,
|
|
22
|
+
/(?:status|状态|enum|枚举)[^.\n]{0,30}(?:等|etc)/i,
|
|
23
|
+
/(?:^|[\s,。;])同前(?!缀)/,
|
|
24
|
+
/(?:^|[\s,。;])同上(?!所述)/,
|
|
25
|
+
/待定|未定/,
|
|
26
|
+
];
|
|
27
|
+
export function findForbiddenMatch(content) {
|
|
28
|
+
for (const p of FORBIDDEN_STUB_PATTERNS) {
|
|
29
|
+
if (p.test(content))
|
|
30
|
+
return p;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
export function findInconsistencyHints(content) {
|
|
35
|
+
return INCONSISTENCY_HINT_PATTERNS.filter((p) => p.test(content));
|
|
36
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — gate orchestration (T10.4).
|
|
3
|
+
* Runs lint → structured coverage → review check → budget → zod, collecting diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
import { Outline, Entities, ReviewResult, ChapterComponent } from './schemas.js';
|
|
6
|
+
export interface GateDiagnostic {
|
|
7
|
+
gate: string;
|
|
8
|
+
code: string;
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
message: string;
|
|
11
|
+
chapter?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface GatesInput {
|
|
14
|
+
outline: Outline;
|
|
15
|
+
chapterNarratives: Map<string, string>;
|
|
16
|
+
chapterEntities: Map<string, Entities>;
|
|
17
|
+
chapterTokens: Map<string, number>;
|
|
18
|
+
chapterBudget: Map<string, number | undefined>;
|
|
19
|
+
entities: Entities;
|
|
20
|
+
review?: ReviewResult;
|
|
21
|
+
workRoot: string;
|
|
22
|
+
/** Chapter component library (for per-chapter gates like 架构图/时序图). */
|
|
23
|
+
library?: Map<string, ChapterComponent>;
|
|
24
|
+
/** Multi-repo synthesize repo list (from repos.json). When ≥2, every required chapter must
|
|
25
|
+
* contain per-repo sections (`### <repo>`) — enforced in runGates for the library pipeline,
|
|
26
|
+
* matching validateWork (Agent mode). */
|
|
27
|
+
repos?: readonly string[];
|
|
28
|
+
/** 审查篡改校验用的当前哈希快照(P2):persist=false 时磁盘无章节,由调用方传内存哈希;
|
|
29
|
+
* 缺省时 checkReviewResult 回退读磁盘。 */
|
|
30
|
+
currentHashes?: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
export interface GatesResult {
|
|
36
|
+
ok: boolean;
|
|
37
|
+
diagnostics: GateDiagnostic[];
|
|
38
|
+
}
|
|
39
|
+
export declare function runGates(input: GatesInput): Promise<GatesResult>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document Engine — gate orchestration (T10.4).
|
|
3
|
+
* Runs lint → structured coverage → review check → budget → zod, collecting diagnostics.
|
|
4
|
+
*/
|
|
5
|
+
import { lintNarrative, checkBudget, checkDiagramPresence, checkNumericPresence, checkGoldenSignals, checkRepoSectionCoverage } from './lint.js';
|
|
6
|
+
import { checkStructuredCoverage } from './coverage.js';
|
|
7
|
+
import { checkReviewResult } from './review.js';
|
|
8
|
+
export async function runGates(input) {
|
|
9
|
+
const diags = [];
|
|
10
|
+
// lint: narrative stub/short + budget per chapter
|
|
11
|
+
// 遍历 outline 全部章节(P2):只遍历 chapterNarratives 会让缺失的必写章节
|
|
12
|
+
// 静默跳过全部门禁——现在缺失章节显式报 chapter_missing。
|
|
13
|
+
for (const ch of input.outline.chapters) {
|
|
14
|
+
const chId = ch.id;
|
|
15
|
+
const narrative = input.chapterNarratives.get(chId);
|
|
16
|
+
if (narrative === undefined || narrative.trim().length === 0) {
|
|
17
|
+
diags.push({ gate: 'lint', code: 'chapter_missing', severity: 'error', message: `Chapter "${chId}" has no narrative content`, chapter: chId });
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
for (const d of lintNarrative(chId, narrative)) {
|
|
21
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
22
|
+
}
|
|
23
|
+
// diagram-presence gates (README §1.7): Mermaid/时序图 must be present + 有文字说明
|
|
24
|
+
const gates = input.library?.get(chId)?.gates;
|
|
25
|
+
if (gates) {
|
|
26
|
+
for (const d of checkDiagramPresence(chId, narrative, gates)) {
|
|
27
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
28
|
+
}
|
|
29
|
+
// numeric-presence gates (反AI决策 #2): benchmark/research 必须含数字
|
|
30
|
+
for (const d of checkNumericPresence(chId, narrative, gates)) {
|
|
31
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
32
|
+
}
|
|
33
|
+
// ops golden-signals gates (SRE 四金信号,f1 §2.6.3):缺一报 warning
|
|
34
|
+
for (const d of checkGoldenSignals(chId, narrative, gates)) {
|
|
35
|
+
diags.push({ gate: 'lint', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// 预算检查:token 记录缺失时不再当作 0(P2:欠计会静默绕过 budget_exceeded),
|
|
39
|
+
// 显式报数据缺口;仅在有 token 记录时执行预算门禁。
|
|
40
|
+
const tokens = input.chapterTokens.get(chId);
|
|
41
|
+
const budget = input.chapterBudget.get(chId);
|
|
42
|
+
if (tokens === undefined) {
|
|
43
|
+
diags.push({ gate: 'budget', code: 'chapter_tokens_missing', severity: 'warning', message: `Chapter "${chId}" has no token estimate; budget check skipped`, chapter: chId });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
for (const b of checkBudget(chId, tokens, budget)) {
|
|
47
|
+
diags.push({ gate: 'budget', code: b.code, severity: b.severity, message: b.message, chapter: chId });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
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.
|
|
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]));
|
|
55
|
+
for (const [chId, narrative] of input.chapterNarratives) {
|
|
56
|
+
if (!requiredById.has(chId))
|
|
57
|
+
continue;
|
|
58
|
+
for (const d of checkRepoSectionCoverage(chId, narrative, input.repos)) {
|
|
59
|
+
diags.push({ gate: 'synthesize', code: d.code, severity: d.severity, message: d.message, chapter: chId });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// structured coverage
|
|
64
|
+
for (const d of checkStructuredCoverage(input.outline, input.chapterEntities)) {
|
|
65
|
+
diags.push({ gate: 'coverage', code: d.code, severity: d.severity, message: d.message, chapter: d.chapter });
|
|
66
|
+
}
|
|
67
|
+
// review check (semantic channel + tamper + verdict)
|
|
68
|
+
if (input.review) {
|
|
69
|
+
const r = await checkReviewResult(input.workRoot, input.review, input.outline, input.currentHashes);
|
|
70
|
+
for (const d of r.diagnostics) {
|
|
71
|
+
diags.push({ gate: 'review', code: d.code, severity: d.severity, message: d.message });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { ok: diags.every((d) => d.severity !== 'error'), diagnostics: diags };
|
|
75
|
+
}
|