@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.
Files changed (41) hide show
  1. package/README.md +2 -2
  2. package/dist/cli/commands/approval-assemble.d.ts +48 -5
  3. package/dist/cli/commands/approval-assemble.js +347 -34
  4. package/dist/core/approval/assemble.js +64 -17
  5. package/dist/core/approval/bundle.d.ts +9 -0
  6. package/dist/core/approval/bundle.js +172 -0
  7. package/dist/core/approval/forbidden-patterns.d.ts +6 -0
  8. package/dist/core/approval/forbidden-patterns.js +37 -0
  9. package/dist/core/approval/index-schema.d.ts +432 -0
  10. package/dist/core/approval/index-schema.js +103 -0
  11. package/dist/core/approval/index.d.ts +10 -2
  12. package/dist/core/approval/index.js +7 -1
  13. package/dist/core/approval/lint.d.ts +10 -0
  14. package/dist/core/approval/lint.js +302 -0
  15. package/dist/core/approval/paths.d.ts +5 -0
  16. package/dist/core/approval/paths.js +15 -0
  17. package/dist/core/approval/pipeline.d.ts +28 -0
  18. package/dist/core/approval/pipeline.js +146 -0
  19. package/dist/core/approval/playbook-schema.d.ts +182 -0
  20. package/dist/core/approval/playbook-schema.js +51 -0
  21. package/dist/core/approval/render.d.ts +20 -0
  22. package/dist/core/approval/render.js +210 -0
  23. package/dist/core/approval/review-pack.d.ts +26 -0
  24. package/dist/core/approval/review-pack.js +205 -0
  25. package/dist/core/approval/types.d.ts +131 -0
  26. package/dist/integrations/shared/capability-evidence.js +2 -0
  27. package/dist/integrations/shared/parity-manifest.js +2 -0
  28. package/package.json +2 -1
  29. package/prompts/approval/acp-pipeline.md +104 -0
  30. package/prompts/approval/ai-review.md +145 -0
  31. package/prompts/approval/api-guidance.md +179 -0
  32. package/prompts/approval/generate.md +164 -13
  33. package/prompts/approval/multi-repo-guidance.md +238 -0
  34. package/prompts/approval/project-conventions-guidance.md +1 -1
  35. package/prompts/approval/runtime-guidance.md +64 -0
  36. package/prompts/approval/segmented-generation.md +23 -11
  37. package/skills/GUIDANCE_PACKS.md +2 -2
  38. package/skills/specflow-approval/SKILL.md +80 -18
  39. package/templates/approval-index.yaml +41 -0
  40. package/templates/approval-part.md +1 -1
  41. package/templates/approval-playbook.yaml +28 -0
@@ -0,0 +1,10 @@
1
+ import type { ApprovalDiagnostic } from './types.js';
2
+ export interface ApprovalLintOptions {
3
+ readonly changeDir: string;
4
+ readonly strict?: boolean;
5
+ }
6
+ export interface ApprovalLintResult {
7
+ readonly ok: boolean;
8
+ readonly diagnostics: readonly ApprovalDiagnostic[];
9
+ }
10
+ export declare function lintApprovalDocument(options: ApprovalLintOptions): Promise<ApprovalLintResult>;
@@ -0,0 +1,302 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { findForbiddenMatch, findInconsistencyHints } from './forbidden-patterns.js';
3
+ import { readApprovalIndex } from './assemble.js';
4
+ import { getApprovalAnalysisPath, getApprovalPartPath } from './paths.js';
5
+ async function readPartContents(changeDir, index) {
6
+ const parts = new Map();
7
+ for (const partId of index.parts_order) {
8
+ try {
9
+ parts.set(partId, await fs.readFile(getApprovalPartPath(changeDir, partId), 'utf-8'));
10
+ }
11
+ catch {
12
+ // missing parts handled by assemble check
13
+ }
14
+ }
15
+ return parts;
16
+ }
17
+ function allPartsText(parts) {
18
+ return [...parts.values()].join('\n');
19
+ }
20
+ export async function lintApprovalDocument(options) {
21
+ const diagnostics = [];
22
+ const { changeDir, strict = false } = options;
23
+ let index;
24
+ try {
25
+ index = await readApprovalIndex(changeDir);
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return {
30
+ ok: false,
31
+ diagnostics: [
32
+ {
33
+ code: 'missing_index',
34
+ severity: 'error',
35
+ message: `Cannot read approval/index.yaml: ${message}`,
36
+ },
37
+ ],
38
+ };
39
+ }
40
+ const partContents = await readPartContents(changeDir, index);
41
+ const combined = allPartsText(partContents);
42
+ // C3: design_points referenced somewhere in parts
43
+ for (const point of index.design_points) {
44
+ const id = point.replace(/^P/, 'P');
45
+ const pattern = new RegExp(`\\b${escapeRegExp(id)}\\b`);
46
+ if (!pattern.test(combined)) {
47
+ diagnostics.push({
48
+ code: 'design_point_unreferenced',
49
+ severity: strict ? 'error' : 'warning',
50
+ message: `Design point "${id}" listed in index but not referenced in any part`,
51
+ });
52
+ }
53
+ }
54
+ // C3b: decisions referenced in parts
55
+ for (const decision of index.decisions) {
56
+ const id = decision.replace(/^D/, 'D');
57
+ const pattern = new RegExp(`\\b${escapeRegExp(id)}\\b`);
58
+ if (!pattern.test(combined)) {
59
+ diagnostics.push({
60
+ code: 'decision_unreferenced',
61
+ severity: strict ? 'error' : 'warning',
62
+ message: `Decision "${id}" listed in index but not referenced in any part`,
63
+ });
64
+ }
65
+ }
66
+ // C3c: tables / capabilities referenced in assigned parts
67
+ for (const table of index.tables) {
68
+ const partText = partContents.get(table.part) ?? '';
69
+ if (partText.length === 0) {
70
+ continue;
71
+ }
72
+ const hasRef = partText.includes(table.id) ||
73
+ partText.includes(table.name) ||
74
+ new RegExp(`\\b${escapeRegExp(table.id)}\\b`).test(partText);
75
+ if (!hasRef) {
76
+ diagnostics.push({
77
+ code: 'table_unreferenced',
78
+ severity: strict ? 'error' : 'warning',
79
+ message: `Table "${table.id}" (${table.name}) not referenced in part "${table.part}"`,
80
+ part: table.part,
81
+ });
82
+ }
83
+ }
84
+ for (const cap of index.capabilities) {
85
+ const partText = partContents.get(cap.part) ?? '';
86
+ if (partText.length === 0) {
87
+ continue;
88
+ }
89
+ if (!partText.includes(cap.id)) {
90
+ diagnostics.push({
91
+ code: 'capability_unreferenced',
92
+ severity: strict ? 'error' : 'warning',
93
+ message: `Capability "${cap.id}" not referenced in part "${cap.part}"`,
94
+ part: cap.part,
95
+ });
96
+ }
97
+ }
98
+ // C4: page apis exist as interface ids
99
+ const interfaceIds = new Set(index.interfaces.map((i) => i.id));
100
+ for (const page of index.pages) {
101
+ for (const apiId of page.apis) {
102
+ if (!interfaceIds.has(apiId)) {
103
+ diagnostics.push({
104
+ code: 'page_api_missing',
105
+ severity: 'error',
106
+ message: `Page "${page.id}" references api "${apiId}" not in index.interfaces`,
107
+ part: page.part,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ // C1 + C2: frozen fields and pairs_with
113
+ for (const iface of index.interfaces) {
114
+ const frozen = iface.frozen;
115
+ const partText = partContents.get(iface.part) ?? '';
116
+ if (frozen?.rpc) {
117
+ const rpcShort = frozen.rpc.split('.').pop() ?? frozen.rpc;
118
+ if (partText.length > 0 && !partText.includes(frozen.rpc) && !partText.includes(rpcShort)) {
119
+ diagnostics.push({
120
+ code: 'frozen_rpc_missing',
121
+ severity: 'error',
122
+ message: `Interface "${iface.id}" frozen.rpc "${frozen.rpc}" not found in part "${iface.part}"`,
123
+ part: iface.part,
124
+ });
125
+ }
126
+ }
127
+ if (frozen?.http_path) {
128
+ const pathOnly = frozen.http_path.replace(/^(GET|POST|PUT|PATCH|DELETE)\s+/i, '');
129
+ if (partText.length > 0 && !partText.includes(pathOnly)) {
130
+ diagnostics.push({
131
+ code: 'frozen_http_path_missing',
132
+ severity: 'error',
133
+ message: `Interface "${iface.id}" frozen.http_path "${frozen.http_path}" not found in part "${iface.part}"`,
134
+ part: iface.part,
135
+ });
136
+ }
137
+ }
138
+ if (frozen?.status_enum && frozen.status_enum.length > 0) {
139
+ for (const status of frozen.status_enum) {
140
+ if (partText.length > 0 && !partText.includes(status)) {
141
+ diagnostics.push({
142
+ code: 'frozen_status_enum_missing',
143
+ severity: strict ? 'error' : 'warning',
144
+ message: `Interface "${iface.id}" frozen status "${status}" not mentioned in part "${iface.part}"`,
145
+ part: iface.part,
146
+ });
147
+ }
148
+ }
149
+ }
150
+ if (iface.pairs_with) {
151
+ const pair = index.interfaces.find((i) => i.id === iface.pairs_with);
152
+ if (!pair) {
153
+ diagnostics.push({
154
+ code: 'pairs_with_missing',
155
+ severity: 'error',
156
+ message: `Interface "${iface.id}" pairs_with "${iface.pairs_with}" not in index.interfaces`,
157
+ part: iface.part,
158
+ });
159
+ }
160
+ else if (!partContents.has(pair.part)) {
161
+ diagnostics.push({
162
+ code: 'pairs_with_part_missing',
163
+ severity: 'error',
164
+ message: `Interface "${iface.id}" pairs_with part "${pair.part}" file missing`,
165
+ part: iface.part,
166
+ });
167
+ }
168
+ else {
169
+ const pairText = partContents.get(pair.part) ?? '';
170
+ if (partText.length > 0 && !partText.includes(iface.pairs_with)) {
171
+ diagnostics.push({
172
+ code: 'pairs_with_unref',
173
+ severity: 'warning',
174
+ message: `Interface "${iface.id}" part does not cross-ref pairs_with "${iface.pairs_with}"`,
175
+ part: iface.part,
176
+ });
177
+ }
178
+ if (pairText.length > 0 && !pairText.includes(iface.id)) {
179
+ diagnostics.push({
180
+ code: 'pairs_with_unref',
181
+ severity: 'warning',
182
+ message: `Interface "${iface.pairs_with}" part does not cross-ref pair "${iface.id}"`,
183
+ part: pair.part,
184
+ });
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // C6: feature_slices — all listed parts exist + depth parity
190
+ if (index.acp?.feature_slices) {
191
+ for (const slice of index.acp.feature_slices) {
192
+ const byteLengths = [];
193
+ for (const partId of slice.parts) {
194
+ const content = partContents.get(partId);
195
+ if (!content) {
196
+ diagnostics.push({
197
+ code: 'feature_slice_part_missing',
198
+ severity: 'error',
199
+ message: `Feature slice "${slice.id}" references missing part "${partId}"`,
200
+ part: partId,
201
+ });
202
+ }
203
+ else {
204
+ byteLengths.push(Buffer.byteLength(content, 'utf8'));
205
+ }
206
+ }
207
+ if (byteLengths.length >= 2) {
208
+ const min = Math.min(...byteLengths);
209
+ const max = Math.max(...byteLengths);
210
+ if (min > 0 && max / min >= 8) {
211
+ diagnostics.push({
212
+ code: 'feature_slice_depth_imbalance',
213
+ severity: strict ? 'error' : 'warning',
214
+ message: `Feature slice "${slice.id}" has large depth imbalance across parts (max/min bytes ≥ 8)`,
215
+ });
216
+ }
217
+ }
218
+ }
219
+ }
220
+ // C6b: context budget hint (acp/v2)
221
+ const budget = index.acp?.context_budget_tokens;
222
+ if (budget && budget > 0) {
223
+ const approxTokens = Math.ceil(combined.length / 4);
224
+ if (approxTokens > budget * 1.5) {
225
+ diagnostics.push({
226
+ code: 'context_budget_exceeded',
227
+ severity: 'warning',
228
+ message: `Combined parts ~${approxTokens} tokens exceed acp.context_budget_tokens (${budget}) — consider smaller Map batches`,
229
+ });
230
+ }
231
+ }
232
+ // C7: analysis.json frozen_rpc aligns with index
233
+ try {
234
+ const analysisRaw = JSON.parse(await fs.readFile(getApprovalAnalysisPath(changeDir), 'utf-8'));
235
+ if (analysisRaw.frozen_rpc) {
236
+ const rpcInterfaces = index.interfaces.filter((i) => i.frozen?.rpc);
237
+ if (rpcInterfaces.length > 0) {
238
+ const mismatch = rpcInterfaces.some((i) => i.frozen.rpc !== analysisRaw.frozen_rpc);
239
+ if (mismatch) {
240
+ diagnostics.push({
241
+ code: 'analysis_frozen_rpc_mismatch',
242
+ severity: 'warning',
243
+ message: `analysis.json frozen_rpc "${analysisRaw.frozen_rpc}" differs from index interface frozen.rpc values`,
244
+ });
245
+ }
246
+ }
247
+ }
248
+ }
249
+ catch {
250
+ // analysis optional for lint
251
+ }
252
+ // C8: multi_repo — each repo has at least one entity or part tagged
253
+ if (index.multi_repo?.enabled) {
254
+ for (const repo of index.multi_repo.repos) {
255
+ const hasEntity = index.interfaces.some((i) => i.repo === repo.id) ||
256
+ index.pages.some((p) => p.repo === repo.id) ||
257
+ index.tables.some((t) => t.repo === repo.id);
258
+ if (!hasEntity && index.multi_repo.document_mode === 'per_repo') {
259
+ diagnostics.push({
260
+ code: 'multi_repo_repo_unrepresented',
261
+ severity: 'warning',
262
+ message: `Multi-repo entry "${repo.id}" has no interfaces/pages/tables with repo tag`,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ // C5: forbidden patterns per part
268
+ for (const [partId, content] of partContents) {
269
+ const forbidden = findForbiddenMatch(content);
270
+ if (forbidden) {
271
+ diagnostics.push({
272
+ code: 'forbidden_pattern',
273
+ severity: 'error',
274
+ message: `Part "${partId}": forbidden stub pattern (${forbidden.source})`,
275
+ part: partId,
276
+ });
277
+ }
278
+ for (const hint of findInconsistencyHints(content)) {
279
+ diagnostics.push({
280
+ code: 'inconsistency_hint',
281
+ severity: strict ? 'error' : 'warning',
282
+ message: `Part "${partId}": possible semantic inconsistency (${hint.source})`,
283
+ part: partId,
284
+ });
285
+ }
286
+ }
287
+ // C9: appendix duplication in signoff
288
+ const signoff = partContents.get('10-signoff') ?? '';
289
+ if (/附录\s*A/i.test(signoff)) {
290
+ diagnostics.push({
291
+ code: 'appendix_in_signoff',
292
+ severity: 'warning',
293
+ message: 'Part "10-signoff" contains 附录 A; assemble will skip auto-appendix — ensure no duplicate in output',
294
+ part: '10-signoff',
295
+ });
296
+ }
297
+ const hasError = diagnostics.some((d) => d.severity === 'error');
298
+ return { ok: !hasError, diagnostics };
299
+ }
300
+ function escapeRegExp(value) {
301
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
302
+ }
@@ -6,3 +6,8 @@ export declare function getApprovalPartsDir(changeDir: string): string;
6
6
  export declare function getApprovalPartPath(changeDir: string, partId: string): string;
7
7
  export declare function getApprovalOutputPath(changeDir: string): string;
8
8
  export declare function getApprovalAnalysisPath(changeDir: string): string;
9
+ export declare function getApprovalReviewPacketPath(changeDir: string): string;
10
+ export declare function getApprovalReviewPath(changeDir: string): string;
11
+ export declare function getApprovalPlaybookPath(changeDir: string): string;
12
+ export declare function getApprovalBundleOutputPath(changeDir: string, relativePath: string): string;
13
+ export declare function getApprovalHtmlOutputPath(changeDir: string): string;
@@ -26,3 +26,18 @@ export function getApprovalOutputPath(changeDir) {
26
26
  export function getApprovalAnalysisPath(changeDir) {
27
27
  return join(getApprovalWorkspaceDir(changeDir), 'analysis.json');
28
28
  }
29
+ export function getApprovalReviewPacketPath(changeDir) {
30
+ return join(getApprovalWorkspaceDir(changeDir), 'review-packet.json');
31
+ }
32
+ export function getApprovalReviewPath(changeDir) {
33
+ return join(getApprovalWorkspaceDir(changeDir), 'review-result.json');
34
+ }
35
+ export function getApprovalPlaybookPath(changeDir) {
36
+ return join(getApprovalWorkspaceDir(changeDir), 'approval-playbook.yaml');
37
+ }
38
+ export function getApprovalBundleOutputPath(changeDir, relativePath) {
39
+ return join(changeDir, relativePath);
40
+ }
41
+ export function getApprovalHtmlOutputPath(changeDir) {
42
+ return join(changeDir, 'approval.html');
43
+ }
@@ -0,0 +1,28 @@
1
+ import type { ApprovalDiagnostic } from './types.js';
2
+ export type ApprovalFinalizeStage = 'lint' | 'review-pack' | 'review-required' | 'review-check' | 'check' | 'assemble' | 'bundle' | 'done';
3
+ export interface FinalizeApprovalOptions {
4
+ readonly changeDir: string;
5
+ readonly workspaceRoot?: string;
6
+ readonly strict?: boolean;
7
+ readonly skipReview?: boolean;
8
+ readonly force?: boolean;
9
+ readonly bundle?: boolean | 'auto';
10
+ readonly playbook?: string;
11
+ readonly dryRun?: boolean;
12
+ readonly validateLazy?: boolean;
13
+ }
14
+ export interface FinalizeApprovalResult {
15
+ readonly ok: boolean;
16
+ readonly paused?: boolean;
17
+ readonly stage: ApprovalFinalizeStage;
18
+ readonly change: string;
19
+ readonly outputPath?: string;
20
+ readonly bundlePath?: string;
21
+ readonly reviewPacketPath?: string;
22
+ readonly diagnostics: readonly ApprovalDiagnostic[];
23
+ }
24
+ /**
25
+ * One-shot ACP finalize: lint → review gate → check → assemble → optional bundle.
26
+ * When AI review is required but review-result.json is missing, pauses after review-pack.
27
+ */
28
+ export declare function finalizeApprovalDocument(options: FinalizeApprovalOptions): Promise<FinalizeApprovalResult>;
@@ -0,0 +1,146 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { assembleApprovalDocument, readApprovalIndex } from './assemble.js';
4
+ import { lintApprovalDocument } from './lint.js';
5
+ import { bundleApprovalDocument, resolvePlaybookPath } from './bundle.js';
6
+ import { checkApprovalReview, generateReviewPacket } from './review-pack.js';
7
+ import { getApprovalReviewPath } from './paths.js';
8
+ function mergeDiagnostics(target, next) {
9
+ target.push(...next);
10
+ return next.some((d) => d.severity === 'error');
11
+ }
12
+ async function reviewResultExists(changeDir) {
13
+ try {
14
+ await fs.access(getApprovalReviewPath(changeDir));
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ function shouldRunBundle(bundleOption, multiRepoBundleEnabled) {
22
+ if (bundleOption === true) {
23
+ return true;
24
+ }
25
+ if (bundleOption === false) {
26
+ return false;
27
+ }
28
+ return multiRepoBundleEnabled === true;
29
+ }
30
+ /**
31
+ * One-shot ACP finalize: lint → review gate → check → assemble → optional bundle.
32
+ * When AI review is required but review-result.json is missing, pauses after review-pack.
33
+ */
34
+ export async function finalizeApprovalDocument(options) {
35
+ const diagnostics = [];
36
+ const { changeDir, workspaceRoot = join(changeDir, '..', '..', '..'), strict = true, skipReview = false, force = true, bundle = 'auto', dryRun = false, validateLazy = true, } = options;
37
+ let index;
38
+ try {
39
+ index = await readApprovalIndex(changeDir);
40
+ }
41
+ catch (error) {
42
+ const message = error instanceof Error ? error.message : String(error);
43
+ return {
44
+ ok: false,
45
+ stage: 'lint',
46
+ change: '',
47
+ diagnostics: [
48
+ { code: 'missing_index', severity: 'error', message: `Cannot read index: ${message}` },
49
+ ],
50
+ };
51
+ }
52
+ const lintResult = await lintApprovalDocument({ changeDir, strict });
53
+ if (mergeDiagnostics(diagnostics, lintResult.diagnostics) || !lintResult.ok) {
54
+ return { ok: false, stage: 'lint', change: index.change, diagnostics };
55
+ }
56
+ const reviewEnabled = !skipReview &&
57
+ (index.acp?.review?.enabled === true || index.acp?.review?.required_pass === true);
58
+ let reviewPacketPath;
59
+ if (reviewEnabled) {
60
+ const hasReviewResult = await reviewResultExists(changeDir);
61
+ if (!hasReviewResult) {
62
+ const packResult = await generateReviewPacket({ changeDir });
63
+ reviewPacketPath = packResult.packetPath;
64
+ mergeDiagnostics(diagnostics, packResult.diagnostics);
65
+ if (!packResult.ok) {
66
+ return {
67
+ ok: false,
68
+ stage: 'review-pack',
69
+ change: index.change,
70
+ reviewPacketPath,
71
+ diagnostics,
72
+ };
73
+ }
74
+ diagnostics.push({
75
+ code: 'review_required',
76
+ severity: 'error',
77
+ message: 'AI review required: read prompts/approval/ai-review.md, fix parts, write approval/review-result.json, then re-run specflow approval finalize',
78
+ });
79
+ return {
80
+ ok: false,
81
+ paused: true,
82
+ stage: 'review-required',
83
+ change: index.change,
84
+ reviewPacketPath,
85
+ diagnostics,
86
+ };
87
+ }
88
+ const reviewCheck = await checkApprovalReview({
89
+ changeDir,
90
+ requirePass: true,
91
+ forceRequire: index.acp?.review?.required_pass === true,
92
+ });
93
+ if (mergeDiagnostics(diagnostics, reviewCheck.diagnostics) || !reviewCheck.ok) {
94
+ return { ok: false, stage: 'review-check', change: index.change, diagnostics };
95
+ }
96
+ }
97
+ const checkResult = await assembleApprovalDocument({
98
+ changeDir,
99
+ checkOnly: true,
100
+ validateLazy,
101
+ });
102
+ if (mergeDiagnostics(diagnostics, checkResult.diagnostics) || !checkResult.ok) {
103
+ return { ok: false, stage: 'check', change: index.change, diagnostics };
104
+ }
105
+ if (dryRun) {
106
+ return { ok: true, stage: 'done', change: index.change, diagnostics };
107
+ }
108
+ const assembleResult = await assembleApprovalDocument({
109
+ changeDir,
110
+ checkOnly: false,
111
+ force,
112
+ validateLazy,
113
+ });
114
+ if (mergeDiagnostics(diagnostics, assembleResult.diagnostics) || !assembleResult.ok) {
115
+ return { ok: false, stage: 'assemble', change: index.change, diagnostics };
116
+ }
117
+ let bundlePath;
118
+ if (shouldRunBundle(bundle, index.multi_repo?.bundle?.enabled)) {
119
+ const playbookPath = await resolvePlaybookPath(changeDir, options.playbook);
120
+ const bundleResult = await bundleApprovalDocument({
121
+ workspaceRoot,
122
+ changeDir,
123
+ playbookPath,
124
+ write: true,
125
+ });
126
+ bundlePath = bundleResult.outputPath;
127
+ if (mergeDiagnostics(diagnostics, bundleResult.diagnostics) || !bundleResult.ok) {
128
+ return {
129
+ ok: false,
130
+ stage: 'bundle',
131
+ change: index.change,
132
+ outputPath: assembleResult.outputPath,
133
+ bundlePath,
134
+ diagnostics,
135
+ };
136
+ }
137
+ }
138
+ return {
139
+ ok: true,
140
+ stage: 'done',
141
+ change: index.change,
142
+ outputPath: assembleResult.outputPath,
143
+ bundlePath,
144
+ diagnostics,
145
+ };
146
+ }