spec-agent 2.0.4 → 2.0.6

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.

Potentially problematic release.


This version of spec-agent might be problematic. Click here for more details.

@@ -23,6 +23,28 @@ interface HandoffTask {
23
23
  assignedType: string;
24
24
  estimatedHours?: number;
25
25
  sourceChunks?: number[];
26
+ prototypeRefs?: PrototypeMatch[];
27
+ }
28
+
29
+ interface PrototypeIndexFile {
30
+ assets: PrototypeAssetEntry[];
31
+ }
32
+
33
+ interface PrototypeAssetEntry {
34
+ id: string;
35
+ path: string;
36
+ sourceFile: string;
37
+ alt?: string;
38
+ summary?: string;
39
+ sourceChunks?: number[];
40
+ }
41
+
42
+ interface PrototypeMatch {
43
+ id: string;
44
+ path: string;
45
+ reason: string;
46
+ score: number;
47
+ summary?: string;
26
48
  }
27
49
 
28
50
  export async function handoffCommand(options: HandoffOptions, command: Command): Promise<void> {
@@ -33,6 +55,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
33
55
  const dispatchPath = path.join(workspacePath, 'dispatch_plan.json');
34
56
  const taskPlanPath = path.join(workspacePath, 'task_plan.json');
35
57
  const specPath = path.join(workspacePath, 'spec_summary.json');
58
+ const prototypeIndexPath = path.join(workspacePath, 'prototype_index.json');
36
59
 
37
60
  if (!(await fileExists(dispatchPath))) {
38
61
  logger.error(`dispatch_plan.json not found in workspace: ${workspacePath}`);
@@ -52,6 +75,10 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
52
75
  const dispatch = await readJson<DispatchPlan>(dispatchPath);
53
76
  const taskPlan = await readJson<TaskPlan>(taskPlanPath);
54
77
  const spec = (await fileExists(specPath)) ? await readJson<SpecSummary>(specPath) : null;
78
+ const prototypeIndex = (await fileExists(prototypeIndexPath))
79
+ ? await readJson<PrototypeIndexFile>(prototypeIndexPath)
80
+ : null;
81
+ const prototypeAssets = prototypeIndex?.assets || [];
55
82
 
56
83
  const taskById = new Map<string, Task>();
57
84
  for (const group of taskPlan.parallelGroups) {
@@ -70,6 +97,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
70
97
  for (const taskId of agent.assignedTasks) {
71
98
  const task = taskById.get(taskId);
72
99
  if (!task) continue;
100
+ const sourceChunks = guessSourceChunks(task, sourceChunkMap);
73
101
  const mapped: HandoffTask = {
74
102
  id: task.id,
75
103
  name: task.name,
@@ -79,7 +107,8 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
79
107
  assignedAgent: agent.agentId,
80
108
  assignedType: agentType,
81
109
  estimatedHours: task.estimatedHours,
82
- sourceChunks: guessSourceChunks(task, sourceChunkMap),
110
+ sourceChunks,
111
+ prototypeRefs: matchTaskPrototypes(task, sourceChunks, prototypeAssets),
83
112
  };
84
113
  rows.push(mapped);
85
114
  handoffTasks.push(mapped);
@@ -136,10 +165,12 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
136
165
  taskPlan: 'task_plan.json',
137
166
  dispatchPlan: 'dispatch_plan.json',
138
167
  summariesDir: options.includeSummaries ? 'summaries/' : null,
168
+ prototypeIndex: prototypeAssets.length > 0 ? 'prototype_index.json' : null,
139
169
  },
140
170
  totals: {
141
171
  agents: agentTaskMap.size,
142
172
  tasks: handoffTasks.length,
173
+ prototypeAssets: prototypeAssets.length,
143
174
  },
144
175
  tasks: handoffTasks,
145
176
  unassigned: dispatch.unassigned,
@@ -152,6 +183,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
152
183
  target: normalizeTarget(options.target),
153
184
  totalAgents: agentTaskMap.size,
154
185
  totalTasks: handoffTasks.length,
186
+ totalPrototypeAssets: prototypeAssets.length,
155
187
  includeSummaries: Boolean(options.includeSummaries),
156
188
  }),
157
189
  'utf-8'
@@ -164,6 +196,7 @@ export async function handoffCommand(options: HandoffOptions, command: Command):
164
196
  target: normalizeTarget(options.target),
165
197
  totalAgents: agentTaskMap.size,
166
198
  totalTasks: handoffTasks.length,
199
+ totalPrototypeAssets: prototypeAssets.length,
167
200
  outputPath,
168
201
  bundlePath,
169
202
  indexPath,
@@ -235,6 +268,8 @@ function renderTaskPrompt(input: {
235
268
  const sourceChunkText = row.sourceChunks && row.sourceChunks.length > 0
236
269
  ? row.sourceChunks.map(i => `chunk_${i}_summary.json`).join(', ')
237
270
  : '未定位到明确 source chunk,请从 spec_summary.json 补充判断';
271
+ const prototypeLines = (row.prototypeRefs || [])
272
+ .map(ref => `- ${ref.id} | \`${ref.path}\` | score=${ref.score} | ${ref.reason}${ref.summary ? ` | ${ref.summary}` : ''}`);
238
273
 
239
274
  return [
240
275
  `# Task Handoff - ${row.id} ${row.name}`,
@@ -259,11 +294,16 @@ function renderTaskPrompt(input: {
259
294
  '- `task_plan.json`',
260
295
  '- `dispatch_plan.json`',
261
296
  includeSummaries ? '- `summaries/`' : '- (可选)`summaries/`',
297
+ '- (可选)`prototype_index.json` / `prototypes/*`',
262
298
  '',
263
299
  '## Suggested Evidence',
264
300
  '',
265
301
  `- ${sourceChunkText}`,
266
302
  '',
303
+ '## Prototype References',
304
+ '',
305
+ ...(prototypeLines.length > 0 ? prototypeLines : ['- 无直接匹配原型,必要时在 prototype_index.json 中人工筛选']),
306
+ '',
267
307
  '## Execution Constraints',
268
308
  '',
269
309
  '- 只改与该任务直接相关文件;避免大范围重构',
@@ -309,20 +349,23 @@ function renderIndexMarkdown(input: {
309
349
  target: string;
310
350
  totalAgents: number;
311
351
  totalTasks: number;
352
+ totalPrototypeAssets: number;
312
353
  includeSummaries: boolean;
313
354
  }): string {
314
- const { target, totalAgents, totalTasks, includeSummaries } = input;
355
+ const { target, totalAgents, totalTasks, totalPrototypeAssets, includeSummaries } = input;
315
356
  return [
316
357
  '# Handoff Bundle Index',
317
358
  '',
318
359
  `- Target: ${target}`,
319
360
  `- Total Agents: ${totalAgents}`,
320
361
  `- Total Tasks: ${totalTasks}`,
362
+ `- Total Prototype Assets: ${totalPrototypeAssets}`,
321
363
  `- Include Summaries: ${includeSummaries ? 'yes' : 'no'}`,
322
364
  '',
323
365
  '## Files',
324
366
  '',
325
367
  '- `handoff_bundle.json`: 机器可读任务总览',
368
+ '- `prototype_index.json`: 原型图索引(如存在)',
326
369
  '- `agents/*.md`: 每个 agent 的任务队列',
327
370
  '- `tasks/*.md`: 每个任务可直接投喂编码 Agent 的提示词模板',
328
371
  '',
@@ -334,6 +377,67 @@ function renderIndexMarkdown(input: {
334
377
  ].join('\n');
335
378
  }
336
379
 
380
+ function matchTaskPrototypes(
381
+ task: Task,
382
+ sourceChunks: number[] | undefined,
383
+ assets: PrototypeAssetEntry[]
384
+ ): PrototypeMatch[] {
385
+ if (assets.length === 0) {
386
+ return [];
387
+ }
388
+
389
+ const taskName = normalizeName(task.name);
390
+ const taskTokens = new Set(taskName.split(' ').filter(token => token.length >= 2));
391
+ const matches: PrototypeMatch[] = [];
392
+
393
+ for (const asset of assets) {
394
+ let score = 0;
395
+ const reasons: string[] = [];
396
+
397
+ const chunkHits = intersectCount(sourceChunks || [], asset.sourceChunks || []);
398
+ if (chunkHits > 0) {
399
+ score += 70 + Math.min(20, chunkHits * 5);
400
+ reasons.push(`chunk重合(${chunkHits})`);
401
+ }
402
+
403
+ const text = normalizeName(`${asset.alt || ''} ${asset.summary || ''} ${path.basename(asset.sourceFile || '')}`);
404
+ let tokenHits = 0;
405
+ for (const token of taskTokens) {
406
+ if (token.length >= 2 && text.includes(token)) {
407
+ tokenHits++;
408
+ }
409
+ }
410
+ if (tokenHits > 0) {
411
+ score += Math.min(30, tokenHits * 8);
412
+ reasons.push(`关键词命中(${tokenHits})`);
413
+ }
414
+
415
+ if (score >= 35) {
416
+ matches.push({
417
+ id: asset.id,
418
+ path: asset.path,
419
+ score,
420
+ reason: reasons.join(' + ') || '弱匹配',
421
+ summary: asset.summary,
422
+ });
423
+ }
424
+ }
425
+
426
+ return matches
427
+ .sort((a, b) => b.score - a.score)
428
+ .slice(0, 5);
429
+ }
430
+
431
+ function intersectCount(a: number[], b: number[]): number {
432
+ if (a.length === 0 || b.length === 0) return 0;
433
+ const set = new Set(a);
434
+ let count = 0;
435
+ for (const value of b) {
436
+ if (set.has(value)) count++;
437
+ }
438
+ return count;
439
+ }
440
+
337
441
  function normalizeName(input: string): string {
338
442
  return (input || '')
339
443
  .toLowerCase()
@@ -1,4 +1,5 @@
1
1
  import * as path from 'path';
2
+ import * as fs from 'fs-extra';
2
3
  import { Command } from 'commander';
3
4
  import { Logger } from '../utils/logger';
4
5
  import {
@@ -8,8 +9,7 @@ import {
8
9
  formatSize,
9
10
  parseSize,
10
11
  findFiles,
11
- writeJson,
12
- readFileContent
12
+ writeJson
13
13
  } from '../utils/file';
14
14
  import { parseDocument, analyzeBase64Images } from '../services/document-parser';
15
15
  import {
@@ -19,7 +19,7 @@ import {
19
19
  getLLMConfigForPurpose,
20
20
  validateLLMConfig
21
21
  } from '../services/llm';
22
- import { Manifest, Chunk } from '../types';
22
+ import { Manifest, Chunk, PrototypeAsset } from '../types';
23
23
 
24
24
  interface ScanOptions {
25
25
  input?: string;
@@ -53,6 +53,16 @@ const SCAN_EXIT_CODE = {
53
53
  RUNTIME_ERROR: 10,
54
54
  } as const;
55
55
 
56
+ interface PrototypeAssetPending {
57
+ id: string;
58
+ sourceFile: string;
59
+ alt: string;
60
+ mimeType: string;
61
+ estimatedSize: number;
62
+ dataUri: string;
63
+ summary?: string;
64
+ }
65
+
56
66
  export async function scanCommand(options: ScanOptions, command: Command): Promise<void> {
57
67
  const logger = new Logger();
58
68
 
@@ -118,6 +128,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
118
128
  let llmChunkingFallbackFiles = 0;
119
129
  let imageAssetsDetected = 0;
120
130
  let imageAssetsDescribed = 0;
131
+ const pendingPrototypeAssets: PrototypeAssetPending[] = [];
121
132
  if (useLLMChunking && llmConfig) {
122
133
  // LLM chunking is enabled by default; fail fast so we don't silently degrade
123
134
  // to regex-only behavior on complex documents.
@@ -129,23 +140,44 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
129
140
  }
130
141
  }
131
142
 
132
- for (const filePath of inputFiles) {
143
+ for (let fileIdx = 0; fileIdx < inputFiles.length; fileIdx++) {
144
+ const filePath = inputFiles[fileIdx];
133
145
  logger.info(`Parsing ${path.basename(filePath)}...`);
134
146
 
135
147
  try {
136
148
  const parsed = await parseDocument(filePath);
137
149
  let contentForChunking = parsed.content;
138
- imageAssetsDetected += parsed.images?.length || 0;
150
+ const parsedImages = (parsed.images || []).map(image => ({
151
+ ...image,
152
+ id: scopeImageId(fileIdx, image.id),
153
+ }));
154
+ const idMapping = new Map<string, string>();
155
+ for (let i = 0; i < (parsed.images || []).length; i++) {
156
+ idMapping.set(parsed.images![i].id, parsedImages[i].id);
157
+ }
158
+ contentForChunking = replaceImageIds(contentForChunking, idMapping);
159
+ imageAssetsDetected += parsedImages.length;
139
160
  let imageSummaries: Record<string, string> = {};
140
161
 
141
- if (parsed.images && parsed.images.length > 0) {
142
- logger.info(` 检测到 ${parsed.images.length} 张嵌入图片`);
162
+ if (parsedImages.length > 0) {
163
+ logger.info(` 检测到 ${parsedImages.length} 张嵌入图片`);
143
164
  if (llmConfig && llmConfig.apiKey) {
144
- imageSummaries = await describeEmbeddedImages(parsed.images, llmConfig, logger);
165
+ imageSummaries = await describeEmbeddedImages(parsedImages, llmConfig, logger);
145
166
  imageAssetsDescribed += Object.keys(imageSummaries).length;
146
167
  } else {
147
168
  logger.warn(' 未配置 LLM,图片仅保留占位信息,不含语义摘要');
148
169
  }
170
+ for (const image of parsedImages) {
171
+ pendingPrototypeAssets.push({
172
+ id: image.id,
173
+ sourceFile: filePath,
174
+ alt: image.alt,
175
+ mimeType: image.mimeType,
176
+ estimatedSize: image.estimatedSize,
177
+ dataUri: image.dataUri,
178
+ summary: imageSummaries[image.id],
179
+ });
180
+ }
149
181
  }
150
182
 
151
183
  // Log if base64 images were found and removed
@@ -235,7 +267,9 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
235
267
  // Prepare chunks directory
236
268
  const outputDir = path.dirname(path.resolve(options.output));
237
269
  const chunksDir = path.join(outputDir, 'chunks');
270
+ const prototypesDir = path.join(outputDir, 'prototypes');
238
271
  await ensureDir(chunksDir);
272
+ await ensureDir(prototypesDir);
239
273
 
240
274
  // Write chunks to files
241
275
  const chunks: Chunk[] = [];
@@ -244,7 +278,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
244
278
  const chunkFileName = `chunk_${i}.txt`;
245
279
  const chunkFilePath = path.join(chunksDir, chunkFileName);
246
280
 
247
- await require('fs-extra').writeFile(chunkFilePath, rawChunk.content, 'utf-8');
281
+ await fs.writeFile(chunkFilePath, rawChunk.content, 'utf-8');
248
282
 
249
283
  chunks.push({
250
284
  id: i,
@@ -254,6 +288,13 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
254
288
  });
255
289
  }
256
290
 
291
+ const prototypeAssets = await writePrototypeAssets({
292
+ outputDir,
293
+ prototypesDir,
294
+ rawChunks,
295
+ pendingAssets: pendingPrototypeAssets,
296
+ });
297
+
257
298
  // Create manifest
258
299
  const manifest: Manifest = {
259
300
  version: '1.0.0',
@@ -262,6 +303,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
262
303
  totalSize: totalSize,
263
304
  chunkSize: chunkSizeBytes,
264
305
  chunks: chunks,
306
+ prototypeAssets,
265
307
  };
266
308
 
267
309
  // Write manifest
@@ -270,6 +312,9 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
270
312
  await writeJson(outputPath, manifest);
271
313
 
272
314
  logger.success(`Manifest created: ${outputPath}`);
315
+ if (prototypeAssets.length > 0) {
316
+ logger.info(`Prototype index: ${path.join(outputDir, 'prototype_index.json')}`);
317
+ }
273
318
  logger.json({
274
319
  status: 'success',
275
320
  totalFiles: inputFiles.length,
@@ -281,6 +326,7 @@ export async function scanCommand(options: ScanOptions, command: Command): Promi
281
326
  llmChunkingFallbackFiles,
282
327
  imageAssetsDetected,
283
328
  imageAssetsDescribed,
329
+ prototypeAssets: prototypeAssets.length,
284
330
  manifestPath: outputPath,
285
331
  });
286
332
 
@@ -527,7 +573,7 @@ function appendImageSummariesToChunk(content: string, imageSummaries: Record<str
527
573
  return content;
528
574
  }
529
575
 
530
- const idMatches = content.match(/图片引用\s+((?:IMG|PDFIMG)\d{4})/g) || [];
576
+ const idMatches = content.match(/图片引用\s+([A-Z0-9_]+)/g) || [];
531
577
  const imageIds = Array.from(new Set(idMatches.map(item => item.replace(/.*\s+/, '').trim())));
532
578
  if (imageIds.length === 0) {
533
579
  return content;
@@ -548,6 +594,113 @@ function appendImageSummariesToChunk(content: string, imageSummaries: Record<str
548
594
  return `${content}\n\n### 图片语义补充\n${summaryLines.join('\n')}`;
549
595
  }
550
596
 
597
+ async function writePrototypeAssets(input: {
598
+ outputDir: string;
599
+ prototypesDir: string;
600
+ rawChunks: Array<{ content: string; sourceFiles: string[]; title?: string }>;
601
+ pendingAssets: PrototypeAssetPending[];
602
+ }): Promise<PrototypeAsset[]> {
603
+ const { outputDir, prototypesDir, rawChunks, pendingAssets } = input;
604
+ if (pendingAssets.length === 0) {
605
+ return [];
606
+ }
607
+
608
+ const prototypeAssets: PrototypeAsset[] = [];
609
+ for (const asset of pendingAssets) {
610
+ const ext = extFromMimeType(asset.mimeType);
611
+ const fileName = `${asset.id}.${ext}`;
612
+ const absolutePath = path.join(prototypesDir, fileName);
613
+ const written = await writeDataUriToFile(asset.dataUri, absolutePath);
614
+ if (!written) {
615
+ continue;
616
+ }
617
+ const sourceChunks = collectSourceChunks(rawChunks, asset.id);
618
+ prototypeAssets.push({
619
+ id: asset.id,
620
+ sourceFile: asset.sourceFile,
621
+ alt: asset.alt,
622
+ mimeType: asset.mimeType,
623
+ estimatedSize: asset.estimatedSize,
624
+ path: path.join('prototypes', fileName).replace(/\\/g, '/'),
625
+ summary: asset.summary,
626
+ sourceChunks,
627
+ });
628
+ }
629
+
630
+ const indexJsonPath = path.join(outputDir, 'prototype_index.json');
631
+ await writeJson(indexJsonPath, {
632
+ version: '1.0.0',
633
+ createdAt: new Date().toISOString(),
634
+ total: prototypeAssets.length,
635
+ assets: prototypeAssets,
636
+ });
637
+
638
+ const indexMarkdownPath = path.join(outputDir, 'prototype_index.md');
639
+ const lines: string[] = [
640
+ '# Prototype Index',
641
+ '',
642
+ `- Total: ${prototypeAssets.length}`,
643
+ '',
644
+ '| ID | Path | Source File | Chunks | Summary |',
645
+ '| --- | --- | --- | --- | --- |',
646
+ ];
647
+ for (const asset of prototypeAssets) {
648
+ lines.push(
649
+ `| ${asset.id} | \`${asset.path}\` | \`${path.basename(asset.sourceFile)}\` | ${asset.sourceChunks.join(', ') || '-'} | ${(asset.summary || '').replace(/\|/g, '\\|')} |`
650
+ );
651
+ }
652
+ await fs.writeFile(indexMarkdownPath, `${lines.join('\n')}\n`, 'utf-8');
653
+
654
+ return prototypeAssets;
655
+ }
656
+
657
+ function collectSourceChunks(
658
+ rawChunks: Array<{ content: string; sourceFiles: string[]; title?: string }>,
659
+ imageId: string
660
+ ): number[] {
661
+ const results: number[] = [];
662
+ for (let i = 0; i < rawChunks.length; i++) {
663
+ if (rawChunks[i].content.includes(imageId)) {
664
+ results.push(i);
665
+ }
666
+ }
667
+ return results;
668
+ }
669
+
670
+ function scopeImageId(fileIdx: number, imageId: string): string {
671
+ return `F${String(fileIdx + 1).padStart(3, '0')}_${imageId}`;
672
+ }
673
+
674
+ function replaceImageIds(content: string, idMapping: Map<string, string>): string {
675
+ let next = content;
676
+ for (const [fromId, toId] of idMapping.entries()) {
677
+ next = next.replace(new RegExp(`\\b${escapeRegExp(fromId)}\\b`, 'g'), toId);
678
+ }
679
+ return next;
680
+ }
681
+
682
+ function escapeRegExp(input: string): string {
683
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
684
+ }
685
+
686
+ function extFromMimeType(mimeType: string): string {
687
+ if (mimeType.includes('png')) return 'png';
688
+ if (mimeType.includes('jpeg') || mimeType.includes('jpg')) return 'jpg';
689
+ if (mimeType.includes('webp')) return 'webp';
690
+ if (mimeType.includes('gif')) return 'gif';
691
+ return 'img';
692
+ }
693
+
694
+ async function writeDataUriToFile(dataUri: string, filePath: string): Promise<boolean> {
695
+ const match = dataUri.match(/^data:image\/[a-zA-Z0-9.+-]+;base64,([A-Za-z0-9+/=]+)$/);
696
+ if (!match) {
697
+ return false;
698
+ }
699
+ const buffer = Buffer.from(match[1], 'base64');
700
+ await fs.writeFile(filePath, buffer);
701
+ return true;
702
+ }
703
+
551
704
  function readStdin(): Promise<string> {
552
705
  return new Promise((resolve) => {
553
706
  let data = '';
@@ -36,19 +36,10 @@ export function getLLMConfig(): LLMConfig {
36
36
  };
37
37
  }
38
38
 
39
- export function getLLMConfigForPurpose(purpose: 'scan' | 'analyze' | 'vision' | 'default'): LLMConfig {
40
- const base = getLLMConfig();
41
- const purposeModelMap: Record<string, string | undefined> = {
42
- scan: process.env.LLM_MODEL_SCAN,
43
- analyze: process.env.LLM_MODEL_ANALYZE,
44
- vision: process.env.LLM_MODEL_VISION,
45
- default: process.env.LLM_MODEL,
46
- };
47
- const selectedModel = purposeModelMap[purpose] || base.model;
48
- return {
49
- ...base,
50
- model: selectedModel,
51
- };
39
+ export function getLLMConfigForPurpose(_purpose: 'scan' | 'analyze' | 'vision' | 'default'): LLMConfig {
40
+ // Unified model strategy: always use LLM_MODEL.
41
+ // Keep this function for backward-compatible call sites.
42
+ return getLLMConfig();
52
43
  }
53
44
 
54
45
  export function validateLLMConfig(config: LLMConfig): void {
@@ -348,7 +339,7 @@ export async function describeEmbeddedImages(
348
339
 
349
340
  const visionConfig = {
350
341
  ...config,
351
- model: getLLMConfigForPurpose('vision').model || config.model,
342
+ model: getLLMConfig().model || config.model,
352
343
  };
353
344
  validateLLMConfig(visionConfig);
354
345
  const maxImages = Math.max(0, parseEnvInt('LLM_IMAGE_MAX_PER_DOC', 12));
package/src/types.ts CHANGED
@@ -10,6 +10,17 @@ export interface Chunk {
10
10
  endLine?: number;
11
11
  }
12
12
 
13
+ export interface PrototypeAsset {
14
+ id: string;
15
+ sourceFile: string;
16
+ alt: string;
17
+ mimeType: string;
18
+ estimatedSize: number;
19
+ path: string;
20
+ summary?: string;
21
+ sourceChunks: number[];
22
+ }
23
+
13
24
  export interface Manifest {
14
25
  version: string;
15
26
  createdAt: string;
@@ -17,6 +28,7 @@ export interface Manifest {
17
28
  totalSize: number;
18
29
  chunkSize: number;
19
30
  chunks: Chunk[];
31
+ prototypeAssets?: PrototypeAsset[];
20
32
  }
21
33
 
22
34
  export interface Feature {