@soleri/core 2.1.0 → 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 (207) hide show
  1. package/dist/brain/brain.d.ts +3 -1
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +60 -4
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +36 -1
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +119 -14
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +32 -0
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/control/identity-manager.d.ts +22 -0
  12. package/dist/control/identity-manager.d.ts.map +1 -0
  13. package/dist/control/identity-manager.js +233 -0
  14. package/dist/control/identity-manager.js.map +1 -0
  15. package/dist/control/intent-router.d.ts +32 -0
  16. package/dist/control/intent-router.d.ts.map +1 -0
  17. package/dist/control/intent-router.js +242 -0
  18. package/dist/control/intent-router.js.map +1 -0
  19. package/dist/control/types.d.ts +68 -0
  20. package/dist/control/types.d.ts.map +1 -0
  21. package/dist/control/types.js +9 -0
  22. package/dist/control/types.js.map +1 -0
  23. package/dist/curator/curator.d.ts +29 -0
  24. package/dist/curator/curator.d.ts.map +1 -1
  25. package/dist/curator/curator.js +135 -0
  26. package/dist/curator/curator.js.map +1 -1
  27. package/dist/facades/types.d.ts +1 -1
  28. package/dist/governance/governance.d.ts +42 -0
  29. package/dist/governance/governance.d.ts.map +1 -0
  30. package/dist/governance/governance.js +488 -0
  31. package/dist/governance/governance.js.map +1 -0
  32. package/dist/governance/index.d.ts +3 -0
  33. package/dist/governance/index.d.ts.map +1 -0
  34. package/dist/governance/index.js +2 -0
  35. package/dist/governance/index.js.map +1 -0
  36. package/dist/governance/types.d.ts +102 -0
  37. package/dist/governance/types.d.ts.map +1 -0
  38. package/dist/governance/types.js +3 -0
  39. package/dist/governance/types.js.map +1 -0
  40. package/dist/index.d.ts +32 -3
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +29 -1
  43. package/dist/index.js.map +1 -1
  44. package/dist/logging/logger.d.ts +37 -0
  45. package/dist/logging/logger.d.ts.map +1 -0
  46. package/dist/logging/logger.js +145 -0
  47. package/dist/logging/logger.js.map +1 -0
  48. package/dist/logging/types.d.ts +19 -0
  49. package/dist/logging/types.d.ts.map +1 -0
  50. package/dist/logging/types.js +2 -0
  51. package/dist/logging/types.js.map +1 -0
  52. package/dist/loop/loop-manager.d.ts +49 -0
  53. package/dist/loop/loop-manager.d.ts.map +1 -0
  54. package/dist/loop/loop-manager.js +105 -0
  55. package/dist/loop/loop-manager.js.map +1 -0
  56. package/dist/loop/types.d.ts +35 -0
  57. package/dist/loop/types.d.ts.map +1 -0
  58. package/dist/loop/types.js +8 -0
  59. package/dist/loop/types.js.map +1 -0
  60. package/dist/planning/gap-analysis.d.ts +29 -0
  61. package/dist/planning/gap-analysis.d.ts.map +1 -0
  62. package/dist/planning/gap-analysis.js +265 -0
  63. package/dist/planning/gap-analysis.js.map +1 -0
  64. package/dist/planning/gap-types.d.ts +29 -0
  65. package/dist/planning/gap-types.d.ts.map +1 -0
  66. package/dist/planning/gap-types.js +28 -0
  67. package/dist/planning/gap-types.js.map +1 -0
  68. package/dist/planning/planner.d.ts +150 -1
  69. package/dist/planning/planner.d.ts.map +1 -1
  70. package/dist/planning/planner.js +365 -2
  71. package/dist/planning/planner.js.map +1 -1
  72. package/dist/project/project-registry.d.ts +79 -0
  73. package/dist/project/project-registry.d.ts.map +1 -0
  74. package/dist/project/project-registry.js +276 -0
  75. package/dist/project/project-registry.js.map +1 -0
  76. package/dist/project/types.d.ts +28 -0
  77. package/dist/project/types.d.ts.map +1 -0
  78. package/dist/project/types.js +5 -0
  79. package/dist/project/types.js.map +1 -0
  80. package/dist/runtime/admin-extra-ops.d.ts +13 -0
  81. package/dist/runtime/admin-extra-ops.d.ts.map +1 -0
  82. package/dist/runtime/admin-extra-ops.js +284 -0
  83. package/dist/runtime/admin-extra-ops.js.map +1 -0
  84. package/dist/runtime/admin-ops.d.ts +15 -0
  85. package/dist/runtime/admin-ops.d.ts.map +1 -0
  86. package/dist/runtime/admin-ops.js +322 -0
  87. package/dist/runtime/admin-ops.js.map +1 -0
  88. package/dist/runtime/capture-ops.d.ts +15 -0
  89. package/dist/runtime/capture-ops.d.ts.map +1 -0
  90. package/dist/runtime/capture-ops.js +345 -0
  91. package/dist/runtime/capture-ops.js.map +1 -0
  92. package/dist/runtime/core-ops.d.ts +7 -3
  93. package/dist/runtime/core-ops.d.ts.map +1 -1
  94. package/dist/runtime/core-ops.js +474 -8
  95. package/dist/runtime/core-ops.js.map +1 -1
  96. package/dist/runtime/curator-extra-ops.d.ts +9 -0
  97. package/dist/runtime/curator-extra-ops.d.ts.map +1 -0
  98. package/dist/runtime/curator-extra-ops.js +59 -0
  99. package/dist/runtime/curator-extra-ops.js.map +1 -0
  100. package/dist/runtime/domain-ops.d.ts.map +1 -1
  101. package/dist/runtime/domain-ops.js +59 -13
  102. package/dist/runtime/domain-ops.js.map +1 -1
  103. package/dist/runtime/grading-ops.d.ts +14 -0
  104. package/dist/runtime/grading-ops.d.ts.map +1 -0
  105. package/dist/runtime/grading-ops.js +105 -0
  106. package/dist/runtime/grading-ops.js.map +1 -0
  107. package/dist/runtime/loop-ops.d.ts +13 -0
  108. package/dist/runtime/loop-ops.d.ts.map +1 -0
  109. package/dist/runtime/loop-ops.js +179 -0
  110. package/dist/runtime/loop-ops.js.map +1 -0
  111. package/dist/runtime/memory-cross-project-ops.d.ts +12 -0
  112. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -0
  113. package/dist/runtime/memory-cross-project-ops.js +165 -0
  114. package/dist/runtime/memory-cross-project-ops.js.map +1 -0
  115. package/dist/runtime/memory-extra-ops.d.ts +13 -0
  116. package/dist/runtime/memory-extra-ops.d.ts.map +1 -0
  117. package/dist/runtime/memory-extra-ops.js +173 -0
  118. package/dist/runtime/memory-extra-ops.js.map +1 -0
  119. package/dist/runtime/orchestrate-ops.d.ts +17 -0
  120. package/dist/runtime/orchestrate-ops.d.ts.map +1 -0
  121. package/dist/runtime/orchestrate-ops.js +240 -0
  122. package/dist/runtime/orchestrate-ops.js.map +1 -0
  123. package/dist/runtime/planning-extra-ops.d.ts +17 -0
  124. package/dist/runtime/planning-extra-ops.d.ts.map +1 -0
  125. package/dist/runtime/planning-extra-ops.js +300 -0
  126. package/dist/runtime/planning-extra-ops.js.map +1 -0
  127. package/dist/runtime/project-ops.d.ts +15 -0
  128. package/dist/runtime/project-ops.d.ts.map +1 -0
  129. package/dist/runtime/project-ops.js +181 -0
  130. package/dist/runtime/project-ops.js.map +1 -0
  131. package/dist/runtime/runtime.d.ts.map +1 -1
  132. package/dist/runtime/runtime.js +44 -1
  133. package/dist/runtime/runtime.js.map +1 -1
  134. package/dist/runtime/types.d.ts +21 -0
  135. package/dist/runtime/types.d.ts.map +1 -1
  136. package/dist/runtime/vault-extra-ops.d.ts +9 -0
  137. package/dist/runtime/vault-extra-ops.d.ts.map +1 -0
  138. package/dist/runtime/vault-extra-ops.js +195 -0
  139. package/dist/runtime/vault-extra-ops.js.map +1 -0
  140. package/dist/telemetry/telemetry.d.ts +48 -0
  141. package/dist/telemetry/telemetry.d.ts.map +1 -0
  142. package/dist/telemetry/telemetry.js +87 -0
  143. package/dist/telemetry/telemetry.js.map +1 -0
  144. package/dist/vault/vault.d.ts +94 -0
  145. package/dist/vault/vault.d.ts.map +1 -1
  146. package/dist/vault/vault.js +340 -1
  147. package/dist/vault/vault.js.map +1 -1
  148. package/package.json +1 -1
  149. package/src/__tests__/admin-extra-ops.test.ts +420 -0
  150. package/src/__tests__/admin-ops.test.ts +271 -0
  151. package/src/__tests__/brain-intelligence.test.ts +205 -0
  152. package/src/__tests__/brain.test.ts +131 -0
  153. package/src/__tests__/capture-ops.test.ts +509 -0
  154. package/src/__tests__/core-ops.test.ts +266 -2
  155. package/src/__tests__/curator-extra-ops.test.ts +359 -0
  156. package/src/__tests__/domain-ops.test.ts +66 -0
  157. package/src/__tests__/governance.test.ts +522 -0
  158. package/src/__tests__/grading-ops.test.ts +340 -0
  159. package/src/__tests__/identity-manager.test.ts +243 -0
  160. package/src/__tests__/intent-router.test.ts +222 -0
  161. package/src/__tests__/logger.test.ts +200 -0
  162. package/src/__tests__/loop-ops.test.ts +398 -0
  163. package/src/__tests__/memory-cross-project-ops.test.ts +246 -0
  164. package/src/__tests__/memory-extra-ops.test.ts +352 -0
  165. package/src/__tests__/orchestrate-ops.test.ts +284 -0
  166. package/src/__tests__/planner.test.ts +331 -0
  167. package/src/__tests__/planning-extra-ops.test.ts +548 -0
  168. package/src/__tests__/project-ops.test.ts +367 -0
  169. package/src/__tests__/vault-extra-ops.test.ts +407 -0
  170. package/src/brain/brain.ts +114 -7
  171. package/src/brain/intelligence.ts +179 -10
  172. package/src/brain/types.ts +38 -0
  173. package/src/control/identity-manager.ts +354 -0
  174. package/src/control/intent-router.ts +326 -0
  175. package/src/control/types.ts +102 -0
  176. package/src/curator/curator.ts +213 -0
  177. package/src/governance/governance.ts +698 -0
  178. package/src/governance/index.ts +18 -0
  179. package/src/governance/types.ts +111 -0
  180. package/src/index.ts +102 -2
  181. package/src/logging/logger.ts +154 -0
  182. package/src/logging/types.ts +21 -0
  183. package/src/loop/loop-manager.ts +130 -0
  184. package/src/loop/types.ts +44 -0
  185. package/src/planning/gap-analysis.ts +506 -0
  186. package/src/planning/gap-types.ts +58 -0
  187. package/src/planning/planner.ts +478 -2
  188. package/src/project/project-registry.ts +358 -0
  189. package/src/project/types.ts +31 -0
  190. package/src/runtime/admin-extra-ops.ts +307 -0
  191. package/src/runtime/admin-ops.ts +329 -0
  192. package/src/runtime/capture-ops.ts +385 -0
  193. package/src/runtime/core-ops.ts +535 -7
  194. package/src/runtime/curator-extra-ops.ts +71 -0
  195. package/src/runtime/domain-ops.ts +65 -13
  196. package/src/runtime/grading-ops.ts +121 -0
  197. package/src/runtime/loop-ops.ts +194 -0
  198. package/src/runtime/memory-cross-project-ops.ts +192 -0
  199. package/src/runtime/memory-extra-ops.ts +186 -0
  200. package/src/runtime/orchestrate-ops.ts +272 -0
  201. package/src/runtime/planning-extra-ops.ts +327 -0
  202. package/src/runtime/project-ops.ts +196 -0
  203. package/src/runtime/runtime.ts +49 -1
  204. package/src/runtime/types.ts +21 -0
  205. package/src/runtime/vault-extra-ops.ts +225 -0
  206. package/src/telemetry/telemetry.ts +118 -0
  207. package/src/vault/vault.ts +412 -1
@@ -0,0 +1,367 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { createAgentRuntime } from '../runtime/runtime.js';
3
+ import { createProjectOps } from '../runtime/project-ops.js';
4
+ import type { AgentRuntime } from '../runtime/types.js';
5
+ import type { OpDefinition } from '../facades/types.js';
6
+
7
+ describe('createProjectOps', () => {
8
+ let runtime: AgentRuntime;
9
+ let ops: OpDefinition[];
10
+
11
+ function findOp(name: string): OpDefinition {
12
+ const op = ops.find((o) => o.name === name);
13
+ if (!op) throw new Error(`Op "${name}" not found`);
14
+ return op;
15
+ }
16
+
17
+ afterEach(() => {
18
+ runtime?.close();
19
+ });
20
+
21
+ function setup() {
22
+ runtime = createAgentRuntime({
23
+ agentId: 'test-project',
24
+ vaultPath: ':memory:',
25
+ });
26
+ ops = createProjectOps(runtime);
27
+ }
28
+
29
+ it('should return 12 ops', () => {
30
+ setup();
31
+ expect(ops).toHaveLength(12);
32
+ const names = ops.map((o) => o.name);
33
+ expect(names).toEqual([
34
+ 'project_get',
35
+ 'project_list',
36
+ 'project_unregister',
37
+ 'project_get_rules',
38
+ 'project_list_rules',
39
+ 'project_add_rule',
40
+ 'project_remove_rule',
41
+ 'project_link',
42
+ 'project_unlink',
43
+ 'project_get_links',
44
+ 'project_linked_projects',
45
+ 'project_touch',
46
+ ]);
47
+ });
48
+
49
+ // ─── Register + Get round-trip ─────────────────────────────────
50
+
51
+ describe('register + project_get round-trip', () => {
52
+ it('should register a project and retrieve it by ID', async () => {
53
+ setup();
54
+ const registered = runtime.projectRegistry.register('/tmp/my-project', 'My Project');
55
+ const result = (await findOp('project_get').handler({
56
+ projectId: registered.id,
57
+ })) as { found: boolean; project: { id: string; path: string; name: string } };
58
+
59
+ expect(result.found).toBe(true);
60
+ expect(result.project.path).toBe('/tmp/my-project');
61
+ expect(result.project.name).toBe('My Project');
62
+ });
63
+
64
+ it('should return found=false for unknown ID', async () => {
65
+ setup();
66
+ const result = (await findOp('project_get').handler({
67
+ projectId: 'nonexistent',
68
+ })) as { found: boolean; project: null };
69
+
70
+ expect(result.found).toBe(false);
71
+ expect(result.project).toBeNull();
72
+ });
73
+ });
74
+
75
+ // ─── project_list ──────────────────────────────────────────────
76
+
77
+ describe('project_list', () => {
78
+ it('should list all registered projects', async () => {
79
+ setup();
80
+ runtime.projectRegistry.register('/tmp/alpha', 'Alpha');
81
+ runtime.projectRegistry.register('/tmp/beta', 'Beta');
82
+
83
+ const result = (await findOp('project_list').handler({})) as {
84
+ count: number;
85
+ projects: Array<{ path: string; name: string }>;
86
+ };
87
+
88
+ expect(result.count).toBe(2);
89
+ const paths = result.projects.map((p) => p.path);
90
+ expect(paths).toContain('/tmp/alpha');
91
+ expect(paths).toContain('/tmp/beta');
92
+ });
93
+
94
+ it('should return empty list when no projects registered', async () => {
95
+ setup();
96
+ const result = (await findOp('project_list').handler({})) as {
97
+ count: number;
98
+ projects: unknown[];
99
+ };
100
+ expect(result.count).toBe(0);
101
+ expect(result.projects).toEqual([]);
102
+ });
103
+ });
104
+
105
+ // ─── project_unregister ────────────────────────────────────────
106
+
107
+ describe('project_unregister', () => {
108
+ it('should unregister a project', async () => {
109
+ setup();
110
+ const registered = runtime.projectRegistry.register('/tmp/to-remove');
111
+
112
+ const result = (await findOp('project_unregister').handler({
113
+ projectId: registered.id,
114
+ })) as { removed: boolean };
115
+
116
+ expect(result.removed).toBe(true);
117
+
118
+ // Verify it is gone
119
+ const getResult = (await findOp('project_get').handler({
120
+ projectId: registered.id,
121
+ })) as { found: boolean };
122
+ expect(getResult.found).toBe(false);
123
+ });
124
+
125
+ it('should return removed=false for unknown project', async () => {
126
+ setup();
127
+ const result = (await findOp('project_unregister').handler({
128
+ projectId: 'nonexistent',
129
+ })) as { removed: boolean };
130
+ expect(result.removed).toBe(false);
131
+ });
132
+ });
133
+
134
+ // ─── Rules CRUD ────────────────────────────────────────────────
135
+
136
+ describe('project_add_rule + project_get_rules', () => {
137
+ it('should add and retrieve rules for a project', async () => {
138
+ setup();
139
+ const project = runtime.projectRegistry.register('/tmp/rules-project');
140
+
141
+ const addResult = (await findOp('project_add_rule').handler({
142
+ projectId: project.id,
143
+ category: 'behavior',
144
+ text: 'Always use semantic tokens',
145
+ priority: 10,
146
+ })) as { added: boolean; rule: { id: string; category: string; text: string; priority: number } };
147
+
148
+ expect(addResult.added).toBe(true);
149
+ expect(addResult.rule.category).toBe('behavior');
150
+ expect(addResult.rule.text).toBe('Always use semantic tokens');
151
+ expect(addResult.rule.priority).toBe(10);
152
+
153
+ const rulesResult = (await findOp('project_get_rules').handler({
154
+ projectId: project.id,
155
+ })) as { count: number; rules: Array<{ text: string }> };
156
+
157
+ expect(rulesResult.count).toBe(1);
158
+ expect(rulesResult.rules[0].text).toBe('Always use semantic tokens');
159
+ });
160
+ });
161
+
162
+ describe('project_remove_rule', () => {
163
+ it('should remove a rule', async () => {
164
+ setup();
165
+ const project = runtime.projectRegistry.register('/tmp/rm-rule');
166
+ const rule = runtime.projectRegistry.addRule(project.id, {
167
+ category: 'restriction',
168
+ text: 'No inline styles',
169
+ priority: 5,
170
+ });
171
+
172
+ const result = (await findOp('project_remove_rule').handler({
173
+ ruleId: rule.id,
174
+ })) as { removed: boolean };
175
+
176
+ expect(result.removed).toBe(true);
177
+
178
+ // Verify rule is gone
179
+ const rulesResult = (await findOp('project_get_rules').handler({
180
+ projectId: project.id,
181
+ })) as { count: number };
182
+ expect(rulesResult.count).toBe(0);
183
+ });
184
+
185
+ it('should return removed=false for unknown rule', async () => {
186
+ setup();
187
+ const result = (await findOp('project_remove_rule').handler({
188
+ ruleId: 'nonexistent-rule',
189
+ })) as { removed: boolean };
190
+ expect(result.removed).toBe(false);
191
+ });
192
+ });
193
+
194
+ describe('project_list_rules', () => {
195
+ it('should list all projects with their rules', async () => {
196
+ setup();
197
+ const p1 = runtime.projectRegistry.register('/tmp/p1', 'P1');
198
+ const p2 = runtime.projectRegistry.register('/tmp/p2', 'P2');
199
+ runtime.projectRegistry.addRule(p1.id, { category: 'behavior', text: 'Rule A', priority: 0 });
200
+ runtime.projectRegistry.addRule(p1.id, { category: 'convention', text: 'Rule B', priority: 1 });
201
+ runtime.projectRegistry.addRule(p2.id, { category: 'preference', text: 'Rule C', priority: 0 });
202
+
203
+ const result = (await findOp('project_list_rules').handler({})) as {
204
+ count: number;
205
+ projects: Array<{ project: { id: string }; ruleCount: number; rules: unknown[] }>;
206
+ };
207
+
208
+ expect(result.count).toBe(2);
209
+ const p1Entry = result.projects.find((p) => p.project.id === p1.id);
210
+ expect(p1Entry?.ruleCount).toBe(2);
211
+ const p2Entry = result.projects.find((p) => p.project.id === p2.id);
212
+ expect(p2Entry?.ruleCount).toBe(1);
213
+ });
214
+ });
215
+
216
+ // ─── Links ─────────────────────────────────────────────────────
217
+
218
+ describe('project_link + project_unlink', () => {
219
+ it('should link and unlink two projects', async () => {
220
+ setup();
221
+ const p1 = runtime.projectRegistry.register('/tmp/link-a', 'Link A');
222
+ const p2 = runtime.projectRegistry.register('/tmp/link-b', 'Link B');
223
+
224
+ const linkResult = (await findOp('project_link').handler({
225
+ sourceId: p1.id,
226
+ targetId: p2.id,
227
+ linkType: 'related',
228
+ })) as { linked: boolean; link: { sourceProjectId: string; targetProjectId: string; linkType: string } };
229
+
230
+ expect(linkResult.linked).toBe(true);
231
+ expect(linkResult.link.sourceProjectId).toBe(p1.id);
232
+ expect(linkResult.link.targetProjectId).toBe(p2.id);
233
+ expect(linkResult.link.linkType).toBe('related');
234
+
235
+ const unlinkResult = (await findOp('project_unlink').handler({
236
+ sourceId: p1.id,
237
+ targetId: p2.id,
238
+ linkType: 'related',
239
+ })) as { removed: number };
240
+
241
+ expect(unlinkResult.removed).toBe(1);
242
+ });
243
+
244
+ it('should unlink all types when linkType is omitted', async () => {
245
+ setup();
246
+ const p1 = runtime.projectRegistry.register('/tmp/unlink-a');
247
+ const p2 = runtime.projectRegistry.register('/tmp/unlink-b');
248
+
249
+ runtime.projectRegistry.link(p1.id, p2.id, 'related');
250
+ runtime.projectRegistry.link(p1.id, p2.id, 'parent');
251
+
252
+ const result = (await findOp('project_unlink').handler({
253
+ sourceId: p1.id,
254
+ targetId: p2.id,
255
+ })) as { removed: number };
256
+
257
+ expect(result.removed).toBe(2);
258
+ });
259
+ });
260
+
261
+ describe('project_get_links', () => {
262
+ it('should get all links for a project', async () => {
263
+ setup();
264
+ const p1 = runtime.projectRegistry.register('/tmp/gl-a');
265
+ const p2 = runtime.projectRegistry.register('/tmp/gl-b');
266
+ const p3 = runtime.projectRegistry.register('/tmp/gl-c');
267
+
268
+ runtime.projectRegistry.link(p1.id, p2.id, 'related');
269
+ runtime.projectRegistry.link(p3.id, p1.id, 'parent');
270
+
271
+ const result = (await findOp('project_get_links').handler({
272
+ projectId: p1.id,
273
+ })) as { count: number; links: Array<{ sourceProjectId: string; targetProjectId: string }> };
274
+
275
+ expect(result.count).toBe(2);
276
+ });
277
+ });
278
+
279
+ describe('project_linked_projects', () => {
280
+ it('should return linked projects with direction info', async () => {
281
+ setup();
282
+ const p1 = runtime.projectRegistry.register('/tmp/lp-a', 'LP-A');
283
+ const p2 = runtime.projectRegistry.register('/tmp/lp-b', 'LP-B');
284
+ const p3 = runtime.projectRegistry.register('/tmp/lp-c', 'LP-C');
285
+
286
+ runtime.projectRegistry.link(p1.id, p2.id, 'related');
287
+ runtime.projectRegistry.link(p3.id, p1.id, 'fork');
288
+
289
+ const result = (await findOp('project_linked_projects').handler({
290
+ projectId: p1.id,
291
+ })) as {
292
+ count: number;
293
+ linked: Array<{
294
+ project: { id: string; name: string };
295
+ linkType: string;
296
+ direction: 'outgoing' | 'incoming';
297
+ }>;
298
+ };
299
+
300
+ expect(result.count).toBe(2);
301
+
302
+ const outgoing = result.linked.find((l) => l.direction === 'outgoing');
303
+ expect(outgoing?.project.name).toBe('LP-B');
304
+ expect(outgoing?.linkType).toBe('related');
305
+
306
+ const incoming = result.linked.find((l) => l.direction === 'incoming');
307
+ expect(incoming?.project.name).toBe('LP-C');
308
+ expect(incoming?.linkType).toBe('fork');
309
+ });
310
+ });
311
+
312
+ // ─── project_touch ─────────────────────────────────────────────
313
+
314
+ describe('project_touch', () => {
315
+ it('should update last accessed timestamp', async () => {
316
+ setup();
317
+ const project = runtime.projectRegistry.register('/tmp/touch-project');
318
+ const originalTs = project.lastAccessedAt;
319
+
320
+ // Small delay to ensure timestamp changes
321
+ await new Promise((r) => setTimeout(r, 10));
322
+
323
+ const result = (await findOp('project_touch').handler({
324
+ projectId: project.id,
325
+ })) as { touched: boolean };
326
+
327
+ expect(result.touched).toBe(true);
328
+
329
+ const updated = runtime.projectRegistry.get(project.id);
330
+ expect(updated!.lastAccessedAt).toBeGreaterThan(originalTs);
331
+ });
332
+ });
333
+
334
+ // ─── Auth levels ───────────────────────────────────────────────
335
+
336
+ describe('auth levels', () => {
337
+ it('should use read auth for read ops', () => {
338
+ setup();
339
+ const readOps = [
340
+ 'project_get',
341
+ 'project_list',
342
+ 'project_get_rules',
343
+ 'project_list_rules',
344
+ 'project_get_links',
345
+ 'project_linked_projects',
346
+ ];
347
+ for (const name of readOps) {
348
+ expect(findOp(name).auth).toBe('read');
349
+ }
350
+ });
351
+
352
+ it('should use write auth for mutation ops', () => {
353
+ setup();
354
+ const writeOps = [
355
+ 'project_unregister',
356
+ 'project_add_rule',
357
+ 'project_remove_rule',
358
+ 'project_link',
359
+ 'project_unlink',
360
+ 'project_touch',
361
+ ];
362
+ for (const name of writeOps) {
363
+ expect(findOp(name).auth).toBe('write');
364
+ }
365
+ });
366
+ });
367
+ });