@soleri/core 2.0.2 → 2.4.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 (226) hide show
  1. package/dist/brain/brain.d.ts +14 -50
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +207 -16
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +86 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -0
  7. package/dist/brain/intelligence.js +771 -0
  8. package/dist/brain/intelligence.js.map +1 -0
  9. package/dist/brain/types.d.ts +197 -0
  10. package/dist/brain/types.d.ts.map +1 -0
  11. package/dist/brain/types.js +2 -0
  12. package/dist/brain/types.js.map +1 -0
  13. package/dist/cognee/client.d.ts +35 -0
  14. package/dist/cognee/client.d.ts.map +1 -0
  15. package/dist/cognee/client.js +291 -0
  16. package/dist/cognee/client.js.map +1 -0
  17. package/dist/cognee/types.d.ts +46 -0
  18. package/dist/cognee/types.d.ts.map +1 -0
  19. package/dist/cognee/types.js +3 -0
  20. package/dist/cognee/types.js.map +1 -0
  21. package/dist/control/identity-manager.d.ts +22 -0
  22. package/dist/control/identity-manager.d.ts.map +1 -0
  23. package/dist/control/identity-manager.js +233 -0
  24. package/dist/control/identity-manager.js.map +1 -0
  25. package/dist/control/intent-router.d.ts +32 -0
  26. package/dist/control/intent-router.d.ts.map +1 -0
  27. package/dist/control/intent-router.js +242 -0
  28. package/dist/control/intent-router.js.map +1 -0
  29. package/dist/control/types.d.ts +68 -0
  30. package/dist/control/types.d.ts.map +1 -0
  31. package/dist/control/types.js +9 -0
  32. package/dist/control/types.js.map +1 -0
  33. package/dist/curator/curator.d.ts +29 -0
  34. package/dist/curator/curator.d.ts.map +1 -1
  35. package/dist/curator/curator.js +142 -5
  36. package/dist/curator/curator.js.map +1 -1
  37. package/dist/facades/types.d.ts +1 -1
  38. package/dist/governance/governance.d.ts +42 -0
  39. package/dist/governance/governance.d.ts.map +1 -0
  40. package/dist/governance/governance.js +488 -0
  41. package/dist/governance/governance.js.map +1 -0
  42. package/dist/governance/index.d.ts +3 -0
  43. package/dist/governance/index.d.ts.map +1 -0
  44. package/dist/governance/index.js +2 -0
  45. package/dist/governance/index.js.map +1 -0
  46. package/dist/governance/types.d.ts +102 -0
  47. package/dist/governance/types.d.ts.map +1 -0
  48. package/dist/governance/types.js +3 -0
  49. package/dist/governance/types.js.map +1 -0
  50. package/dist/index.d.ts +35 -3
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +32 -1
  53. package/dist/index.js.map +1 -1
  54. package/dist/llm/llm-client.d.ts.map +1 -1
  55. package/dist/llm/llm-client.js +9 -2
  56. package/dist/llm/llm-client.js.map +1 -1
  57. package/dist/logging/logger.d.ts +37 -0
  58. package/dist/logging/logger.d.ts.map +1 -0
  59. package/dist/logging/logger.js +145 -0
  60. package/dist/logging/logger.js.map +1 -0
  61. package/dist/logging/types.d.ts +19 -0
  62. package/dist/logging/types.d.ts.map +1 -0
  63. package/dist/logging/types.js +2 -0
  64. package/dist/logging/types.js.map +1 -0
  65. package/dist/loop/loop-manager.d.ts +49 -0
  66. package/dist/loop/loop-manager.d.ts.map +1 -0
  67. package/dist/loop/loop-manager.js +105 -0
  68. package/dist/loop/loop-manager.js.map +1 -0
  69. package/dist/loop/types.d.ts +35 -0
  70. package/dist/loop/types.d.ts.map +1 -0
  71. package/dist/loop/types.js +8 -0
  72. package/dist/loop/types.js.map +1 -0
  73. package/dist/planning/gap-analysis.d.ts +29 -0
  74. package/dist/planning/gap-analysis.d.ts.map +1 -0
  75. package/dist/planning/gap-analysis.js +265 -0
  76. package/dist/planning/gap-analysis.js.map +1 -0
  77. package/dist/planning/gap-types.d.ts +29 -0
  78. package/dist/planning/gap-types.d.ts.map +1 -0
  79. package/dist/planning/gap-types.js +28 -0
  80. package/dist/planning/gap-types.js.map +1 -0
  81. package/dist/planning/planner.d.ts +150 -1
  82. package/dist/planning/planner.d.ts.map +1 -1
  83. package/dist/planning/planner.js +365 -2
  84. package/dist/planning/planner.js.map +1 -1
  85. package/dist/project/project-registry.d.ts +79 -0
  86. package/dist/project/project-registry.d.ts.map +1 -0
  87. package/dist/project/project-registry.js +276 -0
  88. package/dist/project/project-registry.js.map +1 -0
  89. package/dist/project/types.d.ts +28 -0
  90. package/dist/project/types.d.ts.map +1 -0
  91. package/dist/project/types.js +5 -0
  92. package/dist/project/types.js.map +1 -0
  93. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  94. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  95. package/dist/runtime/admin-extra-ops.js +284 -0
  96. package/dist/runtime/admin-extra-ops.js.map +1 -0
  97. package/dist/runtime/admin-ops.d.ts +15 -0
  98. package/dist/runtime/admin-ops.d.ts.map +1 -0
  99. package/dist/runtime/admin-ops.js +322 -0
  100. package/dist/runtime/admin-ops.js.map +1 -0
  101. package/dist/runtime/capture-ops.d.ts +15 -0
  102. package/dist/runtime/capture-ops.d.ts.map +1 -0
  103. package/dist/runtime/capture-ops.js +345 -0
  104. package/dist/runtime/capture-ops.js.map +1 -0
  105. package/dist/runtime/core-ops.d.ts +7 -3
  106. package/dist/runtime/core-ops.d.ts.map +1 -1
  107. package/dist/runtime/core-ops.js +646 -15
  108. package/dist/runtime/core-ops.js.map +1 -1
  109. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  110. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  111. package/dist/runtime/curator-extra-ops.js +59 -0
  112. package/dist/runtime/curator-extra-ops.js.map +1 -0
  113. package/dist/runtime/domain-ops.d.ts.map +1 -1
  114. package/dist/runtime/domain-ops.js +59 -13
  115. package/dist/runtime/domain-ops.js.map +1 -1
  116. package/dist/runtime/grading-ops.d.ts +14 -0
  117. package/dist/runtime/grading-ops.d.ts.map +1 -0
  118. package/dist/runtime/grading-ops.js +105 -0
  119. package/dist/runtime/grading-ops.js.map +1 -0
  120. package/dist/runtime/loop-ops.d.ts +13 -0
  121. package/dist/runtime/loop-ops.d.ts.map +1 -0
  122. package/dist/runtime/loop-ops.js +179 -0
  123. package/dist/runtime/loop-ops.js.map +1 -0
  124. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  125. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  126. package/dist/runtime/memory-cross-project-ops.js +165 -0
  127. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  128. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  129. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  130. package/dist/runtime/memory-extra-ops.js +173 -0
  131. package/dist/runtime/memory-extra-ops.js.map +1 -0
  132. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  133. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  134. package/dist/runtime/orchestrate-ops.js +240 -0
  135. package/dist/runtime/orchestrate-ops.js.map +1 -0
  136. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  137. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/planning-extra-ops.js +300 -0
  139. package/dist/runtime/planning-extra-ops.js.map +1 -0
  140. package/dist/runtime/project-ops.d.ts +15 -0
  141. package/dist/runtime/project-ops.d.ts.map +1 -0
  142. package/dist/runtime/project-ops.js +181 -0
  143. package/dist/runtime/project-ops.js.map +1 -0
  144. package/dist/runtime/runtime.d.ts.map +1 -1
  145. package/dist/runtime/runtime.js +48 -1
  146. package/dist/runtime/runtime.js.map +1 -1
  147. package/dist/runtime/types.d.ts +23 -0
  148. package/dist/runtime/types.d.ts.map +1 -1
  149. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  150. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  151. package/dist/runtime/vault-extra-ops.js +195 -0
  152. package/dist/runtime/vault-extra-ops.js.map +1 -0
  153. package/dist/telemetry/telemetry.d.ts +48 -0
  154. package/dist/telemetry/telemetry.d.ts.map +1 -0
  155. package/dist/telemetry/telemetry.js +87 -0
  156. package/dist/telemetry/telemetry.js.map +1 -0
  157. package/dist/vault/vault.d.ts +94 -0
  158. package/dist/vault/vault.d.ts.map +1 -1
  159. package/dist/vault/vault.js +340 -1
  160. package/dist/vault/vault.js.map +1 -1
  161. package/package.json +1 -1
  162. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  163. package/src/__tests__/admin-ops.test.ts +271 -0
  164. package/src/__tests__/brain-intelligence.test.ts +828 -0
  165. package/src/__tests__/brain.test.ts +396 -27
  166. package/src/__tests__/capture-ops.test.ts +509 -0
  167. package/src/__tests__/cognee-client.test.ts +524 -0
  168. package/src/__tests__/core-ops.test.ts +341 -49
  169. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  170. package/src/__tests__/curator.test.ts +126 -31
  171. package/src/__tests__/domain-ops.test.ts +111 -9
  172. package/src/__tests__/governance.test.ts +522 -0
  173. package/src/__tests__/grading-ops.test.ts +340 -0
  174. package/src/__tests__/identity-manager.test.ts +243 -0
  175. package/src/__tests__/intent-router.test.ts +222 -0
  176. package/src/__tests__/logger.test.ts +200 -0
  177. package/src/__tests__/loop-ops.test.ts +398 -0
  178. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  179. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  180. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  181. package/src/__tests__/planner.test.ts +331 -0
  182. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  183. package/src/__tests__/project-ops.test.ts +367 -0
  184. package/src/__tests__/runtime.test.ts +13 -11
  185. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  186. package/src/brain/brain.ts +308 -72
  187. package/src/brain/intelligence.ts +1230 -0
  188. package/src/brain/types.ts +214 -0
  189. package/src/cognee/client.ts +352 -0
  190. package/src/cognee/types.ts +62 -0
  191. package/src/control/identity-manager.ts +354 -0
  192. package/src/control/intent-router.ts +326 -0
  193. package/src/control/types.ts +102 -0
  194. package/src/curator/curator.ts +265 -15
  195. package/src/governance/governance.ts +698 -0
  196. package/src/governance/index.ts +18 -0
  197. package/src/governance/types.ts +111 -0
  198. package/src/index.ts +128 -3
  199. package/src/llm/llm-client.ts +18 -24
  200. package/src/logging/logger.ts +154 -0
  201. package/src/logging/types.ts +21 -0
  202. package/src/loop/loop-manager.ts +130 -0
  203. package/src/loop/types.ts +44 -0
  204. package/src/planning/gap-analysis.ts +506 -0
  205. package/src/planning/gap-types.ts +58 -0
  206. package/src/planning/planner.ts +478 -2
  207. package/src/project/project-registry.ts +358 -0
  208. package/src/project/types.ts +31 -0
  209. package/src/runtime/admin-extra-ops.ts +307 -0
  210. package/src/runtime/admin-ops.ts +329 -0
  211. package/src/runtime/capture-ops.ts +385 -0
  212. package/src/runtime/core-ops.ts +747 -26
  213. package/src/runtime/curator-extra-ops.ts +71 -0
  214. package/src/runtime/domain-ops.ts +65 -13
  215. package/src/runtime/grading-ops.ts +121 -0
  216. package/src/runtime/loop-ops.ts +194 -0
  217. package/src/runtime/memory-cross-project-ops.ts +192 -0
  218. package/src/runtime/memory-extra-ops.ts +186 -0
  219. package/src/runtime/orchestrate-ops.ts +272 -0
  220. package/src/runtime/planning-extra-ops.ts +327 -0
  221. package/src/runtime/project-ops.ts +196 -0
  222. package/src/runtime/runtime.ts +54 -1
  223. package/src/runtime/types.ts +23 -0
  224. package/src/runtime/vault-extra-ops.ts +225 -0
  225. package/src/telemetry/telemetry.ts +118 -0
  226. package/src/vault/vault.ts +412 -1
@@ -41,12 +41,28 @@ describe('createDomainFacade', () => {
41
41
 
42
42
  it('get_patterns should scope to domain', async () => {
43
43
  runtime.vault.seed([
44
- { id: 'sec-1', type: 'pattern', domain: 'security', title: 'Auth', severity: 'warning', description: 'Auth.', tags: ['auth'] },
45
- { id: 'api-1', type: 'pattern', domain: 'api-design', title: 'REST', severity: 'warning', description: 'REST.', tags: ['rest'] },
44
+ {
45
+ id: 'sec-1',
46
+ type: 'pattern',
47
+ domain: 'security',
48
+ title: 'Auth',
49
+ severity: 'warning',
50
+ description: 'Auth.',
51
+ tags: ['auth'],
52
+ },
53
+ {
54
+ id: 'api-1',
55
+ type: 'pattern',
56
+ domain: 'api-design',
57
+ title: 'REST',
58
+ severity: 'warning',
59
+ description: 'REST.',
60
+ tags: ['rest'],
61
+ },
46
62
  ]);
47
63
  const facade = createDomainFacade(runtime, 'test-domain', 'security');
48
64
  const op = facade.ops.find((o) => o.name === 'get_patterns')!;
49
- const results = await op.handler({}) as IntelligenceEntry[];
65
+ const results = (await op.handler({})) as IntelligenceEntry[];
50
66
  expect(results.every((e) => e.domain === 'security')).toBe(true);
51
67
  });
52
68
 
@@ -68,29 +84,111 @@ describe('createDomainFacade', () => {
68
84
 
69
85
  it('remove should delete entry', async () => {
70
86
  runtime.vault.seed([
71
- { id: 'rm-1', type: 'pattern', domain: 'security', title: 'Remove me', severity: 'warning', description: 'Remove.', tags: ['test'] },
87
+ {
88
+ id: 'rm-1',
89
+ type: 'pattern',
90
+ domain: 'security',
91
+ title: 'Remove me',
92
+ severity: 'warning',
93
+ description: 'Remove.',
94
+ tags: ['test'],
95
+ },
72
96
  ]);
73
97
  const facade = createDomainFacade(runtime, 'test-domain', 'security');
74
98
  const removeOp = facade.ops.find((o) => o.name === 'remove')!;
75
- const result = await removeOp.handler({ id: 'rm-1' }) as { removed: boolean };
99
+ const result = (await removeOp.handler({ id: 'rm-1' })) as { removed: boolean };
76
100
  expect(result.removed).toBe(true);
77
101
  expect(runtime.vault.get('rm-1')).toBeNull();
78
102
  });
79
103
 
104
+ it('capture should include governance action on default (moderate) preset', async () => {
105
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
106
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
107
+ const result = (await captureOp.handler({
108
+ id: 'gov-cap-1',
109
+ type: 'pattern',
110
+ title: 'Governed Pattern',
111
+ severity: 'warning',
112
+ description: 'Test governance capture.',
113
+ tags: ['gov'],
114
+ })) as { captured: boolean; governance: { action: string } };
115
+ expect(result.captured).toBe(true);
116
+ expect(result.governance.action).toBe('capture');
117
+ expect(runtime.vault.get('gov-cap-1')).not.toBeNull();
118
+ });
119
+
120
+ it('capture should create proposal under strict preset', async () => {
121
+ runtime.governance.applyPreset('.', 'strict', 'test');
122
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
123
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
124
+ const result = (await captureOp.handler({
125
+ id: 'gov-prop-1',
126
+ type: 'pattern',
127
+ title: 'Needs Review',
128
+ severity: 'warning',
129
+ description: 'Should be proposed.',
130
+ tags: ['gov'],
131
+ })) as {
132
+ captured: boolean;
133
+ governance: { action: string; proposalId: number; reason?: string };
134
+ };
135
+ expect(result.captured).toBe(false);
136
+ expect(result.governance.action).toBe('propose');
137
+ expect(result.governance.proposalId).toBeGreaterThan(0);
138
+ // Entry should NOT be in vault
139
+ expect(runtime.vault.get('gov-prop-1')).toBeNull();
140
+ });
141
+
142
+ it('capture should reject when total quota exceeded', async () => {
143
+ runtime.governance.setPolicy('.', 'quota', { maxEntriesTotal: 1 }, 'test');
144
+ runtime.vault.seed([
145
+ {
146
+ id: 'existing-1',
147
+ type: 'pattern',
148
+ domain: 'security',
149
+ title: 'Existing',
150
+ severity: 'warning',
151
+ description: 'Takes the slot.',
152
+ tags: ['fill'],
153
+ },
154
+ ]);
155
+ const facade = createDomainFacade(runtime, 'test-domain', 'security');
156
+ const captureOp = facade.ops.find((o) => o.name === 'capture')!;
157
+ const result = (await captureOp.handler({
158
+ id: 'gov-rej-1',
159
+ type: 'pattern',
160
+ title: 'Over Quota',
161
+ severity: 'warning',
162
+ description: 'Should be rejected.',
163
+ tags: ['gov'],
164
+ })) as { captured: boolean; governance: { action: string; reason?: string } };
165
+ expect(result.captured).toBe(false);
166
+ expect(result.governance.action).toBe('reject');
167
+ expect(runtime.vault.get('gov-rej-1')).toBeNull();
168
+ });
169
+
80
170
  it('get_entry should return specific entry', async () => {
81
171
  runtime.vault.seed([
82
- { id: 'ge-1', type: 'pattern', domain: 'security', title: 'Get me', severity: 'warning', description: 'Get.', tags: ['test'] },
172
+ {
173
+ id: 'ge-1',
174
+ type: 'pattern',
175
+ domain: 'security',
176
+ title: 'Get me',
177
+ severity: 'warning',
178
+ description: 'Get.',
179
+ tags: ['test'],
180
+ },
83
181
  ]);
84
182
  const facade = createDomainFacade(runtime, 'test-domain', 'security');
85
183
  const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
86
- const result = await getOp.handler({ id: 'ge-1' }) as IntelligenceEntry;
184
+ const result = (await getOp.handler({ id: 'ge-1' })) as IntelligenceEntry;
87
185
  expect(result.id).toBe('ge-1');
88
186
  });
89
187
 
90
188
  it('get_entry should return error for missing entry', async () => {
91
189
  const facade = createDomainFacade(runtime, 'test-domain', 'security');
92
190
  const getOp = facade.ops.find((o) => o.name === 'get_entry')!;
93
- const result = await getOp.handler({ id: 'nope' }) as { error: string };
191
+ const result = (await getOp.handler({ id: 'nope' })) as { error: string };
94
192
  expect(result.error).toBeDefined();
95
193
  });
96
194
  });
@@ -110,7 +208,11 @@ describe('createDomainFacades', () => {
110
208
  });
111
209
 
112
210
  it('should create one facade per domain', () => {
113
- const facades = createDomainFacades(runtime, 'test-multi', ['security', 'api-design', 'testing']);
211
+ const facades = createDomainFacades(runtime, 'test-multi', [
212
+ 'security',
213
+ 'api-design',
214
+ 'testing',
215
+ ]);
114
216
  expect(facades.length).toBe(3);
115
217
  expect(facades[0].name).toBe('test-multi_security');
116
218
  expect(facades[1].name).toBe('test-multi_api_design');
@@ -0,0 +1,522 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { mkdirSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { createAgentRuntime } from '../runtime/runtime.js';
6
+ import type { AgentRuntime } from '../runtime/types.js';
7
+
8
+ describe('Governance', () => {
9
+ let runtime: AgentRuntime;
10
+ let plannerDir: string;
11
+
12
+ beforeEach(() => {
13
+ plannerDir = join(tmpdir(), 'governance-test-' + Date.now());
14
+ mkdirSync(plannerDir, { recursive: true });
15
+ runtime = createAgentRuntime({
16
+ agentId: 'test-governance',
17
+ vaultPath: ':memory:',
18
+ plansPath: join(plannerDir, 'plans.json'),
19
+ });
20
+ });
21
+
22
+ afterEach(() => {
23
+ runtime.close();
24
+ rmSync(plannerDir, { recursive: true, force: true });
25
+ });
26
+
27
+ // ─── Policy CRUD ──────────────────────────────────────────────────
28
+
29
+ describe('Policy CRUD', () => {
30
+ it('should return moderate defaults for unknown project', () => {
31
+ const policy = runtime.governance.getPolicy('/unknown');
32
+ expect(policy.projectPath).toBe('/unknown');
33
+ expect(policy.quotas.maxEntriesTotal).toBe(500);
34
+ expect(policy.retention.archiveAfterDays).toBe(90);
35
+ expect(policy.autoCapture.enabled).toBe(true);
36
+ expect(policy.autoCapture.requireReview).toBe(false);
37
+ });
38
+
39
+ it('should persist setPolicy changes', () => {
40
+ runtime.governance.setPolicy('/test', 'quota', {
41
+ maxEntriesTotal: 100,
42
+ maxEntriesPerCategory: 30,
43
+ maxEntriesPerType: 50,
44
+ warnAtPercent: 75,
45
+ });
46
+ const policy = runtime.governance.getPolicy('/test');
47
+ expect(policy.quotas.maxEntriesTotal).toBe(100);
48
+ expect(policy.quotas.warnAtPercent).toBe(75);
49
+ // Other policies should remain default
50
+ expect(policy.retention.archiveAfterDays).toBe(90);
51
+ });
52
+
53
+ it('should applyPreset for all 3 policy types', () => {
54
+ runtime.governance.applyPreset('/test', 'strict', 'admin');
55
+ const policy = runtime.governance.getPolicy('/test');
56
+ expect(policy.quotas.maxEntriesTotal).toBe(200);
57
+ expect(policy.retention.archiveAfterDays).toBe(30);
58
+ expect(policy.autoCapture.requireReview).toBe(true);
59
+ expect(policy.autoCapture.maxPendingProposals).toBe(10);
60
+ });
61
+ });
62
+
63
+ // ─── Evaluation Cascade ───────────────────────────────────────────
64
+
65
+ describe('Evaluation Cascade', () => {
66
+ it('should reject when auto-capture is disabled', () => {
67
+ runtime.governance.setPolicy('/test', 'auto-capture', {
68
+ enabled: false,
69
+ requireReview: false,
70
+ maxPendingProposals: 25,
71
+ autoExpireDays: 14,
72
+ });
73
+ const decision = runtime.governance.evaluateCapture('/test', {
74
+ type: 'pattern',
75
+ category: 'testing',
76
+ });
77
+ expect(decision.allowed).toBe(false);
78
+ expect(decision.action).toBe('reject');
79
+ expect(decision.reason).toContain('disabled');
80
+ });
81
+
82
+ it('should propose when review is required', () => {
83
+ runtime.governance.applyPreset('/test', 'strict'); // requireReview: true
84
+ const decision = runtime.governance.evaluateCapture('/test', {
85
+ type: 'pattern',
86
+ category: 'testing',
87
+ });
88
+ expect(decision.allowed).toBe(false);
89
+ expect(decision.action).toBe('propose');
90
+ });
91
+
92
+ it('should reject when total quota exceeded', () => {
93
+ // Set very low quota
94
+ runtime.governance.setPolicy('/test', 'quota', {
95
+ maxEntriesTotal: 1,
96
+ maxEntriesPerCategory: 100,
97
+ maxEntriesPerType: 100,
98
+ warnAtPercent: 80,
99
+ });
100
+ // Seed entries to exceed quota
101
+ runtime.vault.seed([
102
+ {
103
+ id: 'q1',
104
+ type: 'pattern',
105
+ domain: 'testing',
106
+ title: 'Test',
107
+ severity: 'warning',
108
+ description: 'Test',
109
+ tags: ['t'],
110
+ },
111
+ ]);
112
+ const decision = runtime.governance.evaluateCapture('/test', {
113
+ type: 'pattern',
114
+ category: 'testing',
115
+ });
116
+ expect(decision.allowed).toBe(false);
117
+ expect(decision.action).toBe('reject');
118
+ expect(decision.reason).toContain('Total quota exceeded');
119
+ });
120
+
121
+ it('should quarantine when category quota exceeded', () => {
122
+ runtime.governance.setPolicy('/test', 'quota', {
123
+ maxEntriesTotal: 1000,
124
+ maxEntriesPerCategory: 1,
125
+ maxEntriesPerType: 1000,
126
+ warnAtPercent: 80,
127
+ });
128
+ runtime.vault.seed([
129
+ {
130
+ id: 'cq1',
131
+ type: 'pattern',
132
+ domain: 'testing',
133
+ title: 'Test',
134
+ severity: 'warning',
135
+ description: 'Test',
136
+ tags: ['t'],
137
+ },
138
+ ]);
139
+ const decision = runtime.governance.evaluateCapture('/test', {
140
+ type: 'pattern',
141
+ category: 'testing',
142
+ });
143
+ expect(decision.allowed).toBe(false);
144
+ expect(decision.action).toBe('quarantine');
145
+ });
146
+
147
+ it('should quarantine when type quota exceeded', () => {
148
+ runtime.governance.setPolicy('/test', 'quota', {
149
+ maxEntriesTotal: 1000,
150
+ maxEntriesPerCategory: 1000,
151
+ maxEntriesPerType: 1,
152
+ warnAtPercent: 80,
153
+ });
154
+ runtime.vault.seed([
155
+ {
156
+ id: 'tq1',
157
+ type: 'pattern',
158
+ domain: 'testing',
159
+ title: 'Test',
160
+ severity: 'warning',
161
+ description: 'Test',
162
+ tags: ['t'],
163
+ },
164
+ ]);
165
+ const decision = runtime.governance.evaluateCapture('/test', {
166
+ type: 'pattern',
167
+ category: 'testing',
168
+ });
169
+ expect(decision.allowed).toBe(false);
170
+ expect(decision.action).toBe('quarantine');
171
+ });
172
+
173
+ it('should allow capture when within all quotas', () => {
174
+ const decision = runtime.governance.evaluateCapture('/test', {
175
+ type: 'pattern',
176
+ category: 'testing',
177
+ title: 'A good pattern',
178
+ });
179
+ expect(decision.allowed).toBe(true);
180
+ expect(decision.action).toBe('capture');
181
+ });
182
+ });
183
+
184
+ // ─── Batch Evaluation ─────────────────────────────────────────────
185
+
186
+ describe('Batch Evaluation', () => {
187
+ it('should evaluate multiple entries with running state', () => {
188
+ const results = runtime.governance.evaluateBatch('/test', [
189
+ { type: 'pattern', category: 'a' },
190
+ { type: 'anti-pattern', category: 'b' },
191
+ { type: 'rule', category: 'c' },
192
+ ]);
193
+ expect(results).toHaveLength(3);
194
+ expect(results.every((r) => r.decision.action === 'capture')).toBe(true);
195
+ });
196
+ });
197
+
198
+ // ─── Proposal Lifecycle ───────────────────────────────────────────
199
+
200
+ describe('Proposal Lifecycle', () => {
201
+ it('should create and approve a proposal', () => {
202
+ const id = runtime.governance.propose('/test', {
203
+ title: 'New pattern',
204
+ type: 'pattern',
205
+ category: 'testing',
206
+ data: { description: 'A discovered pattern' },
207
+ });
208
+ expect(id).toBeGreaterThan(0);
209
+
210
+ const approved = runtime.governance.approveProposal(id, 'admin');
211
+ expect(approved).not.toBeNull();
212
+ expect(approved!.status).toBe('approved');
213
+ expect(approved!.decidedBy).toBe('admin');
214
+ });
215
+
216
+ it('should auto-capture entry into vault on approval', () => {
217
+ const id = runtime.governance.propose('/test', {
218
+ entryId: 'approved-entry-1',
219
+ title: 'Approved pattern',
220
+ type: 'pattern',
221
+ category: 'testing',
222
+ data: {
223
+ severity: 'warning',
224
+ description: 'A pattern that was reviewed and approved.',
225
+ tags: ['governance', 'approved'],
226
+ },
227
+ });
228
+
229
+ // Before approval — not in vault
230
+ expect(runtime.vault.get('approved-entry-1')).toBeNull();
231
+
232
+ runtime.governance.approveProposal(id, 'admin');
233
+
234
+ // After approval — entry is in vault
235
+ const entry = runtime.vault.get('approved-entry-1');
236
+ expect(entry).not.toBeNull();
237
+ expect(entry!.domain).toBe('testing');
238
+ expect(entry!.title).toBe('Approved pattern');
239
+ expect(entry!.tags).toContain('governance');
240
+ });
241
+
242
+ it('should generate entry id from proposal id when entryId is missing', () => {
243
+ const id = runtime.governance.propose('/test', {
244
+ title: 'No entry id',
245
+ type: 'rule',
246
+ category: 'styling',
247
+ data: { severity: 'suggestion', description: 'Auto-id test.' },
248
+ });
249
+
250
+ runtime.governance.approveProposal(id);
251
+
252
+ const entry = runtime.vault.get(`proposal-${id}`);
253
+ expect(entry).not.toBeNull();
254
+ expect(entry!.type).toBe('rule');
255
+ expect(entry!.domain).toBe('styling');
256
+ });
257
+
258
+ it('should reject a proposal with a note', () => {
259
+ const id = runtime.governance.propose('/test', {
260
+ title: 'Bad pattern',
261
+ type: 'pattern',
262
+ category: 'testing',
263
+ });
264
+
265
+ const rejected = runtime.governance.rejectProposal(id, 'admin', 'Not useful');
266
+ expect(rejected).not.toBeNull();
267
+ expect(rejected!.status).toBe('rejected');
268
+ expect(rejected!.modificationNote).toBe('Not useful');
269
+ });
270
+
271
+ it('should modify a proposal and mark as modified', () => {
272
+ const id = runtime.governance.propose('/test', {
273
+ title: 'Draft pattern',
274
+ type: 'pattern',
275
+ category: 'testing',
276
+ data: { description: 'Original' },
277
+ });
278
+
279
+ const modified = runtime.governance.modifyProposal(
280
+ id,
281
+ { description: 'Updated description', severity: 'critical' },
282
+ 'editor',
283
+ );
284
+ expect(modified).not.toBeNull();
285
+ expect(modified!.status).toBe('modified');
286
+ expect(modified!.proposedData.description).toBe('Updated description');
287
+ expect(modified!.proposedData.severity).toBe('critical');
288
+ });
289
+
290
+ it('should auto-capture into vault on modify with merged data', () => {
291
+ const id = runtime.governance.propose('/test', {
292
+ entryId: 'mod-cap-1',
293
+ title: 'Modify me',
294
+ type: 'pattern',
295
+ category: 'testing',
296
+ data: { severity: 'warning', description: 'Original desc', tags: ['test'] },
297
+ });
298
+
299
+ // Before modify — not in vault
300
+ expect(runtime.vault.get('mod-cap-1')).toBeNull();
301
+
302
+ runtime.governance.modifyProposal(id, { description: 'Improved desc' }, 'editor');
303
+
304
+ // After modify — captured with merged data
305
+ const entry = runtime.vault.get('mod-cap-1');
306
+ expect(entry).not.toBeNull();
307
+ expect(entry!.description).toBe('Improved desc');
308
+ expect(entry!.domain).toBe('testing');
309
+ });
310
+
311
+ it('should return null when approving nonexistent proposal', () => {
312
+ const result = runtime.governance.approveProposal(999);
313
+ expect(result).toBeNull();
314
+ });
315
+
316
+ it('should not allow double-approval', () => {
317
+ const id = runtime.governance.propose('/test', {
318
+ title: 'Test',
319
+ type: 'pattern',
320
+ category: 'testing',
321
+ });
322
+ runtime.governance.approveProposal(id);
323
+ const second = runtime.governance.approveProposal(id);
324
+ expect(second).toBeNull();
325
+ });
326
+
327
+ it('should list pending proposals', () => {
328
+ runtime.governance.propose('/test', { title: 'P1', type: 'pattern', category: 'a' });
329
+ runtime.governance.propose('/test', { title: 'P2', type: 'rule', category: 'b' });
330
+ runtime.governance.propose('/other', { title: 'P3', type: 'pattern', category: 'c' });
331
+
332
+ const all = runtime.governance.listPendingProposals();
333
+ expect(all).toHaveLength(3);
334
+
335
+ const testOnly = runtime.governance.listPendingProposals('/test');
336
+ expect(testOnly).toHaveLength(2);
337
+ });
338
+ });
339
+
340
+ // ─── Proposal Stats ──────────────────────────────────────────────
341
+
342
+ describe('Proposal Stats', () => {
343
+ it('should compute counts and acceptance rate', () => {
344
+ const id1 = runtime.governance.propose('/test', {
345
+ title: 'P1',
346
+ type: 'pattern',
347
+ category: 'a',
348
+ });
349
+ const id2 = runtime.governance.propose('/test', {
350
+ title: 'P2',
351
+ type: 'pattern',
352
+ category: 'a',
353
+ });
354
+ runtime.governance.propose('/test', { title: 'P3', type: 'rule', category: 'b' });
355
+
356
+ runtime.governance.approveProposal(id1);
357
+ runtime.governance.rejectProposal(id2);
358
+ // id3 remains pending
359
+
360
+ const stats = runtime.governance.getProposalStats('/test');
361
+ expect(stats.total).toBe(3);
362
+ expect(stats.approved).toBe(1);
363
+ expect(stats.rejected).toBe(1);
364
+ expect(stats.pending).toBe(1);
365
+ expect(stats.acceptanceRate).toBe(0.5); // 1 approved / 2 decided
366
+ });
367
+
368
+ it('should compute byCategory breakdown', () => {
369
+ const id1 = runtime.governance.propose('/test', {
370
+ title: 'P1',
371
+ type: 'pattern',
372
+ category: 'styling',
373
+ });
374
+ const id2 = runtime.governance.propose('/test', {
375
+ title: 'P2',
376
+ type: 'pattern',
377
+ category: 'styling',
378
+ });
379
+ runtime.governance.approveProposal(id1);
380
+ runtime.governance.rejectProposal(id2);
381
+
382
+ const stats = runtime.governance.getProposalStats('/test');
383
+ expect(stats.byCategory.styling).toBeDefined();
384
+ expect(stats.byCategory.styling.total).toBe(2);
385
+ expect(stats.byCategory.styling.accepted).toBe(1);
386
+ expect(stats.byCategory.styling.rate).toBe(0.5);
387
+ });
388
+ });
389
+
390
+ // ─── Audit Trail ──────────────────────────────────────────────────
391
+
392
+ describe('Audit Trail', () => {
393
+ it('should log policy changes', () => {
394
+ runtime.governance.setPolicy(
395
+ '/test',
396
+ 'quota',
397
+ {
398
+ maxEntriesTotal: 100,
399
+ maxEntriesPerCategory: 30,
400
+ maxEntriesPerType: 50,
401
+ warnAtPercent: 75,
402
+ },
403
+ 'admin',
404
+ );
405
+
406
+ const trail = runtime.governance.getAuditTrail('/test');
407
+ expect(trail.length).toBeGreaterThan(0);
408
+ expect(trail[0].policyType).toBe('quota');
409
+ expect(trail[0].changedBy).toBe('admin');
410
+ expect(trail[0].oldConfig).toBeNull(); // First set, no previous
411
+ expect(trail[0].newConfig).toHaveProperty('maxEntriesTotal', 100);
412
+ });
413
+
414
+ it('should record old config on policy update', () => {
415
+ runtime.governance.setPolicy('/test', 'quota', { maxEntriesTotal: 100 } as Record<
416
+ string,
417
+ unknown
418
+ >);
419
+ runtime.governance.setPolicy('/test', 'quota', { maxEntriesTotal: 200 } as Record<
420
+ string,
421
+ unknown
422
+ >);
423
+
424
+ const trail = runtime.governance.getAuditTrail('/test');
425
+ expect(trail.length).toBe(2);
426
+ // Both entries present — find the second change (the one with oldConfig)
427
+ const updateEntry = trail.find((t) => t.oldConfig !== null);
428
+ expect(updateEntry).toBeDefined();
429
+ expect(updateEntry!.newConfig).toHaveProperty('maxEntriesTotal', 200);
430
+ expect(updateEntry!.oldConfig).toHaveProperty('maxEntriesTotal', 100);
431
+ });
432
+ });
433
+
434
+ // ─── Dashboard ────────────────────────────────────────────────────
435
+
436
+ describe('Dashboard', () => {
437
+ it('should return combined health view', () => {
438
+ const dashboard = runtime.governance.getDashboard('/test');
439
+ expect(dashboard.vaultSize).toBe(0);
440
+ expect(dashboard.quotaPercent).toBe(0);
441
+ expect(dashboard.pendingProposals).toBe(0);
442
+ expect(dashboard.policySummary.maxEntries).toBe(500);
443
+ expect(dashboard.policySummary.requireReview).toBe(false);
444
+ expect(typeof dashboard.acceptanceRate).toBe('number');
445
+ expect(typeof dashboard.evaluationTrend).toBe('object');
446
+ });
447
+
448
+ it('should reflect vault entries in quota percent', () => {
449
+ // Set low quota for easy percentage
450
+ runtime.governance.setPolicy('/test', 'quota', {
451
+ maxEntriesTotal: 10,
452
+ maxEntriesPerCategory: 100,
453
+ maxEntriesPerType: 100,
454
+ warnAtPercent: 80,
455
+ });
456
+ runtime.vault.seed([
457
+ {
458
+ id: 'dq1',
459
+ type: 'pattern',
460
+ domain: 'd',
461
+ title: 'T',
462
+ severity: 'warning',
463
+ description: 'D',
464
+ tags: ['t'],
465
+ },
466
+ {
467
+ id: 'dq2',
468
+ type: 'pattern',
469
+ domain: 'd',
470
+ title: 'T',
471
+ severity: 'warning',
472
+ description: 'D',
473
+ tags: ['t'],
474
+ },
475
+ {
476
+ id: 'dq3',
477
+ type: 'pattern',
478
+ domain: 'd',
479
+ title: 'T',
480
+ severity: 'warning',
481
+ description: 'D',
482
+ tags: ['t'],
483
+ },
484
+ ]);
485
+
486
+ const dashboard = runtime.governance.getDashboard('/test');
487
+ expect(dashboard.vaultSize).toBe(3);
488
+ expect(dashboard.quotaPercent).toBe(30);
489
+ });
490
+ });
491
+
492
+ // ─── Edge Cases ───────────────────────────────────────────────────
493
+
494
+ describe('Edge Cases', () => {
495
+ it('should handle empty vault gracefully', () => {
496
+ const status = runtime.governance.getQuotaStatus('/empty');
497
+ expect(status.total).toBe(0);
498
+ expect(status.isWarning).toBe(false);
499
+ });
500
+
501
+ it('should handle unknown project defaults', () => {
502
+ const policy = runtime.governance.getPolicy('/nonexistent');
503
+ expect(policy.quotas.maxEntriesTotal).toBe(500);
504
+ });
505
+
506
+ it('should return null for approving nonexistent proposal', () => {
507
+ expect(runtime.governance.approveProposal(9999)).toBeNull();
508
+ });
509
+
510
+ it('should return 0 expired when no stale proposals', () => {
511
+ const expired = runtime.governance.expireStaleProposals(1);
512
+ expect(expired).toBe(0);
513
+ });
514
+
515
+ it('should return empty stats when no proposals exist', () => {
516
+ const stats = runtime.governance.getProposalStats('/test');
517
+ expect(stats.total).toBe(0);
518
+ expect(stats.acceptanceRate).toBe(0);
519
+ expect(Object.keys(stats.byCategory)).toHaveLength(0);
520
+ });
521
+ });
522
+ });