holosphere 1.1.20 → 2.0.0-alpha1

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 (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,446 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { TaskBreakdown } from '../../../src/ai/breakdown.js';
3
+
4
+ describe('Unit: TaskBreakdown', () => {
5
+ let breakdown;
6
+ let mockLLM;
7
+ let mockHolosphere;
8
+
9
+ beforeEach(() => {
10
+ vi.clearAllMocks();
11
+
12
+ mockLLM = {
13
+ getJSON: vi.fn()
14
+ };
15
+
16
+ mockHolosphere = {
17
+ getAll: vi.fn().mockResolvedValue([]),
18
+ getSchema: vi.fn().mockResolvedValue(null),
19
+ write: vi.fn().mockResolvedValue({ success: true }),
20
+ update: vi.fn().mockResolvedValue({ success: true })
21
+ };
22
+
23
+ breakdown = new TaskBreakdown(mockLLM, mockHolosphere);
24
+ });
25
+
26
+ describe('Constructor', () => {
27
+ it('should initialize with LLM service', () => {
28
+ const b = new TaskBreakdown(mockLLM);
29
+ expect(b.llm).toBe(mockLLM);
30
+ expect(b.holosphere).toBeNull();
31
+ });
32
+
33
+ it('should accept optional HoloSphere instance', () => {
34
+ expect(breakdown.holosphere).toBe(mockHolosphere);
35
+ });
36
+ });
37
+
38
+ describe('setHoloSphere', () => {
39
+ it('should set HoloSphere instance', () => {
40
+ const b = new TaskBreakdown(mockLLM);
41
+ b.setHoloSphere(mockHolosphere);
42
+ expect(b.holosphere).toBe(mockHolosphere);
43
+ });
44
+ });
45
+
46
+ describe('breakdown', () => {
47
+ it('should break down item into subtasks', async () => {
48
+ mockLLM.getJSON.mockResolvedValue([
49
+ { id: 'task1-1', title: 'Subtask 1', description: 'First step', parent: 'task1', dependencies: [], status: 'pending' },
50
+ { id: 'task1-2', title: 'Subtask 2', description: 'Second step', parent: 'task1', dependencies: ['task1-1'], status: 'pending' }
51
+ ]);
52
+
53
+ const item = { id: 'task1', title: 'Main Task', description: 'Build something' };
54
+ const result = await breakdown.breakdown(item, 'holon1', 'tasks');
55
+
56
+ expect(result.original).toBe(item);
57
+ expect(result.holonId).toBe('holon1');
58
+ expect(result.lensName).toBe('tasks');
59
+ expect(result.breakdown).toBeDefined();
60
+ expect(result.totalSubtasks).toBeGreaterThan(0);
61
+ });
62
+
63
+ it('should throw error if item has no id', async () => {
64
+ await expect(breakdown.breakdown({ title: 'No ID' }, 'holon', 'lens'))
65
+ .rejects.toThrow('Item must have an id property');
66
+
67
+ await expect(breakdown.breakdown(null, 'holon', 'lens'))
68
+ .rejects.toThrow('Item must have an id property');
69
+ });
70
+
71
+ it('should respect depth option', async () => {
72
+ mockLLM.getJSON.mockResolvedValue([
73
+ { id: 'task1-1', title: 'Subtask', status: 'pending' }
74
+ ]);
75
+
76
+ const result = await breakdown.breakdown(
77
+ { id: 'task1' },
78
+ 'holon',
79
+ 'lens',
80
+ { depth: 1 }
81
+ );
82
+
83
+ expect(result.maxDepth).toBe(1);
84
+ });
85
+
86
+ it('should store subtasks when storeResults is true', async () => {
87
+ mockLLM.getJSON.mockResolvedValue([
88
+ { id: 'task1-1', title: 'Subtask', status: 'pending' }
89
+ ]);
90
+
91
+ await breakdown.breakdown(
92
+ { id: 'task1' },
93
+ 'holon',
94
+ 'lens',
95
+ { storeResults: true, depth: 1 }
96
+ );
97
+
98
+ expect(mockHolosphere.write).toHaveBeenCalled();
99
+ });
100
+
101
+ it('should use context from other items', async () => {
102
+ mockHolosphere.getAll.mockResolvedValue([
103
+ { id: 'other1', title: 'Other Task' },
104
+ { id: 'other2', title: 'Another Task' }
105
+ ]);
106
+
107
+ mockLLM.getJSON.mockResolvedValue([]);
108
+
109
+ await breakdown.breakdown(
110
+ { id: 'task1' },
111
+ 'holon',
112
+ 'lens',
113
+ { useContext: true, depth: 1 }
114
+ );
115
+
116
+ const call = mockLLM.getJSON.mock.calls[0];
117
+ expect(call[0]).toContain('context');
118
+ });
119
+
120
+ it('should normalize stepsPerLevel number to object', async () => {
121
+ mockLLM.getJSON.mockResolvedValue([]);
122
+
123
+ await breakdown.breakdown(
124
+ { id: 'task1' },
125
+ 'holon',
126
+ 'lens',
127
+ { stepsPerLevel: 5, depth: 1 }
128
+ );
129
+
130
+ const call = mockLLM.getJSON.mock.calls[0];
131
+ expect(call[0]).toContain('5');
132
+ });
133
+ });
134
+
135
+ describe('_generateSubtasks', () => {
136
+ it('should generate subtasks with proper structure', async () => {
137
+ mockLLM.getJSON.mockResolvedValue([
138
+ { id: 'p-1', title: 'Step 1', description: 'Do step 1', parent: 'parent', dependencies: [], status: 'pending' },
139
+ { id: 'p-2', title: 'Step 2', description: 'Do step 2', parent: 'parent', dependencies: ['p-1'], status: 'pending' }
140
+ ]);
141
+
142
+ const parent = { id: 'parent', title: 'Parent Task' };
143
+ const result = await breakdown._generateSubtasks(
144
+ parent,
145
+ [],
146
+ null,
147
+ { min: 2, max: 4 },
148
+ 'dependencies',
149
+ 'parent'
150
+ );
151
+
152
+ expect(result).toHaveLength(2);
153
+ expect(result[0].parent).toBe('parent');
154
+ expect(result[0]._meta.generatedFrom).toBe('parent');
155
+ });
156
+
157
+ it('should handle LLM errors gracefully', async () => {
158
+ mockLLM.getJSON.mockRejectedValue(new Error('API Error'));
159
+
160
+ const result = await breakdown._generateSubtasks(
161
+ { id: 'parent' },
162
+ [],
163
+ null,
164
+ { min: 3, max: 5 },
165
+ 'dependencies',
166
+ 'parent'
167
+ );
168
+
169
+ expect(result).toEqual([]);
170
+ });
171
+
172
+ it('should ensure all subtasks have required fields', async () => {
173
+ mockLLM.getJSON.mockResolvedValue([
174
+ { title: 'Missing ID and status' }
175
+ ]);
176
+
177
+ const result = await breakdown._generateSubtasks(
178
+ { id: 'parent' },
179
+ [],
180
+ null,
181
+ { min: 1, max: 1 },
182
+ 'dependencies',
183
+ 'parent'
184
+ );
185
+
186
+ expect(result[0].id).toBeDefined();
187
+ expect(result[0].parent).toBe('parent');
188
+ expect(result[0].status).toBe('pending');
189
+ });
190
+ });
191
+
192
+ describe('_countSubtasks', () => {
193
+ it('should count all subtasks in tree', () => {
194
+ const tree = {
195
+ item: { id: 'root' },
196
+ children: [
197
+ {
198
+ item: { id: 'c1' },
199
+ children: [
200
+ { item: { id: 'c1-1' }, children: [] },
201
+ { item: { id: 'c1-2' }, children: [] }
202
+ ]
203
+ },
204
+ {
205
+ item: { id: 'c2' },
206
+ children: []
207
+ }
208
+ ]
209
+ };
210
+
211
+ expect(breakdown._countSubtasks(tree)).toBe(4);
212
+ });
213
+
214
+ it('should return 0 for node without children', () => {
215
+ const tree = { item: { id: 'leaf' }, children: [] };
216
+ expect(breakdown._countSubtasks(tree)).toBe(0);
217
+ });
218
+ });
219
+
220
+ describe('flatten', () => {
221
+ it('should flatten breakdown tree into list', () => {
222
+ const breakdownResult = {
223
+ breakdown: {
224
+ item: { id: 'root' },
225
+ children: [
226
+ { item: { id: 'c1' }, children: [] },
227
+ { item: { id: 'c2' }, children: [] }
228
+ ]
229
+ }
230
+ };
231
+
232
+ const flat = breakdown.flatten(breakdownResult);
233
+
234
+ expect(flat).toHaveLength(3);
235
+ expect(flat.map(i => i.id)).toContain('root');
236
+ expect(flat.map(i => i.id)).toContain('c1');
237
+ expect(flat.map(i => i.id)).toContain('c2');
238
+ });
239
+
240
+ it('should handle empty breakdown', () => {
241
+ const flat = breakdown.flatten({});
242
+ expect(flat).toEqual([]);
243
+ });
244
+ });
245
+
246
+ describe('getDependencyOrder', () => {
247
+ it('should order items by depth and dependencies', () => {
248
+ const breakdownResult = {
249
+ breakdown: {
250
+ item: { id: 'root', _meta: { depth: 0 } },
251
+ children: [
252
+ { item: { id: 'c1', _meta: { depth: 1 }, dependencies: [] }, children: [] },
253
+ { item: { id: 'c2', _meta: { depth: 1 }, dependencies: ['c1'] }, children: [] }
254
+ ]
255
+ }
256
+ };
257
+
258
+ const ordered = breakdown.getDependencyOrder(breakdownResult);
259
+
260
+ const c1Index = ordered.findIndex(i => i.id === 'c1');
261
+ const c2Index = ordered.findIndex(i => i.id === 'c2');
262
+ expect(c1Index).toBeLessThan(c2Index);
263
+ });
264
+ });
265
+
266
+ describe('suggestStrategy', () => {
267
+ it('should suggest breakdown strategy', async () => {
268
+ mockLLM.getJSON.mockResolvedValue({
269
+ complexity: 'complex',
270
+ recommendedDepth: 3,
271
+ recommendedStepsPerLevel: { min: 3, max: 5 },
272
+ focusAreas: ['Planning', 'Implementation'],
273
+ potentialChallenges: ['Resource constraints'],
274
+ estimatedTotalSubtasks: 15,
275
+ reasoning: 'Complex project needs thorough breakdown'
276
+ });
277
+
278
+ const item = { id: 'project1', title: 'Big Project', description: 'Complex undertaking' };
279
+ const result = await breakdown.suggestStrategy(item);
280
+
281
+ expect(result.complexity).toBe('complex');
282
+ expect(result.recommendedDepth).toBe(3);
283
+ expect(result.focusAreas).toContain('Planning');
284
+ });
285
+ });
286
+
287
+ describe('rebalance', () => {
288
+ it('should rebalance breakdown tree', async () => {
289
+ mockLLM.getJSON.mockResolvedValue({
290
+ tasks: [
291
+ { id: 'new1', title: 'Merged Task' },
292
+ { id: 'new2', title: 'Split Task A' },
293
+ { id: 'new3', title: 'Split Task B' }
294
+ ],
295
+ changes: [
296
+ { type: 'merge', description: 'Merged granular tasks' },
297
+ { type: 'split', description: 'Split large task' }
298
+ ],
299
+ summary: 'Rebalanced for 3-5 items per level'
300
+ });
301
+
302
+ const breakdownResult = {
303
+ breakdown: {
304
+ item: { id: 'root' },
305
+ children: [
306
+ { item: { id: 'c1' }, children: [] }
307
+ ]
308
+ }
309
+ };
310
+
311
+ const result = await breakdown.rebalance(breakdownResult);
312
+
313
+ expect(result.tasks).toHaveLength(3);
314
+ expect(result.changes).toHaveLength(2);
315
+ });
316
+ });
317
+
318
+ describe('getProgress', () => {
319
+ it('should calculate progress on breakdown tree', async () => {
320
+ mockHolosphere.getAll.mockResolvedValue([
321
+ { id: 'root', title: 'Main Task', status: 'in_progress' },
322
+ { id: 'c1', title: 'Subtask 1', parent: 'root', status: 'completed' },
323
+ { id: 'c2', title: 'Subtask 2', parent: 'root', status: 'pending' },
324
+ { id: 'c3', title: 'Subtask 3', parent: 'root', status: 'in_progress', dependencies: ['c1'] }
325
+ ]);
326
+
327
+ const result = await breakdown.getProgress('holon', 'lens', 'root');
328
+
329
+ expect(result.itemId).toBe('root');
330
+ expect(result.total).toBe(3);
331
+ expect(result.completed).toBe(1);
332
+ expect(result.inProgress).toBe(1);
333
+ expect(result.pending).toBe(1);
334
+ expect(result.percentComplete).toBe(33);
335
+ });
336
+
337
+ it('should throw error if HoloSphere not available', async () => {
338
+ const b = new TaskBreakdown(mockLLM);
339
+
340
+ await expect(b.getProgress('holon', 'lens', 'id'))
341
+ .rejects.toThrow('HoloSphere instance required');
342
+ });
343
+
344
+ it('should throw error if item not found', async () => {
345
+ mockHolosphere.getAll.mockResolvedValue([]);
346
+
347
+ await expect(breakdown.getProgress('holon', 'lens', 'nonexistent'))
348
+ .rejects.toThrow('Item nonexistent not found');
349
+ });
350
+
351
+ it('should find blockers', async () => {
352
+ mockHolosphere.getAll.mockResolvedValue([
353
+ { id: 'root', title: 'Main', status: 'pending' },
354
+ { id: 'c1', title: 'Blocker', parent: 'root', status: 'pending' },
355
+ { id: 'c2', title: 'Blocked', parent: 'root', status: 'pending', dependencies: ['c1'] }
356
+ ]);
357
+
358
+ const result = await breakdown.getProgress('holon', 'lens', 'root');
359
+
360
+ expect(result.blockers).toHaveLength(1);
361
+ expect(result.blockers[0].id).toBe('c2');
362
+ });
363
+
364
+ it('should find next tasks ready to work on', async () => {
365
+ mockHolosphere.getAll.mockResolvedValue([
366
+ { id: 'root', title: 'Main', status: 'pending' },
367
+ { id: 'c1', title: 'Ready', parent: 'root', status: 'pending', dependencies: [] },
368
+ { id: 'c2', title: 'Also Ready', parent: 'root', status: 'pending' }
369
+ ]);
370
+
371
+ const result = await breakdown.getProgress('holon', 'lens', 'root');
372
+
373
+ expect(result.nextUp.length).toBeGreaterThan(0);
374
+ expect(result.nextUp.map(t => t.id)).toContain('c1');
375
+ });
376
+ });
377
+
378
+ describe('_findDescendants', () => {
379
+ it('should find all descendants', () => {
380
+ const items = [
381
+ { id: 'c1', parent: 'root' },
382
+ { id: 'c2', parent: 'root' },
383
+ { id: 'c1-1', parent: 'c1' },
384
+ { id: 'other', parent: 'different' }
385
+ ];
386
+
387
+ const descendants = breakdown._findDescendants('root', items);
388
+
389
+ expect(descendants).toHaveLength(3);
390
+ expect(descendants.map(d => d.id)).toContain('c1');
391
+ expect(descendants.map(d => d.id)).toContain('c1-1');
392
+ });
393
+ });
394
+
395
+ describe('_findBlockers', () => {
396
+ it('should find blocked tasks', () => {
397
+ const items = [
398
+ { id: 'c1', status: 'pending' },
399
+ { id: 'c2', status: 'pending', dependencies: ['c1'] }
400
+ ];
401
+
402
+ const blockers = breakdown._findBlockers(items);
403
+
404
+ expect(blockers).toHaveLength(1);
405
+ expect(blockers[0].id).toBe('c2');
406
+ });
407
+
408
+ it('should not include tasks with completed dependencies', () => {
409
+ const items = [
410
+ { id: 'c1', status: 'completed' },
411
+ { id: 'c2', status: 'pending', dependencies: ['c1'] }
412
+ ];
413
+
414
+ const blockers = breakdown._findBlockers(items);
415
+
416
+ expect(blockers).toHaveLength(0);
417
+ });
418
+ });
419
+
420
+ describe('_findNextTasks', () => {
421
+ it('should find tasks ready to start', () => {
422
+ const items = [
423
+ { id: 'c1', status: 'pending' },
424
+ { id: 'c2', status: 'pending', dependencies: ['c1'] }
425
+ ];
426
+
427
+ const next = breakdown._findNextTasks(items);
428
+
429
+ expect(next).toHaveLength(1);
430
+ expect(next[0].id).toBe('c1');
431
+ });
432
+
433
+ it('should exclude completed and in-progress tasks', () => {
434
+ const items = [
435
+ { id: 'c1', status: 'completed' },
436
+ { id: 'c2', status: 'in_progress' },
437
+ { id: 'c3', status: 'pending' }
438
+ ];
439
+
440
+ const next = breakdown._findNextTasks(items);
441
+
442
+ expect(next.map(t => t.id)).not.toContain('c1');
443
+ expect(next.map(t => t.id)).not.toContain('c2');
444
+ });
445
+ });
446
+ });