@soleri/core 2.7.0 → 2.9.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 (137) hide show
  1. package/dist/extensions/index.d.ts +3 -0
  2. package/dist/extensions/index.d.ts.map +1 -0
  3. package/dist/extensions/index.js +2 -0
  4. package/dist/extensions/index.js.map +1 -0
  5. package/dist/extensions/middleware.d.ts +13 -0
  6. package/dist/extensions/middleware.d.ts.map +1 -0
  7. package/dist/extensions/middleware.js +47 -0
  8. package/dist/extensions/middleware.js.map +1 -0
  9. package/dist/extensions/types.d.ts +64 -0
  10. package/dist/extensions/types.d.ts.map +1 -0
  11. package/dist/extensions/types.js +2 -0
  12. package/dist/extensions/types.js.map +1 -0
  13. package/dist/index.d.ts +8 -16
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -16
  16. package/dist/index.js.map +1 -1
  17. package/dist/planning/gap-analysis.d.ts +2 -1
  18. package/dist/planning/gap-analysis.d.ts.map +1 -1
  19. package/dist/planning/gap-analysis.js +70 -1
  20. package/dist/planning/gap-analysis.js.map +1 -1
  21. package/dist/planning/gap-types.d.ts +8 -3
  22. package/dist/planning/gap-types.d.ts.map +1 -1
  23. package/dist/planning/gap-types.js +9 -1
  24. package/dist/planning/gap-types.js.map +1 -1
  25. package/dist/planning/planner.d.ts.map +1 -1
  26. package/dist/planning/planner.js +17 -5
  27. package/dist/planning/planner.js.map +1 -1
  28. package/dist/runtime/core-ops.d.ts +1 -1
  29. package/dist/runtime/core-ops.js +1 -1
  30. package/dist/runtime/facades/admin-facade.d.ts +8 -0
  31. package/dist/runtime/facades/admin-facade.d.ts.map +1 -0
  32. package/dist/runtime/facades/admin-facade.js +90 -0
  33. package/dist/runtime/facades/admin-facade.js.map +1 -0
  34. package/dist/runtime/facades/brain-facade.d.ts +8 -0
  35. package/dist/runtime/facades/brain-facade.d.ts.map +1 -0
  36. package/dist/runtime/facades/brain-facade.js +294 -0
  37. package/dist/runtime/facades/brain-facade.js.map +1 -0
  38. package/dist/runtime/facades/cognee-facade.d.ts +8 -0
  39. package/dist/runtime/facades/cognee-facade.d.ts.map +1 -0
  40. package/dist/runtime/facades/cognee-facade.js +154 -0
  41. package/dist/runtime/facades/cognee-facade.js.map +1 -0
  42. package/dist/runtime/facades/control-facade.d.ts +8 -0
  43. package/dist/runtime/facades/control-facade.d.ts.map +1 -0
  44. package/dist/runtime/facades/control-facade.js +244 -0
  45. package/dist/runtime/facades/control-facade.js.map +1 -0
  46. package/dist/runtime/facades/curator-facade.d.ts +8 -0
  47. package/dist/runtime/facades/curator-facade.d.ts.map +1 -0
  48. package/dist/runtime/facades/curator-facade.js +117 -0
  49. package/dist/runtime/facades/curator-facade.js.map +1 -0
  50. package/dist/runtime/facades/index.d.ts +10 -0
  51. package/dist/runtime/facades/index.d.ts.map +1 -0
  52. package/dist/runtime/facades/index.js +71 -0
  53. package/dist/runtime/facades/index.js.map +1 -0
  54. package/dist/runtime/facades/loop-facade.d.ts +8 -0
  55. package/dist/runtime/facades/loop-facade.d.ts.map +1 -0
  56. package/dist/runtime/facades/loop-facade.js +9 -0
  57. package/dist/runtime/facades/loop-facade.js.map +1 -0
  58. package/dist/runtime/facades/memory-facade.d.ts +8 -0
  59. package/dist/runtime/facades/memory-facade.d.ts.map +1 -0
  60. package/dist/runtime/facades/memory-facade.js +108 -0
  61. package/dist/runtime/facades/memory-facade.js.map +1 -0
  62. package/dist/runtime/facades/orchestrate-facade.d.ts +8 -0
  63. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -0
  64. package/dist/runtime/facades/orchestrate-facade.js +58 -0
  65. package/dist/runtime/facades/orchestrate-facade.js.map +1 -0
  66. package/dist/runtime/facades/plan-facade.d.ts +8 -0
  67. package/dist/runtime/facades/plan-facade.d.ts.map +1 -0
  68. package/dist/runtime/facades/plan-facade.js +110 -0
  69. package/dist/runtime/facades/plan-facade.js.map +1 -0
  70. package/dist/runtime/facades/vault-facade.d.ts +8 -0
  71. package/dist/runtime/facades/vault-facade.d.ts.map +1 -0
  72. package/dist/runtime/facades/vault-facade.js +194 -0
  73. package/dist/runtime/facades/vault-facade.js.map +1 -0
  74. package/dist/runtime/grading-ops.d.ts +1 -1
  75. package/dist/runtime/grading-ops.js +2 -2
  76. package/dist/runtime/grading-ops.js.map +1 -1
  77. package/dist/runtime/vault-extra-ops.d.ts +2 -2
  78. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  79. package/dist/runtime/vault-extra-ops.js +37 -2
  80. package/dist/runtime/vault-extra-ops.js.map +1 -1
  81. package/dist/streams/index.d.ts +4 -0
  82. package/dist/streams/index.d.ts.map +1 -0
  83. package/dist/streams/index.js +3 -0
  84. package/dist/streams/index.js.map +1 -0
  85. package/dist/streams/normalize.d.ts +14 -0
  86. package/dist/streams/normalize.d.ts.map +1 -0
  87. package/dist/streams/normalize.js +43 -0
  88. package/dist/streams/normalize.js.map +1 -0
  89. package/dist/streams/replayable-stream.d.ts +19 -0
  90. package/dist/streams/replayable-stream.d.ts.map +1 -0
  91. package/dist/streams/replayable-stream.js +90 -0
  92. package/dist/streams/replayable-stream.js.map +1 -0
  93. package/dist/vault/content-hash.d.ts +16 -0
  94. package/dist/vault/content-hash.d.ts.map +1 -0
  95. package/dist/vault/content-hash.js +21 -0
  96. package/dist/vault/content-hash.js.map +1 -0
  97. package/dist/vault/vault.d.ts +9 -0
  98. package/dist/vault/vault.d.ts.map +1 -1
  99. package/dist/vault/vault.js +49 -3
  100. package/dist/vault/vault.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/__tests__/content-hash.test.ts +60 -0
  103. package/src/__tests__/core-ops.test.ts +10 -7
  104. package/src/__tests__/extensions.test.ts +233 -0
  105. package/src/__tests__/grading-ops.test.ts +2 -2
  106. package/src/__tests__/memory-cross-project-ops.test.ts +2 -2
  107. package/src/__tests__/normalize.test.ts +75 -0
  108. package/src/__tests__/playbook.test.ts +4 -4
  109. package/src/__tests__/replayable-stream.test.ts +66 -0
  110. package/src/__tests__/vault-extra-ops.test.ts +1 -1
  111. package/src/__tests__/vault.test.ts +72 -0
  112. package/src/extensions/index.ts +2 -0
  113. package/src/extensions/middleware.ts +53 -0
  114. package/src/extensions/types.ts +64 -0
  115. package/src/index.ts +14 -17
  116. package/src/planning/gap-analysis.ts +95 -1
  117. package/src/planning/gap-types.ts +12 -2
  118. package/src/planning/planner.ts +17 -5
  119. package/src/runtime/facades/admin-facade.ts +101 -0
  120. package/src/runtime/facades/brain-facade.ts +331 -0
  121. package/src/runtime/facades/cognee-facade.ts +162 -0
  122. package/src/runtime/facades/control-facade.ts +279 -0
  123. package/src/runtime/facades/curator-facade.ts +132 -0
  124. package/src/runtime/facades/index.ts +74 -0
  125. package/src/runtime/facades/loop-facade.ts +12 -0
  126. package/src/runtime/facades/memory-facade.ts +114 -0
  127. package/src/runtime/facades/orchestrate-facade.ts +68 -0
  128. package/src/runtime/facades/plan-facade.ts +119 -0
  129. package/src/runtime/facades/vault-facade.ts +223 -0
  130. package/src/runtime/grading-ops.ts +2 -2
  131. package/src/runtime/vault-extra-ops.ts +38 -2
  132. package/src/streams/index.ts +3 -0
  133. package/src/streams/normalize.ts +56 -0
  134. package/src/streams/replayable-stream.ts +92 -0
  135. package/src/vault/content-hash.ts +31 -0
  136. package/src/vault/vault.ts +73 -3
  137. package/src/runtime/core-ops.ts +0 -1443
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/core",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "description": "Shared engine for Soleri agents — vault, brain, planner, LLM utilities, and facade infrastructure.",
5
5
  "keywords": [
6
6
  "agent",
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { computeContentHash } from '../vault/content-hash.js';
3
+
4
+ describe('computeContentHash', () => {
5
+ const base = {
6
+ type: 'pattern',
7
+ domain: 'testing',
8
+ title: 'Test Pattern',
9
+ description: 'A test pattern.',
10
+ tags: ['a', 'b'],
11
+ };
12
+
13
+ it('returns 40-char hex string', () => {
14
+ const hash = computeContentHash(base);
15
+ expect(hash).toMatch(/^[0-9a-f]{40}$/);
16
+ });
17
+
18
+ it('is deterministic', () => {
19
+ expect(computeContentHash(base)).toBe(computeContentHash(base));
20
+ });
21
+
22
+ it('normalizes tag order', () => {
23
+ const a = computeContentHash({ ...base, tags: ['b', 'a'] });
24
+ const b = computeContentHash({ ...base, tags: ['a', 'b'] });
25
+ expect(a).toBe(b);
26
+ });
27
+
28
+ it('normalizes whitespace', () => {
29
+ const a = computeContentHash(base);
30
+ const b = computeContentHash({
31
+ ...base,
32
+ title: ' Test Pattern ',
33
+ description: ' A test pattern. ',
34
+ });
35
+ expect(a).toBe(b);
36
+ });
37
+
38
+ it('normalizes domain case', () => {
39
+ const a = computeContentHash(base);
40
+ const b = computeContentHash({ ...base, domain: 'TESTING' });
41
+ expect(a).toBe(b);
42
+ });
43
+
44
+ it('different content produces different hash', () => {
45
+ const a = computeContentHash(base);
46
+ const b = computeContentHash({ ...base, title: 'Different' });
47
+ expect(a).not.toBe(b);
48
+ });
49
+
50
+ it('handles missing optional fields', () => {
51
+ const hash = computeContentHash({ type: 'rule', domain: 'd', title: 't', description: 'd' });
52
+ expect(hash).toMatch(/^[0-9a-f]{40}$/);
53
+ });
54
+
55
+ it('example and counterExample affect hash', () => {
56
+ const a = computeContentHash({ ...base, example: 'do this' });
57
+ const b = computeContentHash({ ...base, example: 'do that' });
58
+ expect(a).not.toBe(b);
59
+ });
60
+ });
@@ -3,11 +3,11 @@ import { mkdirSync, rmSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { createAgentRuntime } from '../runtime/runtime.js';
6
- import { createCoreOps } from '../runtime/core-ops.js';
6
+ import { createSemanticFacades } from '../runtime/facades/index.js';
7
7
  import type { AgentRuntime } from '../runtime/types.js';
8
8
  import type { OpDefinition } from '../facades/types.js';
9
9
 
10
- describe('createCoreOps', () => {
10
+ describe('createSemanticFacades', () => {
11
11
  let runtime: AgentRuntime;
12
12
  let ops: OpDefinition[];
13
13
  let plannerDir: string;
@@ -20,7 +20,7 @@ describe('createCoreOps', () => {
20
20
  vaultPath: ':memory:',
21
21
  plansPath: join(plannerDir, 'plans.json'),
22
22
  });
23
- ops = createCoreOps(runtime);
23
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
24
24
  });
25
25
 
26
26
  afterEach(() => {
@@ -34,8 +34,8 @@ describe('createCoreOps', () => {
34
34
  return op;
35
35
  }
36
36
 
37
- it('should return 207 ops', () => {
38
- expect(ops.length).toBe(207);
37
+ it('should return 209 ops', () => {
38
+ expect(ops.length).toBe(209);
39
39
  });
40
40
 
41
41
  it('should have all expected op names', () => {
@@ -159,6 +159,9 @@ describe('createCoreOps', () => {
159
159
  expect(names).toContain('vault_archive');
160
160
  expect(names).toContain('vault_restore');
161
161
  expect(names).toContain('vault_optimize');
162
+ // Vault content hashing (#166)
163
+ expect(names).toContain('vault_content_hash');
164
+ expect(names).toContain('vault_dedup_status');
162
165
  // Admin (8)
163
166
  expect(names).toContain('admin_health');
164
167
  expect(names).toContain('admin_tool_list');
@@ -264,7 +267,7 @@ describe('createCoreOps', () => {
264
267
  runtime.brain.rebuildVocabulary();
265
268
 
266
269
  // Re-create ops since brain state changed
267
- ops = createCoreOps(runtime);
270
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
268
271
  const results = (await findOp('search').handler({ query: 'core ops test' })) as unknown[];
269
272
  expect(results.length).toBeGreaterThan(0);
270
273
  });
@@ -371,7 +374,7 @@ describe('createCoreOps', () => {
371
374
  tags: ['test'],
372
375
  },
373
376
  ]);
374
- ops = createCoreOps(runtime);
377
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
375
378
  const result = (await findOp('brain_feedback').handler({
376
379
  query: 'test',
377
380
  entryId: 'bf-1',
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { wrapWithMiddleware } from '../extensions/middleware.js';
3
+ import type { FacadeConfig } from '../facades/types.js';
4
+ import type { OpMiddleware, AgentExtensions } from '../extensions/types.js';
5
+
6
+ describe('extensions', () => {
7
+ describe('AgentExtensions type', () => {
8
+ it('should accept empty extensions', () => {
9
+ const ext: AgentExtensions = {};
10
+ expect(ext.ops).toBeUndefined();
11
+ expect(ext.facades).toBeUndefined();
12
+ expect(ext.middleware).toBeUndefined();
13
+ expect(ext.hooks).toBeUndefined();
14
+ });
15
+
16
+ it('should accept extensions with ops', () => {
17
+ const ext: AgentExtensions = {
18
+ ops: [
19
+ {
20
+ name: 'custom_op',
21
+ description: 'A custom op',
22
+ auth: 'read',
23
+ handler: async () => ({ ok: true }),
24
+ },
25
+ ],
26
+ };
27
+ expect(ext.ops).toHaveLength(1);
28
+ });
29
+
30
+ it('should accept extensions with facades', () => {
31
+ const ext: AgentExtensions = {
32
+ facades: [
33
+ {
34
+ name: 'my_facade',
35
+ description: 'Custom facade',
36
+ ops: [
37
+ {
38
+ name: 'do_thing',
39
+ description: 'Does a thing',
40
+ auth: 'write',
41
+ handler: async () => ({ done: true }),
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ };
47
+ expect(ext.facades).toHaveLength(1);
48
+ });
49
+ });
50
+
51
+ describe('wrapWithMiddleware', () => {
52
+ it('should wrap facade ops with before middleware', async () => {
53
+ const calls: string[] = [];
54
+ const facade: FacadeConfig = {
55
+ name: 'test',
56
+ description: 'Test facade',
57
+ ops: [
58
+ {
59
+ name: 'greet',
60
+ description: 'Say hello',
61
+ auth: 'read',
62
+ handler: async (params) => {
63
+ calls.push('handler');
64
+ return { message: `Hello ${params.name}` };
65
+ },
66
+ },
67
+ ],
68
+ };
69
+
70
+ const mw: OpMiddleware = {
71
+ name: 'logger',
72
+ before: async (ctx) => {
73
+ calls.push(`before:${ctx.op}`);
74
+ return ctx.params;
75
+ },
76
+ };
77
+
78
+ wrapWithMiddleware([facade], [mw]);
79
+ const result = await facade.ops[0].handler({ name: 'World' });
80
+
81
+ expect(calls).toEqual(['before:greet', 'handler']);
82
+ expect(result).toEqual({ message: 'Hello World' });
83
+ });
84
+
85
+ it('should wrap facade ops with after middleware', async () => {
86
+ const facade: FacadeConfig = {
87
+ name: 'test',
88
+ description: 'Test facade',
89
+ ops: [
90
+ {
91
+ name: 'greet',
92
+ description: 'Say hello',
93
+ auth: 'read',
94
+ handler: async () => ({ message: 'Hello' }),
95
+ },
96
+ ],
97
+ };
98
+
99
+ const mw: OpMiddleware = {
100
+ name: 'enricher',
101
+ after: async (ctx) => {
102
+ const data = ctx.result as Record<string, unknown>;
103
+ return { ...data, enriched: true };
104
+ },
105
+ };
106
+
107
+ wrapWithMiddleware([facade], [mw]);
108
+ const result = await facade.ops[0].handler({});
109
+ expect(result).toEqual({ message: 'Hello', enriched: true });
110
+ });
111
+
112
+ it('should chain multiple middleware in order', async () => {
113
+ const order: string[] = [];
114
+ const facade: FacadeConfig = {
115
+ name: 'test',
116
+ description: 'Test',
117
+ ops: [
118
+ {
119
+ name: 'op1',
120
+ description: 'Op',
121
+ auth: 'read',
122
+ handler: async () => {
123
+ order.push('handler');
124
+ return { v: 1 };
125
+ },
126
+ },
127
+ ],
128
+ };
129
+
130
+ const mw1: OpMiddleware = {
131
+ name: 'first',
132
+ before: async (ctx) => {
133
+ order.push('first:before');
134
+ return ctx.params;
135
+ },
136
+ after: async (ctx) => {
137
+ order.push('first:after');
138
+ return ctx.result;
139
+ },
140
+ };
141
+ const mw2: OpMiddleware = {
142
+ name: 'second',
143
+ before: async (ctx) => {
144
+ order.push('second:before');
145
+ return ctx.params;
146
+ },
147
+ after: async (ctx) => {
148
+ order.push('second:after');
149
+ return ctx.result;
150
+ },
151
+ };
152
+
153
+ wrapWithMiddleware([facade], [mw1, mw2]);
154
+ await facade.ops[0].handler({});
155
+
156
+ expect(order).toEqual([
157
+ 'first:before',
158
+ 'second:before',
159
+ 'handler',
160
+ 'second:after',
161
+ 'first:after',
162
+ ]);
163
+ });
164
+
165
+ it('should allow before middleware to modify params', async () => {
166
+ const facade: FacadeConfig = {
167
+ name: 'test',
168
+ description: 'Test',
169
+ ops: [
170
+ {
171
+ name: 'echo',
172
+ description: 'Echo',
173
+ auth: 'read',
174
+ handler: async (params) => params,
175
+ },
176
+ ],
177
+ };
178
+
179
+ const mw: OpMiddleware = {
180
+ name: 'injector',
181
+ before: async (ctx) => ({ ...ctx.params, injected: true }),
182
+ };
183
+
184
+ wrapWithMiddleware([facade], [mw]);
185
+ const result = await facade.ops[0].handler({ original: true });
186
+ expect(result).toEqual({ original: true, injected: true });
187
+ });
188
+
189
+ it('should handle empty middleware array (no-op)', async () => {
190
+ const facade: FacadeConfig = {
191
+ name: 'test',
192
+ description: 'Test',
193
+ ops: [
194
+ {
195
+ name: 'op',
196
+ description: 'Op',
197
+ auth: 'read',
198
+ handler: async () => ({ ok: true }),
199
+ },
200
+ ],
201
+ };
202
+
203
+ wrapWithMiddleware([facade], []);
204
+ const result = await facade.ops[0].handler({});
205
+ expect(result).toEqual({ ok: true });
206
+ });
207
+
208
+ it('should propagate middleware errors', async () => {
209
+ const facade: FacadeConfig = {
210
+ name: 'test',
211
+ description: 'Test',
212
+ ops: [
213
+ {
214
+ name: 'op',
215
+ description: 'Op',
216
+ auth: 'read',
217
+ handler: async () => ({ ok: true }),
218
+ },
219
+ ],
220
+ };
221
+
222
+ const mw: OpMiddleware = {
223
+ name: 'blocker',
224
+ before: async () => {
225
+ throw new Error('Blocked by policy');
226
+ },
227
+ };
228
+
229
+ wrapWithMiddleware([facade], [mw]);
230
+ await expect(facade.ops[0].handler({})).rejects.toThrow('Blocked by policy');
231
+ });
232
+ });
233
+ });
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { createAgentRuntime } from '../runtime/runtime.js';
6
- import { createCoreOps } from '../runtime/core-ops.js';
6
+ import { createSemanticFacades } from '../runtime/facades/index.js';
7
7
  import type { AgentRuntime } from '../runtime/types.js';
8
8
  import type { OpDefinition } from '../facades/types.js';
9
9
 
@@ -20,7 +20,7 @@ describe('Grading Ops', () => {
20
20
  vaultPath: ':memory:',
21
21
  plansPath: join(plannerDir, 'plans.json'),
22
22
  });
23
- ops = createCoreOps(runtime);
23
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
24
24
  });
25
25
 
26
26
  afterEach(() => {
@@ -3,7 +3,7 @@ import { mkdirSync, rmSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { createAgentRuntime } from '../runtime/runtime.js';
6
- import { createCoreOps } from '../runtime/core-ops.js';
6
+ import { createSemanticFacades } from '../runtime/facades/index.js';
7
7
  import type { AgentRuntime } from '../runtime/types.js';
8
8
  import type { OpDefinition } from '../facades/types.js';
9
9
 
@@ -20,7 +20,7 @@ describe('Memory Cross-Project Ops', () => {
20
20
  vaultPath: ':memory:',
21
21
  plansPath: join(tempDir, 'plans.json'),
22
22
  });
23
- ops = createCoreOps(runtime);
23
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
24
24
  });
25
25
 
26
26
  afterEach(() => {
@@ -0,0 +1,75 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { normalize, collect } from '../streams/normalize.js';
3
+
4
+ describe('normalize', () => {
5
+ it('passes through a single value', async () => {
6
+ expect(await collect(normalize(42))).toEqual([42]);
7
+ });
8
+
9
+ it('flattens an array', async () => {
10
+ expect(await collect(normalize([1, 2, 3]))).toEqual([1, 2, 3]);
11
+ });
12
+
13
+ it('resolves a promise', async () => {
14
+ expect(await collect(normalize(Promise.resolve(99)))).toEqual([99]);
15
+ });
16
+
17
+ it('resolves a promise of array', async () => {
18
+ expect(await collect(normalize(Promise.resolve([4, 5])))).toEqual([4, 5]);
19
+ });
20
+
21
+ it('consumes an async iterable', async () => {
22
+ async function* gen() {
23
+ yield 'a';
24
+ yield 'b';
25
+ }
26
+ expect(await collect(normalize(gen()))).toEqual(['a', 'b']);
27
+ });
28
+
29
+ it('flattens nested arrays', async () => {
30
+ const input = [
31
+ [1, 2],
32
+ [3, [4, 5]],
33
+ ];
34
+ expect(await collect(normalize(input))).toEqual([1, 2, 3, 4, 5]);
35
+ });
36
+
37
+ it('handles mixed nesting', async () => {
38
+ async function* gen() {
39
+ yield 30;
40
+ }
41
+ const input = [10, Promise.resolve(20), gen()];
42
+ expect(await collect(normalize(input))).toEqual([10, 20, 30]);
43
+ });
44
+
45
+ it('handles empty array', async () => {
46
+ expect(await collect(normalize([]))).toEqual([]);
47
+ });
48
+
49
+ it('handles string as leaf (not iterable)', async () => {
50
+ expect(await collect(normalize('hello'))).toEqual(['hello']);
51
+ });
52
+
53
+ it('handles deeply nested structure', async () => {
54
+ const input = [[[1]], [[2, [3]]]];
55
+ expect(await collect(normalize(input))).toEqual([1, 2, 3]);
56
+ });
57
+ });
58
+
59
+ describe('collect', () => {
60
+ it('collects async iterable to array', async () => {
61
+ async function* gen() {
62
+ yield 1;
63
+ yield 2;
64
+ yield 3;
65
+ }
66
+ expect(await collect(gen())).toEqual([1, 2, 3]);
67
+ });
68
+
69
+ it('returns empty for empty iterable', async () => {
70
+ async function* gen() {
71
+ /* empty */
72
+ }
73
+ expect(await collect(gen())).toEqual([]);
74
+ });
75
+ });
@@ -7,7 +7,7 @@ import { validatePlaybook, parsePlaybookFromEntry } from '../vault/playbook.js';
7
7
  import type { Playbook } from '../vault/playbook.js';
8
8
  import type { IntelligenceEntry } from '../intelligence/types.js';
9
9
  import { createAgentRuntime } from '../runtime/runtime.js';
10
- import { createCoreOps } from '../runtime/core-ops.js';
10
+ import { createSemanticFacades } from '../runtime/facades/index.js';
11
11
  import type { AgentRuntime, OpDefinition } from '../runtime/types.js';
12
12
 
13
13
  function makePlaybook(overrides: Partial<Playbook> = {}): Playbook {
@@ -189,7 +189,7 @@ describe('playbook_create op', () => {
189
189
  vaultPath: ':memory:',
190
190
  plansPath: join(plannerDir, 'plans.json'),
191
191
  });
192
- ops = createCoreOps(runtime);
192
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
193
193
  });
194
194
 
195
195
  afterEach(() => {
@@ -300,7 +300,7 @@ describe('playbook_match op', () => {
300
300
  vaultPath: ':memory:',
301
301
  plansPath: join(plannerDir, 'plans.json'),
302
302
  });
303
- ops = createCoreOps(runtime);
303
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
304
304
  });
305
305
 
306
306
  afterEach(() => {
@@ -356,7 +356,7 @@ describe('playbook_seed op', () => {
356
356
  vaultPath: ':memory:',
357
357
  plansPath: join(plannerDir, 'plans.json'),
358
358
  });
359
- ops = createCoreOps(runtime);
359
+ ops = createSemanticFacades(runtime, 'test').flatMap(f => f.ops);
360
360
  });
361
361
 
362
362
  afterEach(() => {
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ReplayableStream } from '../streams/replayable-stream.js';
3
+
4
+ async function* generate(items: number[]): AsyncIterable<number> {
5
+ for (const item of items) yield item;
6
+ }
7
+
8
+ let sourceCallCount = 0;
9
+ async function* trackedGenerate(items: number[]): AsyncIterable<number> {
10
+ sourceCallCount++;
11
+ for (const item of items) yield item;
12
+ }
13
+
14
+ describe('ReplayableStream', () => {
15
+ it('single consumer iterates all items', async () => {
16
+ const stream = new ReplayableStream(generate([1, 2, 3]));
17
+ const result = await stream.collect();
18
+ expect(result).toEqual([1, 2, 3]);
19
+ });
20
+
21
+ it('multiple consumers each see full stream', async () => {
22
+ const stream = new ReplayableStream(generate([10, 20, 30]));
23
+ const a = stream.collect();
24
+ const b = stream.collect();
25
+ expect(await a).toEqual([10, 20, 30]);
26
+ expect(await b).toEqual([10, 20, 30]);
27
+ });
28
+
29
+ it('source executes exactly once', async () => {
30
+ sourceCallCount = 0;
31
+ const stream = new ReplayableStream(trackedGenerate([1, 2]));
32
+ await stream.collect();
33
+ await stream.collect();
34
+ await stream.collect();
35
+ expect(sourceCallCount).toBe(1);
36
+ });
37
+
38
+ it('bufferedCount tracks buffer size', async () => {
39
+ const stream = new ReplayableStream(generate([1, 2, 3]));
40
+ expect(stream.bufferedCount).toBe(0);
41
+ await stream.collect();
42
+ expect(stream.bufferedCount).toBe(3);
43
+ });
44
+
45
+ it('empty stream returns empty array', async () => {
46
+ const stream = new ReplayableStream(generate([]));
47
+ expect(await stream.collect()).toEqual([]);
48
+ expect(stream.isDone).toBe(true);
49
+ });
50
+
51
+ it('error propagates to all consumers', async () => {
52
+ async function* failing(): AsyncIterable<number> {
53
+ yield 1;
54
+ throw new Error('source failed');
55
+ }
56
+ const stream = new ReplayableStream(failing());
57
+ await expect(stream.collect()).rejects.toThrow('source failed');
58
+ });
59
+
60
+ it('for-await-of works', async () => {
61
+ const stream = new ReplayableStream(generate([5, 6]));
62
+ const items: number[] = [];
63
+ for await (const item of stream) items.push(item);
64
+ expect(items).toEqual([5, 6]);
65
+ });
66
+ });
@@ -48,7 +48,7 @@ describe('createVaultExtraOps', () => {
48
48
  }
49
49
 
50
50
  it('should return 23 ops', () => {
51
- expect(ops.length).toBe(23);
51
+ expect(ops.length).toBe(25);
52
52
  });
53
53
 
54
54
  it('should have all expected op names', () => {
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
2
  import { Vault } from '../vault/vault.js';
3
+ import { computeContentHash } from '../vault/content-hash.js';
3
4
  import type { IntelligenceEntry } from '../intelligence/types.js';
4
5
 
5
6
  function makeEntry(overrides: Partial<IntelligenceEntry> = {}): IntelligenceEntry {
@@ -675,4 +676,75 @@ describe('Vault', () => {
675
676
  v.close();
676
677
  });
677
678
  });
679
+
680
+ describe('Content-addressable hashing', () => {
681
+ it('seed populates content_hash', () => {
682
+ vault.seed([
683
+ {
684
+ id: 'ch-1',
685
+ type: 'pattern',
686
+ domain: 'test',
687
+ title: 'Hash test',
688
+ severity: 'warning',
689
+ description: 'Desc',
690
+ tags: ['a'],
691
+ },
692
+ ]);
693
+ const hash = computeContentHash({
694
+ type: 'pattern',
695
+ domain: 'test',
696
+ title: 'Hash test',
697
+ description: 'Desc',
698
+ tags: ['a'],
699
+ });
700
+ expect(vault.findByContentHash(hash)).toBe('ch-1');
701
+ });
702
+
703
+ it('findByContentHash returns null for unknown hash', () => {
704
+ expect(vault.findByContentHash('0000000000000000000000000000000000000000')).toBeNull();
705
+ });
706
+
707
+ it('contentHashStats returns correct counts', () => {
708
+ vault.seed([
709
+ {
710
+ id: 'hs-1',
711
+ type: 'pattern',
712
+ domain: 'd',
713
+ title: 'T1',
714
+ severity: 'warning',
715
+ description: 'D1',
716
+ tags: ['a'],
717
+ },
718
+ {
719
+ id: 'hs-2',
720
+ type: 'rule',
721
+ domain: 'd',
722
+ title: 'T2',
723
+ severity: 'warning',
724
+ description: 'D2',
725
+ tags: ['b'],
726
+ },
727
+ ]);
728
+ const stats = vault.contentHashStats();
729
+ expect(stats.total).toBeGreaterThanOrEqual(2);
730
+ expect(stats.hashed).toBe(stats.total);
731
+ expect(stats.uniqueHashes).toBe(stats.total);
732
+ });
733
+
734
+ it('backfill hashes existing entries on re-initialize', () => {
735
+ vault.seed([
736
+ {
737
+ id: 'bf-1',
738
+ type: 'pattern',
739
+ domain: 'd',
740
+ title: 'Backfill',
741
+ severity: 'warning',
742
+ description: 'D',
743
+ tags: [],
744
+ },
745
+ ]);
746
+ const stats = vault.contentHashStats();
747
+ expect(stats.hashed).toBe(stats.total);
748
+ });
749
+ });
678
750
  });
@@ -0,0 +1,2 @@
1
+ export type { AgentExtensions, OpMiddleware, MiddlewareContext } from './types.js';
2
+ export { wrapWithMiddleware } from './middleware.js';