@gordon.gan/specflow 1.4.6-beta → 1.7.0-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 +2 -2
- package/dist/cli/commands/approval-assemble.d.ts +48 -5
- package/dist/cli/commands/approval-assemble.js +347 -34
- package/dist/core/approval/assemble.js +64 -17
- package/dist/core/approval/bundle.d.ts +9 -0
- package/dist/core/approval/bundle.js +172 -0
- package/dist/core/approval/forbidden-patterns.d.ts +6 -0
- package/dist/core/approval/forbidden-patterns.js +37 -0
- package/dist/core/approval/index-schema.d.ts +432 -0
- package/dist/core/approval/index-schema.js +103 -0
- package/dist/core/approval/index.d.ts +10 -2
- package/dist/core/approval/index.js +7 -1
- package/dist/core/approval/lint.d.ts +10 -0
- package/dist/core/approval/lint.js +302 -0
- package/dist/core/approval/paths.d.ts +5 -0
- package/dist/core/approval/paths.js +15 -0
- package/dist/core/approval/pipeline.d.ts +28 -0
- package/dist/core/approval/pipeline.js +146 -0
- package/dist/core/approval/playbook-schema.d.ts +182 -0
- package/dist/core/approval/playbook-schema.js +51 -0
- package/dist/core/approval/render.d.ts +20 -0
- package/dist/core/approval/render.js +210 -0
- package/dist/core/approval/review-pack.d.ts +26 -0
- package/dist/core/approval/review-pack.js +205 -0
- package/dist/core/approval/types.d.ts +131 -0
- package/dist/integrations/shared/capability-evidence.js +2 -0
- package/dist/integrations/shared/parity-manifest.js +2 -0
- package/package.json +2 -1
- package/prompts/approval/acp-pipeline.md +104 -0
- package/prompts/approval/ai-review.md +145 -0
- package/prompts/approval/api-guidance.md +179 -0
- package/prompts/approval/generate.md +164 -13
- package/prompts/approval/multi-repo-guidance.md +238 -0
- package/prompts/approval/project-conventions-guidance.md +1 -1
- package/prompts/approval/runtime-guidance.md +64 -0
- package/prompts/approval/segmented-generation.md +23 -11
- package/skills/GUIDANCE_PACKS.md +2 -2
- package/skills/specflow-approval/SKILL.md +80 -18
- package/templates/approval-index.yaml +41 -0
- package/templates/approval-part.md +1 -1
- package/templates/approval-playbook.yaml +28 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { parseApprovalPlaybook } from './playbook-schema.js';
|
|
6
|
+
import { readApprovalIndex } from './assemble.js';
|
|
7
|
+
import { getApprovalPartPath, getApprovalWorkspaceDir, getChangeDirectory, } from './paths.js';
|
|
8
|
+
function sha256(content) {
|
|
9
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
10
|
+
}
|
|
11
|
+
function resolveSourceChangeDir(workspaceRoot, source) {
|
|
12
|
+
const planningRoot = source.root ?? workspaceRoot;
|
|
13
|
+
return getChangeDirectory(planningRoot, source.change);
|
|
14
|
+
}
|
|
15
|
+
async function loadSourceParts(changeDir, includeParts) {
|
|
16
|
+
const index = await readApprovalIndex(changeDir);
|
|
17
|
+
const partIds = includeParts === 'all'
|
|
18
|
+
? [...index.parts_order]
|
|
19
|
+
: includeParts.filter((id) => index.parts_order.includes(id));
|
|
20
|
+
const results = [];
|
|
21
|
+
for (const partId of partIds) {
|
|
22
|
+
try {
|
|
23
|
+
const content = await fs.readFile(getApprovalPartPath(changeDir, partId), 'utf-8');
|
|
24
|
+
results.push({ partId, content });
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// skip missing
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
}
|
|
32
|
+
export async function bundleApprovalDocument(options) {
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
const { workspaceRoot, changeDir, playbookPath, write = true } = options;
|
|
35
|
+
let playbook;
|
|
36
|
+
let indexOutputOverride;
|
|
37
|
+
try {
|
|
38
|
+
const raw = yaml.load(await fs.readFile(playbookPath, 'utf-8'));
|
|
39
|
+
playbook = parseApprovalPlaybook(raw);
|
|
40
|
+
try {
|
|
41
|
+
const index = await readApprovalIndex(changeDir);
|
|
42
|
+
indexOutputOverride = index.multi_repo?.bundle?.output;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// index optional for bundle-only dry runs
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
diagnostics: [
|
|
53
|
+
{ code: 'invalid_playbook', severity: 'error', message: `Invalid playbook: ${message}` },
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const body = [];
|
|
58
|
+
body.push(`# ${playbook.title}`);
|
|
59
|
+
body.push('');
|
|
60
|
+
body.push(`> **合订视图** (read-only bundle) | change: \`${playbook.change}\``);
|
|
61
|
+
body.push(`> 生成方式: \`specflow approval bundle\` — 确定性拼接,禁止 LLM Reduce`);
|
|
62
|
+
body.push(`> 主仓: \`${playbook.primary_repo}\``);
|
|
63
|
+
body.push('');
|
|
64
|
+
body.push('---');
|
|
65
|
+
body.push('');
|
|
66
|
+
if (playbook.overlay && playbook.overlay.length > 0) {
|
|
67
|
+
const approvalDir = getApprovalWorkspaceDir(changeDir);
|
|
68
|
+
for (const overlayRel of playbook.overlay) {
|
|
69
|
+
const overlayPath = join(approvalDir, overlayRel);
|
|
70
|
+
try {
|
|
71
|
+
const content = await fs.readFile(overlayPath, 'utf-8');
|
|
72
|
+
if (playbook.assemble.inject_headers !== false) {
|
|
73
|
+
body.push(`## Overlay · ${overlayRel}`);
|
|
74
|
+
body.push('');
|
|
75
|
+
}
|
|
76
|
+
body.push(content.trim());
|
|
77
|
+
body.push('');
|
|
78
|
+
body.push('---');
|
|
79
|
+
body.push('');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
diagnostics.push({
|
|
83
|
+
code: 'overlay_missing',
|
|
84
|
+
severity: 'error',
|
|
85
|
+
message: `Overlay file not found: ${overlayPath}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const source of playbook.sources) {
|
|
91
|
+
const sourceChangeDir = resolveSourceChangeDir(workspaceRoot, source);
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(sourceChangeDir);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
code: 'source_change_missing',
|
|
98
|
+
severity: 'error',
|
|
99
|
+
message: `Source change not found: ${sourceChangeDir} (repo=${source.repo})`,
|
|
100
|
+
});
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const includeParts = source.include_parts ?? 'all';
|
|
104
|
+
const parts = await loadSourceParts(sourceChangeDir, includeParts);
|
|
105
|
+
if (parts.length === 0) {
|
|
106
|
+
diagnostics.push({
|
|
107
|
+
code: 'source_parts_empty',
|
|
108
|
+
severity: 'warning',
|
|
109
|
+
message: `No parts loaded for source ${source.repo}/${source.change}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (playbook.assemble.inject_headers !== false) {
|
|
113
|
+
body.push(`## 来源 · ${source.repo} (\`${source.change}\`)`);
|
|
114
|
+
body.push('');
|
|
115
|
+
}
|
|
116
|
+
for (const { partId, content } of parts) {
|
|
117
|
+
body.push(`<!-- bundle-source: ${source.repo}/${source.change}/${partId} -->`);
|
|
118
|
+
body.push(content.trim());
|
|
119
|
+
body.push('');
|
|
120
|
+
}
|
|
121
|
+
body.push('---');
|
|
122
|
+
body.push('');
|
|
123
|
+
}
|
|
124
|
+
if (diagnostics.some((d) => d.severity === 'error')) {
|
|
125
|
+
return { ok: false, diagnostics };
|
|
126
|
+
}
|
|
127
|
+
let markdown = body.join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
|
|
128
|
+
if (playbook.assemble.dedupe_appendix !== false) {
|
|
129
|
+
const appendixMatches = markdown.match(/^##\s+附录\s*A/mg);
|
|
130
|
+
if (appendixMatches && appendixMatches.length > 1) {
|
|
131
|
+
diagnostics.push({
|
|
132
|
+
code: 'duplicate_appendix',
|
|
133
|
+
severity: 'warning',
|
|
134
|
+
message: `Bundle output contains ${appendixMatches.length} "## 附录 A" headings`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (playbook.assemble.readonly) {
|
|
139
|
+
markdown =
|
|
140
|
+
`> **READ-ONLY VIEW** — apply 请使用各仓 \`approval.md\` 真源,勿直接消费本合订文件。\n\n` +
|
|
141
|
+
markdown;
|
|
142
|
+
}
|
|
143
|
+
const outputRel = indexOutputOverride ?? playbook.assemble.output;
|
|
144
|
+
const outputPath = join(changeDir, outputRel);
|
|
145
|
+
if (!write) {
|
|
146
|
+
return { ok: true, markdown, outputPath, diagnostics };
|
|
147
|
+
}
|
|
148
|
+
await fs.mkdir(dirname(outputPath), { recursive: true });
|
|
149
|
+
await fs.writeFile(outputPath, markdown, 'utf-8');
|
|
150
|
+
const bundleManifestPath = join(getApprovalWorkspaceDir(changeDir), 'bundle-manifest.json');
|
|
151
|
+
await fs.writeFile(bundleManifestPath, JSON.stringify({
|
|
152
|
+
schema: 'specflow.approval.bundle-manifest/v1',
|
|
153
|
+
change: playbook.change,
|
|
154
|
+
playbook: playbookPath,
|
|
155
|
+
output: outputRel,
|
|
156
|
+
sha256: sha256(markdown),
|
|
157
|
+
assembled_at: new Date().toISOString(),
|
|
158
|
+
sources: playbook.sources.map((s) => ({ repo: s.repo, change: s.change })),
|
|
159
|
+
}, null, 2), 'utf-8');
|
|
160
|
+
return { ok: true, markdown, outputPath, diagnostics };
|
|
161
|
+
}
|
|
162
|
+
export async function resolvePlaybookPath(changeDir, explicitPath) {
|
|
163
|
+
if (explicitPath) {
|
|
164
|
+
return explicitPath;
|
|
165
|
+
}
|
|
166
|
+
const index = await readApprovalIndex(changeDir);
|
|
167
|
+
const fromIndex = index.multi_repo?.bundle?.playbook;
|
|
168
|
+
if (fromIndex) {
|
|
169
|
+
return join(getApprovalWorkspaceDir(changeDir), fromIndex);
|
|
170
|
+
}
|
|
171
|
+
return join(getApprovalWorkspaceDir(changeDir), 'approval-playbook.yaml');
|
|
172
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared stub / inconsistency patterns for assemble lazy-check and approval lint. */
|
|
2
|
+
export declare const FORBIDDEN_STUB_PATTERNS: readonly RegExp[];
|
|
3
|
+
/** Semantic inconsistency hints (warnings unless strict mode) — long-document generic. */
|
|
4
|
+
export declare const INCONSISTENCY_HINT_PATTERNS: readonly RegExp[];
|
|
5
|
+
export declare function findForbiddenMatch(content: string): RegExp | null;
|
|
6
|
+
export declare function findInconsistencyHints(content: string): readonly RegExp[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Shared stub / inconsistency patterns for assemble lazy-check and approval lint. */
|
|
2
|
+
export const FORBIDDEN_STUB_PATTERNS = [
|
|
3
|
+
/\bTODO\b/i,
|
|
4
|
+
/待补充/,
|
|
5
|
+
/待 refine 澄清(?!.*\[)/,
|
|
6
|
+
/此处省略/,
|
|
7
|
+
/详见\s*(?:上文|下文|附件)(?!.*§)/,
|
|
8
|
+
/略\s*[。.]?$/,
|
|
9
|
+
/TBD\s*[。.]?$/,
|
|
10
|
+
/(?:^|\n)\s*\.{3}\s*(?:\n|$)/,
|
|
11
|
+
/(?:^|\n)\s*(?:同上|同前)\s*[。.]?\s*(?:\n|$)/,
|
|
12
|
+
/实现时(?:命名|对齐)/,
|
|
13
|
+
/(?:RPC|rpc).*暂定/,
|
|
14
|
+
/暂定\s*[`']?\w+[`']?/,
|
|
15
|
+
/\b如\s+[A-Z]\w*(?:Result|Request|Response)/,
|
|
16
|
+
/内部(?:经|调用)\s*I\d+[^.\n]{0,40}(?:一行|代替|省略)/,
|
|
17
|
+
];
|
|
18
|
+
/** Semantic inconsistency hints (warnings unless strict mode) — long-document generic. */
|
|
19
|
+
export const INCONSISTENCY_HINT_PATTERNS = [
|
|
20
|
+
/约\s*\d+\s*次/,
|
|
21
|
+
/\b(?:等|etc\.?)\s*[。.)]/,
|
|
22
|
+
/(?:status|状态|enum|枚举)[^.\n]{0,30}(?:等|etc)/i,
|
|
23
|
+
/\b(?:TBD|待定|未定)\b/i,
|
|
24
|
+
/(?:同上|同前|见上)(?!.*§)/,
|
|
25
|
+
/\d+\s*[或\\/]\s*\d+\s*(?:均可|都行)/,
|
|
26
|
+
];
|
|
27
|
+
export function findForbiddenMatch(content) {
|
|
28
|
+
for (const pattern of FORBIDDEN_STUB_PATTERNS) {
|
|
29
|
+
if (pattern.test(content)) {
|
|
30
|
+
return pattern;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function findInconsistencyHints(content) {
|
|
36
|
+
return INCONSISTENCY_HINT_PATTERNS.filter((p) => p.test(content));
|
|
37
|
+
}
|