@ddse/acm-aicoder 0.5.0

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 (165) hide show
  1. package/.aicoder/index.json +304 -0
  2. package/AICODER_IMPLEMENTATION_PLAN_PHASE2.md +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +490 -0
  5. package/bin/interactive.tsx +232 -0
  6. package/dist/bin/interactive.d.ts +3 -0
  7. package/dist/bin/interactive.d.ts.map +1 -0
  8. package/dist/bin/interactive.js +155 -0
  9. package/dist/bin/interactive.js.map +1 -0
  10. package/dist/src/config/providers.d.ts +15 -0
  11. package/dist/src/config/providers.d.ts.map +1 -0
  12. package/dist/src/config/providers.js +142 -0
  13. package/dist/src/config/providers.js.map +1 -0
  14. package/dist/src/config/session.d.ts +25 -0
  15. package/dist/src/config/session.d.ts.map +1 -0
  16. package/dist/src/config/session.js +97 -0
  17. package/dist/src/config/session.js.map +1 -0
  18. package/dist/src/context/bm25.d.ts +68 -0
  19. package/dist/src/context/bm25.d.ts.map +1 -0
  20. package/dist/src/context/bm25.js +131 -0
  21. package/dist/src/context/bm25.js.map +1 -0
  22. package/dist/src/context/code-search.d.ts +30 -0
  23. package/dist/src/context/code-search.d.ts.map +1 -0
  24. package/dist/src/context/code-search.js +150 -0
  25. package/dist/src/context/code-search.js.map +1 -0
  26. package/dist/src/context/context-pack.d.ts +25 -0
  27. package/dist/src/context/context-pack.d.ts.map +1 -0
  28. package/dist/src/context/context-pack.js +92 -0
  29. package/dist/src/context/context-pack.js.map +1 -0
  30. package/dist/src/context/dependency-mapper.d.ts +10 -0
  31. package/dist/src/context/dependency-mapper.d.ts.map +1 -0
  32. package/dist/src/context/dependency-mapper.js +62 -0
  33. package/dist/src/context/dependency-mapper.js.map +1 -0
  34. package/dist/src/context/index.d.ts +8 -0
  35. package/dist/src/context/index.d.ts.map +1 -0
  36. package/dist/src/context/index.js +9 -0
  37. package/dist/src/context/index.js.map +1 -0
  38. package/dist/src/context/symbol-extractor.d.ts +26 -0
  39. package/dist/src/context/symbol-extractor.d.ts.map +1 -0
  40. package/dist/src/context/symbol-extractor.js +129 -0
  41. package/dist/src/context/symbol-extractor.js.map +1 -0
  42. package/dist/src/context/test-mapper.d.ts +16 -0
  43. package/dist/src/context/test-mapper.d.ts.map +1 -0
  44. package/dist/src/context/test-mapper.js +66 -0
  45. package/dist/src/context/test-mapper.js.map +1 -0
  46. package/dist/src/context/types.d.ts +61 -0
  47. package/dist/src/context/types.d.ts.map +1 -0
  48. package/dist/src/context/types.js +3 -0
  49. package/dist/src/context/types.js.map +1 -0
  50. package/dist/src/context/workspace-indexer.d.ts +39 -0
  51. package/dist/src/context/workspace-indexer.d.ts.map +1 -0
  52. package/dist/src/context/workspace-indexer.js +222 -0
  53. package/dist/src/context/workspace-indexer.js.map +1 -0
  54. package/dist/src/index.d.ts +5 -0
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +6 -0
  57. package/dist/src/index.js.map +1 -0
  58. package/dist/src/registries.d.ts +34 -0
  59. package/dist/src/registries.d.ts.map +1 -0
  60. package/dist/src/registries.js +87 -0
  61. package/dist/src/registries.js.map +1 -0
  62. package/dist/src/runtime/budget-manager.d.ts +42 -0
  63. package/dist/src/runtime/budget-manager.d.ts.map +1 -0
  64. package/dist/src/runtime/budget-manager.js +82 -0
  65. package/dist/src/runtime/budget-manager.js.map +1 -0
  66. package/dist/src/runtime/interactive-runtime.d.ts +39 -0
  67. package/dist/src/runtime/interactive-runtime.d.ts.map +1 -0
  68. package/dist/src/runtime/interactive-runtime.js +321 -0
  69. package/dist/src/runtime/interactive-runtime.js.map +1 -0
  70. package/dist/src/tasks-v2/analysis-tasks.d.ts +117 -0
  71. package/dist/src/tasks-v2/analysis-tasks.d.ts.map +1 -0
  72. package/dist/src/tasks-v2/analysis-tasks.js +209 -0
  73. package/dist/src/tasks-v2/analysis-tasks.js.map +1 -0
  74. package/dist/src/tasks-v2/developer-tasks.d.ts +226 -0
  75. package/dist/src/tasks-v2/developer-tasks.d.ts.map +1 -0
  76. package/dist/src/tasks-v2/developer-tasks.js +322 -0
  77. package/dist/src/tasks-v2/developer-tasks.js.map +1 -0
  78. package/dist/src/tasks-v2/index.d.ts +3 -0
  79. package/dist/src/tasks-v2/index.d.ts.map +1 -0
  80. package/dist/src/tasks-v2/index.js +4 -0
  81. package/dist/src/tasks-v2/index.js.map +1 -0
  82. package/dist/src/tools-v2/edit-tools.d.ts +67 -0
  83. package/dist/src/tools-v2/edit-tools.d.ts.map +1 -0
  84. package/dist/src/tools-v2/edit-tools.js +117 -0
  85. package/dist/src/tools-v2/edit-tools.js.map +1 -0
  86. package/dist/src/tools-v2/index.d.ts +6 -0
  87. package/dist/src/tools-v2/index.d.ts.map +1 -0
  88. package/dist/src/tools-v2/index.js +7 -0
  89. package/dist/src/tools-v2/index.js.map +1 -0
  90. package/dist/src/tools-v2/read-tools.d.ts +129 -0
  91. package/dist/src/tools-v2/read-tools.d.ts.map +1 -0
  92. package/dist/src/tools-v2/read-tools.js +216 -0
  93. package/dist/src/tools-v2/read-tools.js.map +1 -0
  94. package/dist/src/tools-v2/search-tools.d.ts +73 -0
  95. package/dist/src/tools-v2/search-tools.d.ts.map +1 -0
  96. package/dist/src/tools-v2/search-tools.js +132 -0
  97. package/dist/src/tools-v2/search-tools.js.map +1 -0
  98. package/dist/src/tools-v2/test-tools.d.ts +59 -0
  99. package/dist/src/tools-v2/test-tools.d.ts.map +1 -0
  100. package/dist/src/tools-v2/test-tools.js +111 -0
  101. package/dist/src/tools-v2/test-tools.js.map +1 -0
  102. package/dist/src/tools-v2/workspace-context.d.ts +65 -0
  103. package/dist/src/tools-v2/workspace-context.d.ts.map +1 -0
  104. package/dist/src/tools-v2/workspace-context.js +336 -0
  105. package/dist/src/tools-v2/workspace-context.js.map +1 -0
  106. package/dist/src/ui/App.d.ts +9 -0
  107. package/dist/src/ui/App.d.ts.map +1 -0
  108. package/dist/src/ui/App.js +257 -0
  109. package/dist/src/ui/App.js.map +1 -0
  110. package/dist/src/ui/components/ChatPane.d.ts +12 -0
  111. package/dist/src/ui/components/ChatPane.d.ts.map +1 -0
  112. package/dist/src/ui/components/ChatPane.js +41 -0
  113. package/dist/src/ui/components/ChatPane.js.map +1 -0
  114. package/dist/src/ui/components/EventsPane.d.ts +12 -0
  115. package/dist/src/ui/components/EventsPane.d.ts.map +1 -0
  116. package/dist/src/ui/components/EventsPane.js +48 -0
  117. package/dist/src/ui/components/EventsPane.js.map +1 -0
  118. package/dist/src/ui/components/GoalsTasksPane.d.ts +18 -0
  119. package/dist/src/ui/components/GoalsTasksPane.d.ts.map +1 -0
  120. package/dist/src/ui/components/GoalsTasksPane.js +83 -0
  121. package/dist/src/ui/components/GoalsTasksPane.js.map +1 -0
  122. package/dist/src/ui/store.d.ts +74 -0
  123. package/dist/src/ui/store.d.ts.map +1 -0
  124. package/dist/src/ui/store.js +260 -0
  125. package/dist/src/ui/store.js.map +1 -0
  126. package/dist/tests/integration.test.d.ts +2 -0
  127. package/dist/tests/integration.test.d.ts.map +1 -0
  128. package/dist/tests/integration.test.js +415 -0
  129. package/dist/tests/integration.test.js.map +1 -0
  130. package/dist/tsconfig.tsbuildinfo +1 -0
  131. package/docs/AICODER.png +0 -0
  132. package/docs/INTERACTIVE_CLI_GUIDE.md +201 -0
  133. package/docs/TUI_MOCKUP.md +180 -0
  134. package/package.json +52 -0
  135. package/src/config/providers.ts +174 -0
  136. package/src/config/session.ts +143 -0
  137. package/src/context/bm25.ts +173 -0
  138. package/src/context/code-search.ts +188 -0
  139. package/src/context/context-pack.ts +133 -0
  140. package/src/context/dependency-mapper.ts +72 -0
  141. package/src/context/index.ts +8 -0
  142. package/src/context/symbol-extractor.ts +149 -0
  143. package/src/context/test-mapper.ts +77 -0
  144. package/src/context/types.ts +69 -0
  145. package/src/context/workspace-indexer.ts +249 -0
  146. package/src/index.ts +5 -0
  147. package/src/registries.ts +118 -0
  148. package/src/runtime/budget-manager.ts +118 -0
  149. package/src/runtime/interactive-runtime.ts +423 -0
  150. package/src/tasks-v2/analysis-tasks.ts +311 -0
  151. package/src/tasks-v2/developer-tasks.ts +437 -0
  152. package/src/tasks-v2/index.ts +3 -0
  153. package/src/tools-v2/edit-tools.ts +153 -0
  154. package/src/tools-v2/index.ts +6 -0
  155. package/src/tools-v2/read-tools.ts +286 -0
  156. package/src/tools-v2/search-tools.ts +175 -0
  157. package/src/tools-v2/test-tools.ts +147 -0
  158. package/src/tools-v2/workspace-context.ts +428 -0
  159. package/src/ui/App.tsx +392 -0
  160. package/src/ui/components/ChatPane.tsx +84 -0
  161. package/src/ui/components/EventsPane.tsx +81 -0
  162. package/src/ui/components/GoalsTasksPane.tsx +149 -0
  163. package/src/ui/store.ts +362 -0
  164. package/tests/integration.test.ts +537 -0
  165. package/tsconfig.json +22 -0
@@ -0,0 +1,428 @@
1
+ // Workspace Context Retrieval Tool
2
+ import { Tool, type ContextRetrievalArtifact } from '@ddse/acm-sdk';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { CodeSearch, WorkspaceIndexer, type WorkspaceIndex } from '../context/index.js';
6
+ import { GrepTool } from './search-tools.js';
7
+
8
+ export type WorkspaceContextOperation =
9
+ | {
10
+ type: 'search';
11
+ query: string;
12
+ k?: number;
13
+ includeContext?: boolean;
14
+ contextLines?: number;
15
+ preferTypes?: string[];
16
+ rationale?: string;
17
+ }
18
+ | {
19
+ type: 'symbol';
20
+ symbol: string;
21
+ k?: number;
22
+ rationale?: string;
23
+ }
24
+ | {
25
+ type: 'grep';
26
+ pattern: string;
27
+ regex?: boolean;
28
+ caseInsensitive?: boolean;
29
+ maxResults?: number;
30
+ rationale?: string;
31
+ }
32
+ | {
33
+ type: 'file';
34
+ path: string;
35
+ startLine?: number;
36
+ endLine?: number;
37
+ maxBytes?: number;
38
+ rationale?: string;
39
+ }
40
+ | {
41
+ type: 'recent';
42
+ limit?: number;
43
+ languages?: string[];
44
+ rationale?: string;
45
+ }
46
+ | {
47
+ type: 'metadata';
48
+ summary?: boolean;
49
+ languages?: string[];
50
+ rationale?: string;
51
+ };
52
+
53
+ export interface WorkspaceContextRequest {
54
+ directive?: string;
55
+ goal?: string;
56
+ operations?: WorkspaceContextOperation[];
57
+ }
58
+
59
+ const DEFAULT_MAX_BYTES = 16_000;
60
+
61
+ export class WorkspaceContextRetrievalTool extends Tool<
62
+ WorkspaceContextRequest,
63
+ ContextRetrievalArtifact[]
64
+ > {
65
+ private rootPath: string;
66
+ private codeSearch?: CodeSearch;
67
+ private indexer: WorkspaceIndexer;
68
+ private index?: WorkspaceIndex;
69
+ private grepTool: GrepTool;
70
+ private indexingPromise?: Promise<void>;
71
+
72
+ constructor(rootPath: string = process.cwd()) {
73
+ super();
74
+ this.rootPath = path.resolve(rootPath);
75
+ this.indexer = new WorkspaceIndexer(this.rootPath);
76
+ this.grepTool = new GrepTool(this.rootPath);
77
+ }
78
+
79
+ name(): string {
80
+ return 'workspace_context';
81
+ }
82
+
83
+ async call(request: WorkspaceContextRequest): Promise<ContextRetrievalArtifact[]> {
84
+ const operations = request.operations && request.operations.length > 0
85
+ ? request.operations
86
+ : this.deriveOperationsFromDirective(request.directive);
87
+
88
+ if (!operations || operations.length === 0) {
89
+ return [];
90
+ }
91
+
92
+ await this.ensureSearchIndex();
93
+
94
+ const artifacts: ContextRetrievalArtifact[] = [];
95
+ const seen = new Set<string>();
96
+
97
+ for (const operation of operations) {
98
+ try {
99
+ if (operation.type === 'search') {
100
+ const searchArtifacts = await this.handleSearch(operation);
101
+ this.pushArtifacts(searchArtifacts, artifacts, seen);
102
+ } else if (operation.type === 'symbol') {
103
+ const symbolArtifacts = await this.handleSymbol(operation);
104
+ this.pushArtifacts(symbolArtifacts, artifacts, seen);
105
+ } else if (operation.type === 'grep') {
106
+ const grepArtifacts = await this.handleGrep(operation);
107
+ this.pushArtifacts(grepArtifacts, artifacts, seen);
108
+ } else if (operation.type === 'file') {
109
+ const fileArtifact = await this.handleFile(operation);
110
+ this.pushArtifacts(fileArtifact, artifacts, seen);
111
+ } else if (operation.type === 'recent') {
112
+ const recentArtifact = await this.handleRecent(operation);
113
+ this.pushArtifacts(recentArtifact, artifacts, seen);
114
+ } else if (operation.type === 'metadata') {
115
+ const metadataArtifact = await this.handleMetadata(operation);
116
+ this.pushArtifacts(metadataArtifact, artifacts, seen);
117
+ }
118
+ } catch (error) {
119
+ // Skip failing operation but surface metadata for debugging
120
+ this.pushArtifacts(
121
+ {
122
+ type: 'workspace.debug',
123
+ content: {
124
+ operation,
125
+ error: error instanceof Error ? error.message : String(error),
126
+ },
127
+ promote: false,
128
+ provenance: {
129
+ tool: this.name(),
130
+ stage: 'operation-error',
131
+ },
132
+ },
133
+ artifacts,
134
+ seen
135
+ );
136
+ }
137
+ }
138
+
139
+ return artifacts;
140
+ }
141
+
142
+ private async ensureSearchIndex(): Promise<void> {
143
+ if (this.index && this.codeSearch) {
144
+ return;
145
+ }
146
+
147
+ if (!this.indexingPromise) {
148
+ this.indexingPromise = (async () => {
149
+ this.index = await this.indexer.buildIndex({ useCache: true });
150
+ this.codeSearch = new CodeSearch(this.rootPath);
151
+ await this.codeSearch.indexFiles(this.index);
152
+ })();
153
+ }
154
+
155
+ await this.indexingPromise;
156
+ }
157
+
158
+ private deriveOperationsFromDirective(directive?: string): WorkspaceContextOperation[] {
159
+ if (!directive) {
160
+ return [];
161
+ }
162
+
163
+ const separatorIndex = directive.indexOf(':');
164
+ const payload = separatorIndex >= 0 ? directive.slice(separatorIndex + 1).trim() : '';
165
+
166
+ if (payload.startsWith('{')) {
167
+ try {
168
+ const parsed = JSON.parse(payload);
169
+ if (Array.isArray(parsed.operations)) {
170
+ return parsed.operations as WorkspaceContextOperation[];
171
+ }
172
+ if (typeof parsed.query === 'string' && parsed.query.length > 0) {
173
+ return [
174
+ {
175
+ type: 'search',
176
+ query: parsed.query,
177
+ k: parsed.k,
178
+ includeContext: parsed.includeContext ?? true,
179
+ rationale: parsed.rationale,
180
+ },
181
+ ];
182
+ }
183
+ } catch {
184
+ // fall back to text parsing
185
+ }
186
+ }
187
+
188
+ if (payload.length > 0) {
189
+ return [
190
+ {
191
+ type: 'search',
192
+ query: payload,
193
+ includeContext: true,
194
+ },
195
+ {
196
+ type: 'grep',
197
+ pattern: payload,
198
+ maxResults: 20,
199
+ },
200
+ ];
201
+ }
202
+
203
+ return [];
204
+ }
205
+
206
+ private pushArtifacts(
207
+ artifact: ContextRetrievalArtifact | ContextRetrievalArtifact[] | null | undefined,
208
+ collection: ContextRetrievalArtifact[],
209
+ seen: Set<string>
210
+ ): void {
211
+ if (!artifact) return;
212
+ const artifacts = Array.isArray(artifact) ? artifact : [artifact];
213
+
214
+ for (const entry of artifacts) {
215
+ if (!entry || typeof entry.type !== 'string') continue;
216
+
217
+ const provenance = entry.provenance ?? { tool: this.name() };
218
+ const keySource = JSON.stringify([
219
+ entry.type,
220
+ provenance.tool,
221
+ (entry.content && (entry.content.path || entry.content.id || entry.content.key)) ??
222
+ JSON.stringify(entry.content),
223
+ ]);
224
+
225
+ if (seen.has(keySource)) {
226
+ continue;
227
+ }
228
+
229
+ seen.add(keySource);
230
+ collection.push({
231
+ ...entry,
232
+ provenance,
233
+ });
234
+ }
235
+ }
236
+
237
+ private async handleSearch(operation: Extract<WorkspaceContextOperation, { type: 'search' }>): Promise<ContextRetrievalArtifact[]> {
238
+ if (!this.codeSearch) {
239
+ return [];
240
+ }
241
+
242
+ const results = await this.codeSearch.search(operation.query, {
243
+ k: Math.min(operation.k ?? 8, 20),
244
+ includeContext: operation.includeContext ?? true,
245
+ contextLines: operation.contextLines ?? 2,
246
+ preferTypes: operation.preferTypes,
247
+ });
248
+
249
+ return results.map(result => ({
250
+ type: 'workspace.snippet',
251
+ promote: true,
252
+ content: {
253
+ path: result.path,
254
+ line: result.line,
255
+ snippet: result.snippet,
256
+ score: result.score,
257
+ operation: 'bm25-search',
258
+ query: operation.query,
259
+ rationale: operation.rationale,
260
+ },
261
+ provenance: {
262
+ tool: this.name(),
263
+ operation: 'search',
264
+ },
265
+ }));
266
+ }
267
+
268
+ private async handleSymbol(operation: Extract<WorkspaceContextOperation, { type: 'symbol' }>): Promise<ContextRetrievalArtifact[]> {
269
+ if (!this.codeSearch) {
270
+ return [];
271
+ }
272
+
273
+ const results = await this.codeSearch.searchSymbol(operation.symbol);
274
+ return results.slice(0, Math.min(operation.k ?? 5, 10)).map(result => ({
275
+ type: 'workspace.snippet',
276
+ promote: true,
277
+ content: {
278
+ path: result.path,
279
+ line: result.line,
280
+ snippet: result.snippet,
281
+ score: result.score,
282
+ operation: 'symbol-search',
283
+ symbol: operation.symbol,
284
+ rationale: operation.rationale,
285
+ },
286
+ provenance: {
287
+ tool: this.name(),
288
+ operation: 'symbol',
289
+ },
290
+ }));
291
+ }
292
+
293
+ private async handleGrep(operation: Extract<WorkspaceContextOperation, { type: 'grep' }>): Promise<ContextRetrievalArtifact[]> {
294
+ const result = await this.grepTool.call({
295
+ pattern: operation.pattern,
296
+ regex: operation.regex,
297
+ caseInsensitive: operation.caseInsensitive,
298
+ maxResults: Math.min(operation.maxResults ?? 30, 100),
299
+ });
300
+
301
+ return result.matches.map(match => ({
302
+ type: 'workspace.match',
303
+ promote: true,
304
+ content: {
305
+ path: match.path,
306
+ line: match.line,
307
+ column: match.column,
308
+ preview: match.preview,
309
+ operation: 'grep',
310
+ pattern: operation.pattern,
311
+ rationale: operation.rationale,
312
+ },
313
+ provenance: {
314
+ tool: this.name(),
315
+ operation: 'grep',
316
+ },
317
+ }));
318
+ }
319
+
320
+ private async handleFile(operation: Extract<WorkspaceContextOperation, { type: 'file' }>): Promise<ContextRetrievalArtifact | null> {
321
+ const absolutePath = path.isAbsolute(operation.path)
322
+ ? operation.path
323
+ : path.join(this.rootPath, operation.path);
324
+
325
+ const relativePath = path.relative(this.rootPath, absolutePath);
326
+
327
+ try {
328
+ let content = await fs.readFile(absolutePath, 'utf-8');
329
+ const maxBytes = operation.maxBytes ?? DEFAULT_MAX_BYTES;
330
+ if (content.length > maxBytes) {
331
+ content = content.slice(0, maxBytes);
332
+ }
333
+
334
+ let snippet = content;
335
+ let startLine = 1;
336
+ let endLine = content.split('\n').length;
337
+
338
+ if (operation.startLine || operation.endLine) {
339
+ const lines = content.split('\n');
340
+ startLine = Math.max(operation.startLine ?? 1, 1);
341
+ endLine = Math.min(operation.endLine ?? lines.length, lines.length);
342
+ snippet = lines.slice(startLine - 1, endLine).join('\n');
343
+ }
344
+
345
+ return {
346
+ type: 'workspace.file',
347
+ promote: true,
348
+ content: {
349
+ path: relativePath,
350
+ snippet,
351
+ startLine,
352
+ endLine,
353
+ rationale: operation.rationale,
354
+ },
355
+ provenance: {
356
+ tool: this.name(),
357
+ operation: 'file',
358
+ },
359
+ };
360
+ } catch {
361
+ return {
362
+ type: 'workspace.debug',
363
+ promote: false,
364
+ content: {
365
+ operation,
366
+ error: `Failed to read file: ${relativePath}`,
367
+ },
368
+ provenance: {
369
+ tool: this.name(),
370
+ operation: 'file',
371
+ },
372
+ };
373
+ }
374
+ }
375
+
376
+ private async handleRecent(operation: Extract<WorkspaceContextOperation, { type: 'recent' }>): Promise<ContextRetrievalArtifact | null> {
377
+ if (!this.index) {
378
+ return null;
379
+ }
380
+
381
+ const limit = Math.min(operation.limit ?? 5, 20);
382
+ const files = WorkspaceIndexer.getRecentFiles(this.index, limit);
383
+ const filtered = operation.languages && operation.languages.length > 0
384
+ ? files.filter(file => operation.languages!.includes(file.language))
385
+ : files;
386
+
387
+ return {
388
+ type: 'workspace.metadata',
389
+ promote: true,
390
+ content: {
391
+ kind: 'recent-files',
392
+ files: filtered.map(file => ({
393
+ path: file.path,
394
+ mtime: file.mtime,
395
+ language: file.language,
396
+ size: file.size,
397
+ })),
398
+ rationale: operation.rationale,
399
+ },
400
+ provenance: {
401
+ tool: this.name(),
402
+ operation: 'recent',
403
+ },
404
+ };
405
+ }
406
+
407
+ private async handleMetadata(operation: Extract<WorkspaceContextOperation, { type: 'metadata' }>): Promise<ContextRetrievalArtifact | null> {
408
+ if (!this.index) {
409
+ return null;
410
+ }
411
+
412
+ return {
413
+ type: 'workspace.metadata',
414
+ promote: true,
415
+ content: {
416
+ kind: 'summary',
417
+ files: this.index.totalFiles,
418
+ totalSize: this.index.totalSize,
419
+ languages: Array.from(new Set(this.index.files.map(f => f.language))).slice(0, 32),
420
+ rationale: operation.rationale,
421
+ },
422
+ provenance: {
423
+ tool: this.name(),
424
+ operation: 'metadata',
425
+ },
426
+ };
427
+ }
428
+ }