@cleocode/core 2026.4.30 → 2026.4.31

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 (89) hide show
  1. package/dist/bootstrap.d.ts +35 -0
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/code/index.d.ts +8 -4
  4. package/dist/code/index.d.ts.map +1 -1
  5. package/dist/code/parser.d.ts +22 -9
  6. package/dist/code/parser.d.ts.map +1 -1
  7. package/dist/hooks/handlers/session-hooks.d.ts +11 -4
  8. package/dist/hooks/handlers/session-hooks.d.ts.map +1 -1
  9. package/dist/hooks/payload-schemas.d.ts +6 -6
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +3859 -3008
  13. package/dist/index.js.map +4 -4
  14. package/dist/internal.d.ts +10 -7
  15. package/dist/internal.d.ts.map +1 -1
  16. package/dist/lib/tree-sitter-languages.d.ts +11 -7
  17. package/dist/lib/tree-sitter-languages.d.ts.map +1 -1
  18. package/dist/memory/auto-extract.d.ts +27 -15
  19. package/dist/memory/auto-extract.d.ts.map +1 -1
  20. package/dist/memory/brain-backfill.d.ts +59 -0
  21. package/dist/memory/brain-backfill.d.ts.map +1 -0
  22. package/dist/memory/brain-purge.d.ts +51 -0
  23. package/dist/memory/brain-purge.d.ts.map +1 -0
  24. package/dist/memory/brain-retrieval.d.ts.map +1 -1
  25. package/dist/memory/brain-search.d.ts.map +1 -1
  26. package/dist/memory/decisions.d.ts.map +1 -1
  27. package/dist/memory/engine-compat.d.ts +71 -0
  28. package/dist/memory/engine-compat.d.ts.map +1 -1
  29. package/dist/memory/graph-auto-populate.d.ts +65 -0
  30. package/dist/memory/graph-auto-populate.d.ts.map +1 -0
  31. package/dist/memory/graph-queries.d.ts +127 -0
  32. package/dist/memory/graph-queries.d.ts.map +1 -0
  33. package/dist/memory/learnings.d.ts +2 -0
  34. package/dist/memory/learnings.d.ts.map +1 -1
  35. package/dist/memory/patterns.d.ts +2 -0
  36. package/dist/memory/patterns.d.ts.map +1 -1
  37. package/dist/memory/quality-scoring.d.ts +90 -0
  38. package/dist/memory/quality-scoring.d.ts.map +1 -0
  39. package/dist/sessions/session-memory-bridge.d.ts +16 -10
  40. package/dist/sessions/session-memory-bridge.d.ts.map +1 -1
  41. package/dist/store/brain-accessor.d.ts +7 -0
  42. package/dist/store/brain-accessor.d.ts.map +1 -1
  43. package/dist/store/brain-schema.d.ts +185 -11
  44. package/dist/store/brain-schema.d.ts.map +1 -1
  45. package/dist/store/brain-sqlite.d.ts.map +1 -1
  46. package/dist/store/nexus-schema.d.ts +480 -2
  47. package/dist/store/nexus-schema.d.ts.map +1 -1
  48. package/dist/store/tasks-schema.d.ts +9 -9
  49. package/dist/store/validation-schemas.d.ts +44 -28
  50. package/dist/store/validation-schemas.d.ts.map +1 -1
  51. package/dist/system/dependencies.d.ts +43 -0
  52. package/dist/system/dependencies.d.ts.map +1 -0
  53. package/dist/system/health.d.ts +3 -0
  54. package/dist/system/health.d.ts.map +1 -1
  55. package/dist/tasks/complete.d.ts.map +1 -1
  56. package/package.json +19 -19
  57. package/src/bootstrap.ts +124 -0
  58. package/src/code/index.ts +20 -4
  59. package/src/code/parser.ts +310 -110
  60. package/src/hooks/handlers/__tests__/hook-automation-e2e.test.ts +19 -45
  61. package/src/hooks/handlers/__tests__/session-hooks.test.ts +42 -54
  62. package/src/hooks/handlers/session-hooks.ts +11 -33
  63. package/src/index.ts +14 -0
  64. package/src/internal.ts +37 -7
  65. package/src/lib/tree-sitter-languages.ts +11 -7
  66. package/src/memory/__tests__/auto-extract.test.ts +20 -82
  67. package/src/memory/__tests__/embedding-pipeline.test.ts +389 -0
  68. package/src/memory/auto-extract.ts +34 -120
  69. package/src/memory/brain-backfill.ts +471 -0
  70. package/src/memory/brain-purge.ts +315 -0
  71. package/src/memory/brain-retrieval.ts +43 -2
  72. package/src/memory/brain-search.ts +23 -6
  73. package/src/memory/decisions.ts +76 -3
  74. package/src/memory/engine-compat.ts +168 -0
  75. package/src/memory/graph-auto-populate.ts +173 -0
  76. package/src/memory/graph-queries.ts +424 -0
  77. package/src/memory/learnings.ts +55 -7
  78. package/src/memory/patterns.ts +66 -13
  79. package/src/memory/quality-scoring.ts +173 -0
  80. package/src/sessions/__tests__/session-memory-bridge.test.ts +27 -49
  81. package/src/sessions/session-memory-bridge.ts +19 -47
  82. package/src/store/__tests__/brain-accessor-pageindex.test.ts +93 -22
  83. package/src/store/brain-accessor.ts +48 -2
  84. package/src/store/brain-schema.ts +165 -13
  85. package/src/store/brain-sqlite.ts +35 -0
  86. package/src/store/nexus-schema.ts +257 -3
  87. package/src/system/dependencies.ts +534 -0
  88. package/src/system/health.ts +126 -22
  89. package/src/tasks/complete.ts +40 -0
@@ -0,0 +1,471 @@
1
+ /**
2
+ * Brain graph back-fill — populates brain_page_nodes and brain_page_edges from
3
+ * existing typed table rows (decisions, patterns, learnings, observations,
4
+ * sticky notes).
5
+ *
6
+ * Each row in a typed table gets a corresponding node in brain_page_nodes.
7
+ * Relationship edges are derived from:
8
+ * - decision.contextTaskId / contextEpicId → applies_to edges
9
+ * - observation.sourceSessionId → produced_by edge
10
+ * - observation text referencing task IDs → applies_to edges
11
+ * - pattern entries → derived_from stubs
12
+ *
13
+ * Stub nodes (task:<id>, session:<id>, epic:<id>) are created for referenced
14
+ * external entities so edges have valid targets.
15
+ *
16
+ * Duplicate nodes are silently skipped (INSERT OR IGNORE semantics via
17
+ * Drizzle onConflictDoNothing).
18
+ *
19
+ * @task T530
20
+ * @epic T523
21
+ */
22
+
23
+ import { createHash } from 'node:crypto';
24
+ import { getBrainAccessor } from '../store/brain-accessor.js';
25
+ import type {
26
+ BrainDecisionRow,
27
+ BrainLearningRow,
28
+ BrainObservationRow,
29
+ BrainPatternRow,
30
+ NewBrainPageEdgeRow,
31
+ NewBrainPageNodeRow,
32
+ } from '../store/brain-schema.js';
33
+ import * as brainSchema from '../store/brain-schema.js';
34
+ import { getBrainDb } from '../store/brain-sqlite.js';
35
+
36
+ // ============================================================================
37
+ // Types
38
+ // ============================================================================
39
+
40
+ /** Result returned by backfillBrainGraph. */
41
+ export interface BrainBackfillResult {
42
+ /** Counts before the back-fill ran. */
43
+ before: {
44
+ nodes: number;
45
+ edges: number;
46
+ decisions: number;
47
+ patterns: number;
48
+ learnings: number;
49
+ observations: number;
50
+ stickyNotes: number;
51
+ };
52
+ /** Counts after the back-fill ran. */
53
+ after: {
54
+ nodes: number;
55
+ edges: number;
56
+ };
57
+ /** Number of nodes inserted during this run. */
58
+ nodesInserted: number;
59
+ /** Number of edges inserted during this run. */
60
+ edgesInserted: number;
61
+ /** Number of stub nodes created for external references (tasks, sessions, epics). */
62
+ stubsCreated: number;
63
+ /** Node counts broken down by type. */
64
+ byType: Record<string, number>;
65
+ }
66
+
67
+ // ============================================================================
68
+ // Helpers
69
+ // ============================================================================
70
+
71
+ /**
72
+ * Return the SHA-256 prefix (first 16 hex chars) of the given content string.
73
+ * Normalises to lowercase and trims before hashing to improve dedup accuracy.
74
+ */
75
+ function sha256prefix(content: string): string {
76
+ return createHash('sha256').update(content.trim().toLowerCase()).digest('hex').substring(0, 16);
77
+ }
78
+
79
+ /**
80
+ * Compute quality score for a decision row.
81
+ * High confidence → 0.9, medium → 0.7, low → 0.5.
82
+ */
83
+ function computeDecisionQuality(decision: BrainDecisionRow): number {
84
+ switch (decision.confidence) {
85
+ case 'high':
86
+ return 0.9;
87
+ case 'medium':
88
+ return 0.7;
89
+ default:
90
+ return 0.5;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Compute quality score for a pattern row.
96
+ * Composite: base 0.4 + frequency factor + success-rate factor.
97
+ * Capped at 0.9 to reserve 1.0 for canonical external references.
98
+ */
99
+ function computePatternQuality(pattern: BrainPatternRow): number {
100
+ const freqFactor = Math.min(0.3, (pattern.frequency ?? 1) * 0.05);
101
+ const successFactor = (pattern.successRate ?? 0) * 0.3;
102
+ return Math.min(0.9, 0.4 + freqFactor + successFactor);
103
+ }
104
+
105
+ /**
106
+ * Compute quality score for a learning row.
107
+ * Maps the stored 0.0–1.0 confidence directly to quality, capped at 0.9.
108
+ */
109
+ function computeLearningQuality(learning: BrainLearningRow): number {
110
+ return Math.min(0.9, learning.confidence ?? 0.5);
111
+ }
112
+
113
+ /**
114
+ * Compute quality score for an observation row.
115
+ * Manual entries are highest quality (0.8), agent-generated 0.7, others 0.5.
116
+ */
117
+ function computeObservationQuality(observation: BrainObservationRow): number {
118
+ switch (observation.sourceType) {
119
+ case 'manual':
120
+ return 0.8;
121
+ case 'agent':
122
+ case 'session-debrief':
123
+ return 0.7;
124
+ default:
125
+ return 0.5;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Extract task IDs referenced in a block of text.
131
+ * Matches T followed by 3–6 digits (e.g. T530, T5160).
132
+ */
133
+ function extractTaskRefs(text: string): string[] {
134
+ const matches = text.match(/\bT\d{3,6}\b/g);
135
+ if (!matches) return [];
136
+ return [...new Set(matches)];
137
+ }
138
+
139
+ // ============================================================================
140
+ // Core back-fill function
141
+ // ============================================================================
142
+
143
+ /**
144
+ * Back-fill brain_page_nodes and brain_page_edges from all existing typed rows
145
+ * in brain.db.
146
+ *
147
+ * Safe to run multiple times — duplicate nodes and edges are silently ignored
148
+ * via INSERT OR IGNORE semantics.
149
+ *
150
+ * @param projectRoot - Absolute path to the project root (contains .cleo/).
151
+ * @returns BackfillResult with before/after counts and insertion stats.
152
+ */
153
+ export async function backfillBrainGraph(projectRoot: string): Promise<BrainBackfillResult> {
154
+ const db = await getBrainDb(projectRoot);
155
+ const accessor = await getBrainAccessor(projectRoot);
156
+
157
+ // ── Before counts ────────────────────────────────────────────────────────
158
+ const [beforeNodes, beforeEdges] = await Promise.all([
159
+ db.select({ count: brainSchema.brainPageNodes.id }).from(brainSchema.brainPageNodes),
160
+ db.select({ count: brainSchema.brainPageEdges.fromId }).from(brainSchema.brainPageEdges),
161
+ ]);
162
+
163
+ const [decisions, patterns, learnings, observations, stickyNotes] = await Promise.all([
164
+ accessor.findDecisions(),
165
+ accessor.findPatterns(),
166
+ accessor.findLearnings(),
167
+ accessor.findObservations(),
168
+ accessor.findStickyNotes(),
169
+ ]);
170
+
171
+ const beforeNodeCount = beforeNodes.length;
172
+ const beforeEdgeCount = beforeEdges.length;
173
+
174
+ // ── Tracking state ────────────────────────────────────────────────────────
175
+ let nodesInserted = 0;
176
+ let edgesInserted = 0;
177
+ let stubsCreated = 0;
178
+
179
+ // Track stub nodes we've already created to avoid duplicates
180
+ const createdStubs = new Set<string>();
181
+
182
+ // Accumulate pending inserts for batch efficiency
183
+ const pendingNodes: NewBrainPageNodeRow[] = [];
184
+ const pendingEdges: NewBrainPageEdgeRow[] = [];
185
+
186
+ const now = new Date().toISOString().replace('T', ' ').slice(0, 19);
187
+
188
+ /** Ensure a stub node exists for an external reference (task, session, epic). */
189
+ function scheduleStub(
190
+ nodeId: string,
191
+ nodeType: 'task' | 'session' | 'epic',
192
+ label: string,
193
+ ): void {
194
+ if (createdStubs.has(nodeId)) return;
195
+ createdStubs.add(nodeId);
196
+ pendingNodes.push({
197
+ id: nodeId,
198
+ nodeType,
199
+ label: label.substring(0, 200),
200
+ qualityScore: 1.0,
201
+ contentHash: null,
202
+ lastActivityAt: now,
203
+ metadataJson: null,
204
+ createdAt: now,
205
+ updatedAt: null,
206
+ });
207
+ }
208
+
209
+ /** Schedule an edge insert (deduped by fromId+toId+edgeType within this run). */
210
+ const edgeSet = new Set<string>();
211
+ function scheduleEdge(edge: NewBrainPageEdgeRow): void {
212
+ const key = `${edge.fromId}|${edge.toId}|${edge.edgeType}`;
213
+ if (edgeSet.has(key)) return;
214
+ edgeSet.add(key);
215
+ pendingEdges.push(edge);
216
+ }
217
+
218
+ // ── 1. Decisions ─────────────────────────────────────────────────────────
219
+ const byType: Record<string, number> = {};
220
+
221
+ for (const decision of decisions) {
222
+ const nodeId = `decision:${decision.id}`;
223
+ pendingNodes.push({
224
+ id: nodeId,
225
+ nodeType: 'decision',
226
+ label: decision.decision.substring(0, 200),
227
+ qualityScore: computeDecisionQuality(decision),
228
+ contentHash: sha256prefix(decision.decision + (decision.rationale ?? '')),
229
+ lastActivityAt: decision.updatedAt ?? decision.createdAt ?? now,
230
+ metadataJson: JSON.stringify({
231
+ type: decision.type,
232
+ confidence: decision.confidence,
233
+ outcome: decision.outcome,
234
+ }),
235
+ createdAt: decision.createdAt ?? now,
236
+ updatedAt: decision.updatedAt ?? null,
237
+ });
238
+ byType['decision'] = (byType['decision'] ?? 0) + 1;
239
+
240
+ // Decision → task applies_to edge
241
+ if (decision.contextTaskId) {
242
+ const taskNodeId = `task:${decision.contextTaskId}`;
243
+ scheduleStub(taskNodeId, 'task', decision.contextTaskId);
244
+ scheduleEdge({
245
+ fromId: nodeId,
246
+ toId: taskNodeId,
247
+ edgeType: 'applies_to',
248
+ weight: 1.0,
249
+ provenance: 'backfill:decision.contextTaskId',
250
+ createdAt: now,
251
+ });
252
+ }
253
+
254
+ // Decision → epic applies_to edge
255
+ if (decision.contextEpicId) {
256
+ const epicNodeId = `epic:${decision.contextEpicId}`;
257
+ scheduleStub(epicNodeId, 'epic', decision.contextEpicId);
258
+ scheduleEdge({
259
+ fromId: nodeId,
260
+ toId: epicNodeId,
261
+ edgeType: 'applies_to',
262
+ weight: 0.9,
263
+ provenance: 'backfill:decision.contextEpicId',
264
+ createdAt: now,
265
+ });
266
+ }
267
+ }
268
+
269
+ // ── 2. Patterns ───────────────────────────────────────────────────────────
270
+ for (const pattern of patterns) {
271
+ const nodeId = `pattern:${pattern.id}`;
272
+ pendingNodes.push({
273
+ id: nodeId,
274
+ nodeType: 'pattern',
275
+ label: pattern.pattern.substring(0, 200),
276
+ qualityScore: computePatternQuality(pattern),
277
+ contentHash: sha256prefix(pattern.pattern),
278
+ lastActivityAt: pattern.updatedAt ?? pattern.extractedAt ?? now,
279
+ metadataJson: JSON.stringify({
280
+ type: pattern.type,
281
+ frequency: pattern.frequency,
282
+ impact: pattern.impact,
283
+ }),
284
+ createdAt: pattern.extractedAt ?? now,
285
+ updatedAt: pattern.updatedAt ?? null,
286
+ });
287
+ byType['pattern'] = (byType['pattern'] ?? 0) + 1;
288
+
289
+ // Patterns referencing tasks in their context field → derived_from edge
290
+ if (pattern.context) {
291
+ const taskRefs = extractTaskRefs(pattern.context);
292
+ for (const taskId of taskRefs) {
293
+ const taskNodeId = `task:${taskId}`;
294
+ scheduleStub(taskNodeId, 'task', taskId);
295
+ scheduleEdge({
296
+ fromId: nodeId,
297
+ toId: taskNodeId,
298
+ edgeType: 'derived_from',
299
+ weight: 0.7,
300
+ provenance: 'backfill:pattern.context-task-ref',
301
+ createdAt: now,
302
+ });
303
+ }
304
+ }
305
+ }
306
+
307
+ // ── 3. Learnings ──────────────────────────────────────────────────────────
308
+ for (const learning of learnings) {
309
+ const nodeId = `learning:${learning.id}`;
310
+ pendingNodes.push({
311
+ id: nodeId,
312
+ nodeType: 'learning',
313
+ label: learning.insight.substring(0, 200),
314
+ qualityScore: computeLearningQuality(learning),
315
+ contentHash: sha256prefix(learning.insight + (learning.source ?? '')),
316
+ lastActivityAt: learning.updatedAt ?? learning.createdAt ?? now,
317
+ metadataJson: JSON.stringify({
318
+ confidence: learning.confidence,
319
+ actionable: learning.actionable,
320
+ source: learning.source,
321
+ }),
322
+ createdAt: learning.createdAt ?? now,
323
+ updatedAt: learning.updatedAt ?? null,
324
+ });
325
+ byType['learning'] = (byType['learning'] ?? 0) + 1;
326
+ }
327
+
328
+ // ── 4. Observations ───────────────────────────────────────────────────────
329
+ for (const observation of observations) {
330
+ const nodeId = `observation:${observation.id}`;
331
+ const labelSource =
332
+ observation.title || observation.narrative?.substring(0, 200) || 'Untitled observation';
333
+ pendingNodes.push({
334
+ id: nodeId,
335
+ nodeType: 'observation',
336
+ label: labelSource.substring(0, 200),
337
+ qualityScore: computeObservationQuality(observation),
338
+ contentHash:
339
+ observation.contentHash ?? sha256prefix(observation.narrative ?? observation.title ?? ''),
340
+ lastActivityAt: observation.updatedAt ?? observation.createdAt ?? now,
341
+ metadataJson: JSON.stringify({
342
+ sourceType: observation.sourceType,
343
+ agent: observation.agent,
344
+ sessionId: observation.sourceSessionId,
345
+ }),
346
+ createdAt: observation.createdAt ?? now,
347
+ updatedAt: observation.updatedAt ?? null,
348
+ });
349
+ byType['observation'] = (byType['observation'] ?? 0) + 1;
350
+
351
+ // Observation → session produced_by edge
352
+ if (observation.sourceSessionId) {
353
+ const sessionNodeId = `session:${observation.sourceSessionId}`;
354
+ scheduleStub(
355
+ sessionNodeId,
356
+ 'session',
357
+ `Session ${observation.sourceSessionId.substring(0, 30)}`,
358
+ );
359
+ scheduleEdge({
360
+ fromId: nodeId,
361
+ toId: sessionNodeId,
362
+ edgeType: 'produced_by',
363
+ weight: 1.0,
364
+ provenance: 'backfill:observation.sourceSessionId',
365
+ createdAt: now,
366
+ });
367
+ }
368
+
369
+ // Observation text → task applies_to edges
370
+ const fullText = [observation.title, observation.subtitle, observation.narrative]
371
+ .filter(Boolean)
372
+ .join(' ');
373
+ const taskRefs = extractTaskRefs(fullText);
374
+ for (const taskId of taskRefs) {
375
+ const taskNodeId = `task:${taskId}`;
376
+ scheduleStub(taskNodeId, 'task', taskId);
377
+ scheduleEdge({
378
+ fromId: nodeId,
379
+ toId: taskNodeId,
380
+ edgeType: 'applies_to',
381
+ weight: 0.8,
382
+ provenance: 'backfill:observation.text-task-ref',
383
+ createdAt: now,
384
+ });
385
+ }
386
+ }
387
+
388
+ // ── 5. Sticky Notes ───────────────────────────────────────────────────────
389
+ for (const sticky of stickyNotes) {
390
+ const nodeId = `sticky:${sticky.id}`;
391
+ const labelSource = sticky.content?.substring(0, 200) ?? 'Untitled sticky';
392
+ pendingNodes.push({
393
+ id: nodeId,
394
+ nodeType: 'sticky',
395
+ label: labelSource.substring(0, 200),
396
+ qualityScore: 0.6,
397
+ contentHash: sha256prefix(sticky.content ?? ''),
398
+ lastActivityAt: sticky.updatedAt ?? sticky.createdAt ?? now,
399
+ metadataJson: JSON.stringify({
400
+ status: sticky.status,
401
+ priority: sticky.priority,
402
+ color: sticky.color,
403
+ }),
404
+ createdAt: sticky.createdAt ?? now,
405
+ updatedAt: sticky.updatedAt ?? null,
406
+ });
407
+ byType['sticky'] = (byType['sticky'] ?? 0) + 1;
408
+
409
+ // Sticky notes with task refs → applies_to edges
410
+ if (sticky.content) {
411
+ const taskRefs = extractTaskRefs(sticky.content);
412
+ for (const taskId of taskRefs) {
413
+ const taskNodeId = `task:${taskId}`;
414
+ scheduleStub(taskNodeId, 'task', taskId);
415
+ scheduleEdge({
416
+ fromId: nodeId,
417
+ toId: taskNodeId,
418
+ edgeType: 'applies_to',
419
+ weight: 0.7,
420
+ provenance: 'backfill:sticky.content-task-ref',
421
+ createdAt: now,
422
+ });
423
+ }
424
+ }
425
+ }
426
+
427
+ // ── Flush nodes (INSERT OR IGNORE) ─────────────────────────────────────────
428
+ const BATCH_SIZE = 50;
429
+
430
+ for (let i = 0; i < pendingNodes.length; i += BATCH_SIZE) {
431
+ const batch = pendingNodes.slice(i, i + BATCH_SIZE);
432
+ await db.insert(brainSchema.brainPageNodes).values(batch).onConflictDoNothing();
433
+ nodesInserted += batch.length;
434
+ }
435
+
436
+ // Stub nodes count
437
+ stubsCreated = createdStubs.size;
438
+
439
+ // ── Flush edges (INSERT OR IGNORE) ─────────────────────────────────────────
440
+ for (let i = 0; i < pendingEdges.length; i += BATCH_SIZE) {
441
+ const batch = pendingEdges.slice(i, i + BATCH_SIZE);
442
+ await db.insert(brainSchema.brainPageEdges).values(batch).onConflictDoNothing();
443
+ edgesInserted += batch.length;
444
+ }
445
+
446
+ // ── After counts ──────────────────────────────────────────────────────────
447
+ const [afterNodes, afterEdges] = await Promise.all([
448
+ db.select({ id: brainSchema.brainPageNodes.id }).from(brainSchema.brainPageNodes),
449
+ db.select({ fromId: brainSchema.brainPageEdges.fromId }).from(brainSchema.brainPageEdges),
450
+ ]);
451
+
452
+ return {
453
+ before: {
454
+ nodes: beforeNodeCount,
455
+ edges: beforeEdgeCount,
456
+ decisions: decisions.length,
457
+ patterns: patterns.length,
458
+ learnings: learnings.length,
459
+ observations: observations.length,
460
+ stickyNotes: stickyNotes.length,
461
+ },
462
+ after: {
463
+ nodes: afterNodes.length,
464
+ edges: afterEdges.length,
465
+ },
466
+ nodesInserted,
467
+ edgesInserted,
468
+ stubsCreated,
469
+ byType,
470
+ };
471
+ }