@arke-institute/rhiza 0.1.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 (230) hide show
  1. package/README.md +106 -0
  2. package/dist/__tests__/fixtures/index.d.ts +9 -0
  3. package/dist/__tests__/fixtures/index.d.ts.map +1 -0
  4. package/dist/__tests__/fixtures/index.js +12 -0
  5. package/dist/__tests__/fixtures/index.js.map +1 -0
  6. package/dist/__tests__/fixtures/kladoi/index.d.ts +165 -0
  7. package/dist/__tests__/fixtures/kladoi/index.d.ts.map +1 -0
  8. package/dist/__tests__/fixtures/kladoi/index.js +270 -0
  9. package/dist/__tests__/fixtures/kladoi/index.js.map +1 -0
  10. package/dist/__tests__/fixtures/logs/index.d.ts +19 -0
  11. package/dist/__tests__/fixtures/logs/index.d.ts.map +1 -0
  12. package/dist/__tests__/fixtures/logs/index.js +545 -0
  13. package/dist/__tests__/fixtures/logs/index.js.map +1 -0
  14. package/dist/__tests__/fixtures/mock-client.d.ts +127 -0
  15. package/dist/__tests__/fixtures/mock-client.d.ts.map +1 -0
  16. package/dist/__tests__/fixtures/mock-client.js +415 -0
  17. package/dist/__tests__/fixtures/mock-client.js.map +1 -0
  18. package/dist/__tests__/fixtures/rhizai/index.d.ts +54 -0
  19. package/dist/__tests__/fixtures/rhizai/index.d.ts.map +1 -0
  20. package/dist/__tests__/fixtures/rhizai/index.js +283 -0
  21. package/dist/__tests__/fixtures/rhizai/index.js.map +1 -0
  22. package/dist/__tests__/unit/fixtures.test.d.ts +10 -0
  23. package/dist/__tests__/unit/fixtures.test.d.ts.map +1 -0
  24. package/dist/__tests__/unit/fixtures.test.js +275 -0
  25. package/dist/__tests__/unit/fixtures.test.js.map +1 -0
  26. package/dist/__tests__/unit/handoff/gather.test.d.ts +8 -0
  27. package/dist/__tests__/unit/handoff/gather.test.d.ts.map +1 -0
  28. package/dist/__tests__/unit/handoff/gather.test.js +176 -0
  29. package/dist/__tests__/unit/handoff/gather.test.js.map +1 -0
  30. package/dist/__tests__/unit/handoff/interpret.test.d.ts +9 -0
  31. package/dist/__tests__/unit/handoff/interpret.test.d.ts.map +1 -0
  32. package/dist/__tests__/unit/handoff/interpret.test.js +370 -0
  33. package/dist/__tests__/unit/handoff/interpret.test.js.map +1 -0
  34. package/dist/__tests__/unit/handoff/route.test.d.ts +8 -0
  35. package/dist/__tests__/unit/handoff/route.test.d.ts.map +1 -0
  36. package/dist/__tests__/unit/handoff/route.test.js +271 -0
  37. package/dist/__tests__/unit/handoff/route.test.js.map +1 -0
  38. package/dist/__tests__/unit/handoff/scatter.test.d.ts +7 -0
  39. package/dist/__tests__/unit/handoff/scatter.test.d.ts.map +1 -0
  40. package/dist/__tests__/unit/handoff/scatter.test.js +54 -0
  41. package/dist/__tests__/unit/handoff/scatter.test.js.map +1 -0
  42. package/dist/__tests__/unit/resume.test.d.ts +8 -0
  43. package/dist/__tests__/unit/resume.test.d.ts.map +1 -0
  44. package/dist/__tests__/unit/resume.test.js +134 -0
  45. package/dist/__tests__/unit/resume.test.js.map +1 -0
  46. package/dist/__tests__/unit/status.test.d.ts +8 -0
  47. package/dist/__tests__/unit/status.test.d.ts.map +1 -0
  48. package/dist/__tests__/unit/status.test.js +164 -0
  49. package/dist/__tests__/unit/status.test.js.map +1 -0
  50. package/dist/__tests__/unit/target.test.d.ts +8 -0
  51. package/dist/__tests__/unit/target.test.d.ts.map +1 -0
  52. package/dist/__tests__/unit/target.test.js +116 -0
  53. package/dist/__tests__/unit/target.test.js.map +1 -0
  54. package/dist/__tests__/unit/traverse.test.d.ts +8 -0
  55. package/dist/__tests__/unit/traverse.test.d.ts.map +1 -0
  56. package/dist/__tests__/unit/traverse.test.js +143 -0
  57. package/dist/__tests__/unit/traverse.test.js.map +1 -0
  58. package/dist/__tests__/unit/validation/klados.test.d.ts +16 -0
  59. package/dist/__tests__/unit/validation/klados.test.d.ts.map +1 -0
  60. package/dist/__tests__/unit/validation/klados.test.js +275 -0
  61. package/dist/__tests__/unit/validation/klados.test.js.map +1 -0
  62. package/dist/__tests__/unit/validation/rhiza.test.d.ts +16 -0
  63. package/dist/__tests__/unit/validation/rhiza.test.d.ts.map +1 -0
  64. package/dist/__tests__/unit/validation/rhiza.test.js +612 -0
  65. package/dist/__tests__/unit/validation/rhiza.test.js.map +1 -0
  66. package/dist/__tests__/unit/validation/runtime.test.d.ts +11 -0
  67. package/dist/__tests__/unit/validation/runtime.test.d.ts.map +1 -0
  68. package/dist/__tests__/unit/validation/runtime.test.js +553 -0
  69. package/dist/__tests__/unit/validation/runtime.test.js.map +1 -0
  70. package/dist/__tests__/unit/worker/errors.test.d.ts +2 -0
  71. package/dist/__tests__/unit/worker/errors.test.d.ts.map +1 -0
  72. package/dist/__tests__/unit/worker/errors.test.js +226 -0
  73. package/dist/__tests__/unit/worker/errors.test.js.map +1 -0
  74. package/dist/__tests__/unit/worker/job.test.d.ts +2 -0
  75. package/dist/__tests__/unit/worker/job.test.d.ts.map +1 -0
  76. package/dist/__tests__/unit/worker/job.test.js +233 -0
  77. package/dist/__tests__/unit/worker/job.test.js.map +1 -0
  78. package/dist/client/index.d.ts +10 -0
  79. package/dist/client/index.d.ts.map +1 -0
  80. package/dist/client/index.js +8 -0
  81. package/dist/client/index.js.map +1 -0
  82. package/dist/client/interface.d.ts +142 -0
  83. package/dist/client/interface.d.ts.map +1 -0
  84. package/dist/client/interface.js +8 -0
  85. package/dist/client/interface.js.map +1 -0
  86. package/dist/client/mock.d.ts +95 -0
  87. package/dist/client/mock.d.ts.map +1 -0
  88. package/dist/client/mock.js +795 -0
  89. package/dist/client/mock.js.map +1 -0
  90. package/dist/client/types.d.ts +344 -0
  91. package/dist/client/types.d.ts.map +1 -0
  92. package/dist/client/types.js +8 -0
  93. package/dist/client/types.js.map +1 -0
  94. package/dist/handoff/gather-api.d.ts +60 -0
  95. package/dist/handoff/gather-api.d.ts.map +1 -0
  96. package/dist/handoff/gather-api.js +130 -0
  97. package/dist/handoff/gather-api.js.map +1 -0
  98. package/dist/handoff/gather.d.ts +59 -0
  99. package/dist/handoff/gather.d.ts.map +1 -0
  100. package/dist/handoff/gather.js +134 -0
  101. package/dist/handoff/gather.js.map +1 -0
  102. package/dist/handoff/index.d.ts +19 -0
  103. package/dist/handoff/index.d.ts.map +1 -0
  104. package/dist/handoff/index.js +25 -0
  105. package/dist/handoff/index.js.map +1 -0
  106. package/dist/handoff/interpret.d.ts +79 -0
  107. package/dist/handoff/interpret.d.ts.map +1 -0
  108. package/dist/handoff/interpret.js +197 -0
  109. package/dist/handoff/interpret.js.map +1 -0
  110. package/dist/handoff/invoke.d.ts +82 -0
  111. package/dist/handoff/invoke.d.ts.map +1 -0
  112. package/dist/handoff/invoke.js +196 -0
  113. package/dist/handoff/invoke.js.map +1 -0
  114. package/dist/handoff/route.d.ts +25 -0
  115. package/dist/handoff/route.d.ts.map +1 -0
  116. package/dist/handoff/route.js +65 -0
  117. package/dist/handoff/route.js.map +1 -0
  118. package/dist/handoff/scatter-api.d.ts +62 -0
  119. package/dist/handoff/scatter-api.d.ts.map +1 -0
  120. package/dist/handoff/scatter-api.js +81 -0
  121. package/dist/handoff/scatter-api.js.map +1 -0
  122. package/dist/handoff/scatter.d.ts +19 -0
  123. package/dist/handoff/scatter.d.ts.map +1 -0
  124. package/dist/handoff/scatter.js +27 -0
  125. package/dist/handoff/scatter.js.map +1 -0
  126. package/dist/handoff/target.d.ts +16 -0
  127. package/dist/handoff/target.d.ts.map +1 -0
  128. package/dist/handoff/target.js +51 -0
  129. package/dist/handoff/target.js.map +1 -0
  130. package/dist/index.d.ts +42 -0
  131. package/dist/index.d.ts.map +1 -0
  132. package/dist/index.js +52 -0
  133. package/dist/index.js.map +1 -0
  134. package/dist/logging/index.d.ts +4 -0
  135. package/dist/logging/index.d.ts.map +1 -0
  136. package/dist/logging/index.js +3 -0
  137. package/dist/logging/index.js.map +1 -0
  138. package/dist/logging/logger.d.ts +52 -0
  139. package/dist/logging/logger.d.ts.map +1 -0
  140. package/dist/logging/logger.js +70 -0
  141. package/dist/logging/logger.js.map +1 -0
  142. package/dist/logging/writer.d.ts +63 -0
  143. package/dist/logging/writer.d.ts.map +1 -0
  144. package/dist/logging/writer.js +160 -0
  145. package/dist/logging/writer.js.map +1 -0
  146. package/dist/resume/index.d.ts +77 -0
  147. package/dist/resume/index.d.ts.map +1 -0
  148. package/dist/resume/index.js +110 -0
  149. package/dist/resume/index.js.map +1 -0
  150. package/dist/status/index.d.ts +54 -0
  151. package/dist/status/index.d.ts.map +1 -0
  152. package/dist/status/index.js +107 -0
  153. package/dist/status/index.js.map +1 -0
  154. package/dist/traverse/index.d.ts +53 -0
  155. package/dist/traverse/index.d.ts.map +1 -0
  156. package/dist/traverse/index.js +142 -0
  157. package/dist/traverse/index.js.map +1 -0
  158. package/dist/types/batch.d.ts +53 -0
  159. package/dist/types/batch.d.ts.map +1 -0
  160. package/dist/types/batch.js +2 -0
  161. package/dist/types/batch.js.map +1 -0
  162. package/dist/types/index.d.ts +10 -0
  163. package/dist/types/index.d.ts.map +1 -0
  164. package/dist/types/index.js +2 -0
  165. package/dist/types/index.js.map +1 -0
  166. package/dist/types/klados.d.ts +63 -0
  167. package/dist/types/klados.d.ts.map +1 -0
  168. package/dist/types/klados.js +2 -0
  169. package/dist/types/klados.js.map +1 -0
  170. package/dist/types/log.d.ts +107 -0
  171. package/dist/types/log.d.ts.map +1 -0
  172. package/dist/types/log.js +2 -0
  173. package/dist/types/log.js.map +1 -0
  174. package/dist/types/refs.d.ts +58 -0
  175. package/dist/types/refs.d.ts.map +1 -0
  176. package/dist/types/refs.js +43 -0
  177. package/dist/types/refs.js.map +1 -0
  178. package/dist/types/request.d.ts +69 -0
  179. package/dist/types/request.d.ts.map +1 -0
  180. package/dist/types/request.js +2 -0
  181. package/dist/types/request.js.map +1 -0
  182. package/dist/types/response.d.ts +31 -0
  183. package/dist/types/response.d.ts.map +1 -0
  184. package/dist/types/response.js +2 -0
  185. package/dist/types/response.js.map +1 -0
  186. package/dist/types/rhiza.d.ts +100 -0
  187. package/dist/types/rhiza.d.ts.map +1 -0
  188. package/dist/types/rhiza.js +2 -0
  189. package/dist/types/rhiza.js.map +1 -0
  190. package/dist/types/status.d.ts +84 -0
  191. package/dist/types/status.d.ts.map +1 -0
  192. package/dist/types/status.js +2 -0
  193. package/dist/types/status.js.map +1 -0
  194. package/dist/utils/id.d.ts +15 -0
  195. package/dist/utils/id.d.ts.map +1 -0
  196. package/dist/utils/id.js +25 -0
  197. package/dist/utils/id.js.map +1 -0
  198. package/dist/utils/index.d.ts +2 -0
  199. package/dist/utils/index.d.ts.map +1 -0
  200. package/dist/utils/index.js +2 -0
  201. package/dist/utils/index.js.map +1 -0
  202. package/dist/validation/index.d.ts +9 -0
  203. package/dist/validation/index.d.ts.map +1 -0
  204. package/dist/validation/index.js +9 -0
  205. package/dist/validation/index.js.map +1 -0
  206. package/dist/validation/validate-klados.d.ts +38 -0
  207. package/dist/validation/validate-klados.d.ts.map +1 -0
  208. package/dist/validation/validate-klados.js +139 -0
  209. package/dist/validation/validate-klados.js.map +1 -0
  210. package/dist/validation/validate-rhiza.d.ts +29 -0
  211. package/dist/validation/validate-rhiza.d.ts.map +1 -0
  212. package/dist/validation/validate-rhiza.js +382 -0
  213. package/dist/validation/validate-rhiza.js.map +1 -0
  214. package/dist/validation/validate-runtime.d.ts +28 -0
  215. package/dist/validation/validate-runtime.d.ts.map +1 -0
  216. package/dist/validation/validate-runtime.js +212 -0
  217. package/dist/validation/validate-runtime.js.map +1 -0
  218. package/dist/worker/errors.d.ts +77 -0
  219. package/dist/worker/errors.d.ts.map +1 -0
  220. package/dist/worker/errors.js +143 -0
  221. package/dist/worker/errors.js.map +1 -0
  222. package/dist/worker/index.d.ts +8 -0
  223. package/dist/worker/index.d.ts.map +1 -0
  224. package/dist/worker/index.js +8 -0
  225. package/dist/worker/index.js.map +1 -0
  226. package/dist/worker/job.d.ts +150 -0
  227. package/dist/worker/job.d.ts.map +1 -0
  228. package/dist/worker/job.js +280 -0
  229. package/dist/worker/job.js.map +1 -0
  230. package/package.json +48 -0
@@ -0,0 +1,795 @@
1
+ /**
2
+ * Mock Rhiza Client
3
+ *
4
+ * In-memory implementation of the RhizaClient interface.
5
+ * This serves as the API specification - it defines exactly what the
6
+ * real API endpoints should do.
7
+ *
8
+ * Use this for:
9
+ * - Unit tests
10
+ * - Development without network
11
+ * - API contract specification
12
+ */
13
+ import { validateKladosProperties } from '../validation/validate-klados';
14
+ import { validateRhizaProperties } from '../validation/validate-rhiza';
15
+ import { buildStatusFromLogs } from '../status';
16
+ import { findErrorLeaves } from '../traverse';
17
+ // ============================================================================
18
+ // Helper function for safe type conversion
19
+ // ============================================================================
20
+ function asError(result) {
21
+ return { error: result.error };
22
+ }
23
+ // ============================================================================
24
+ // Mock Client Implementation
25
+ // ============================================================================
26
+ /**
27
+ * In-memory mock implementation of RhizaClient.
28
+ *
29
+ * This implementation defines the expected API behavior.
30
+ * The real API should match this behavior exactly.
31
+ */
32
+ export class MockRhizaClient {
33
+ entities = new Map();
34
+ collections = new Map();
35
+ pendingVerifications = new Map();
36
+ debug;
37
+ idCounter = 0;
38
+ constructor(config = {}) {
39
+ this.debug = config.debug ?? false;
40
+ // Seed initial entities
41
+ if (config.entities) {
42
+ for (const [id, entity] of Object.entries(config.entities)) {
43
+ this.entities.set(id, entity);
44
+ this.addToCollection(entity.collectionId, id);
45
+ }
46
+ }
47
+ }
48
+ // =========================================================================
49
+ // Entity Operations
50
+ // =========================================================================
51
+ async getEntity(id) {
52
+ const entity = this.entities.get(id);
53
+ if (!entity) {
54
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
55
+ }
56
+ return { data: this.toEntityResponse(entity) };
57
+ }
58
+ async getEntityTip(id) {
59
+ const entity = this.entities.get(id);
60
+ if (!entity) {
61
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
62
+ }
63
+ return { data: { cid: entity.cid } };
64
+ }
65
+ async createEntity(params) {
66
+ const id = this.generateId(params.type);
67
+ const now = new Date().toISOString();
68
+ const cid = this.generateCid();
69
+ const entity = {
70
+ id,
71
+ cid,
72
+ type: params.type,
73
+ properties: params.properties,
74
+ collectionId: params.collectionId,
75
+ relationships: params.relationships ?? [],
76
+ createdAt: now,
77
+ updatedAt: now,
78
+ };
79
+ this.entities.set(id, entity);
80
+ this.addToCollection(params.collectionId, id);
81
+ this.log('createEntity', { id, type: params.type });
82
+ return { data: this.toEntityResponse(entity) };
83
+ }
84
+ async updateEntity(id, params) {
85
+ const entity = this.entities.get(id);
86
+ if (!entity) {
87
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
88
+ }
89
+ // CAS check (optional for mock)
90
+ if (params.expectTip && entity.cid !== params.expectTip) {
91
+ return {
92
+ error: {
93
+ code: 'CAS_CONFLICT',
94
+ message: `Expected tip ${params.expectTip}, but current is ${entity.cid}`,
95
+ },
96
+ };
97
+ }
98
+ // Update properties
99
+ if (params.properties) {
100
+ entity.properties = { ...entity.properties, ...params.properties };
101
+ }
102
+ // Update relationships
103
+ if (params.relationshipsAdd) {
104
+ entity.relationships.push(...params.relationshipsAdd);
105
+ }
106
+ if (params.relationshipsRemove) {
107
+ const removeSet = new Set(params.relationshipsRemove.map((r) => `${r.predicate}:${r.peer}`));
108
+ entity.relationships = entity.relationships.filter((r) => !removeSet.has(`${r.predicate}:${r.peer}`));
109
+ }
110
+ // Update metadata
111
+ entity.cid = this.generateCid();
112
+ entity.updatedAt = new Date().toISOString();
113
+ this.log('updateEntity', { id });
114
+ return { data: this.toEntityResponse(entity) };
115
+ }
116
+ // =========================================================================
117
+ // Klados Operations
118
+ // =========================================================================
119
+ async getKlados(id) {
120
+ const entity = this.entities.get(id);
121
+ if (!entity) {
122
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
123
+ }
124
+ if (entity.type !== 'klados') {
125
+ return {
126
+ error: {
127
+ code: 'TYPE_MISMATCH',
128
+ message: `Entity ${id} is not a klados`,
129
+ },
130
+ };
131
+ }
132
+ // Build KladosEntity from stored entity
133
+ return {
134
+ data: {
135
+ id: entity.id,
136
+ cid: entity.cid,
137
+ type: 'klados',
138
+ properties: entity.properties,
139
+ },
140
+ };
141
+ }
142
+ async createKlados(params) {
143
+ // Validate klados properties
144
+ const validation = validateKladosProperties(params.properties);
145
+ if (!validation.valid) {
146
+ return {
147
+ error: {
148
+ code: 'VALIDATION_ERROR',
149
+ message: validation.errors[0].message,
150
+ details: { errors: validation.errors },
151
+ },
152
+ };
153
+ }
154
+ // Create the entity
155
+ const result = await this.createEntity({
156
+ collectionId: params.collectionId,
157
+ type: 'klados',
158
+ properties: {
159
+ ...params.properties,
160
+ status: 'development',
161
+ created_at: new Date().toISOString(),
162
+ },
163
+ });
164
+ if (result.error)
165
+ return asError(result);
166
+ this.log('createKlados', { id: result.data.id });
167
+ return {
168
+ data: {
169
+ id: result.data.id,
170
+ cid: result.data.cid,
171
+ type: 'klados',
172
+ properties: result.data.properties,
173
+ },
174
+ };
175
+ }
176
+ async updateKlados(id, params) {
177
+ const current = await this.getKlados(id);
178
+ if (current.error)
179
+ return current;
180
+ const currentProps = current.data.properties;
181
+ const properties = {};
182
+ // Map params to property updates
183
+ if (params.label !== undefined)
184
+ properties.label = params.label;
185
+ if (params.description !== undefined)
186
+ properties.description = params.description;
187
+ if (params.actionsRequired !== undefined)
188
+ properties.actions_required = params.actionsRequired;
189
+ if (params.accepts !== undefined)
190
+ properties.accepts = params.accepts;
191
+ if (params.produces !== undefined)
192
+ properties.produces = params.produces;
193
+ if (params.inputSchema !== undefined)
194
+ properties.input_schema = params.inputSchema;
195
+ // Special handling for endpoint change
196
+ if (params.endpoint !== undefined &&
197
+ params.endpoint !== currentProps.endpoint) {
198
+ properties.endpoint = params.endpoint;
199
+ properties.endpoint_verified_at = null;
200
+ properties.status = 'development';
201
+ }
202
+ // Status change validation
203
+ if (params.status !== undefined) {
204
+ if (params.status === 'active' && !currentProps.endpoint_verified_at) {
205
+ return {
206
+ error: {
207
+ code: 'VALIDATION_ERROR',
208
+ message: 'Cannot set status to active without verified endpoint',
209
+ },
210
+ };
211
+ }
212
+ properties.status = params.status;
213
+ }
214
+ const result = await this.updateEntity(id, {
215
+ expectTip: params.expectTip,
216
+ properties,
217
+ });
218
+ if (result.error)
219
+ return asError(result);
220
+ this.log('updateKlados', { id });
221
+ return this.getKlados(id);
222
+ }
223
+ async invokeKlados(id, params) {
224
+ const klados = await this.getKlados(id);
225
+ if (klados.error)
226
+ return asError(klados);
227
+ const kladosProps = klados.data.properties;
228
+ // Check if klados is active (or skip in mock for testing)
229
+ // In production, this would require status === 'active'
230
+ // Handle confirmation flow
231
+ if (!params.confirm) {
232
+ return {
233
+ data: {
234
+ status: 'pending_confirmation',
235
+ message: `Klados "${kladosProps.label}" requires confirmation`,
236
+ grants: [
237
+ {
238
+ type: 'klados',
239
+ id,
240
+ label: String(kladosProps.label),
241
+ actions: kladosProps.actions_required,
242
+ },
243
+ ],
244
+ expiresAt: new Date(Date.now() + (params.expiresIn ?? 3600) * 1000).toISOString(),
245
+ },
246
+ };
247
+ }
248
+ // Generate job ID and collection
249
+ const jobId = this.generateId('job');
250
+ const jobCollection = params.jobCollection ?? this.generateId('job_collection');
251
+ // In a real implementation, this would:
252
+ // 1. Create a job collection if needed
253
+ // 2. Create an initial log entry
254
+ // 3. Make HTTP request to klados endpoint
255
+ // 4. Return job info for tracking
256
+ this.log('invokeKlados', { id, jobId, jobCollection });
257
+ return {
258
+ data: {
259
+ status: 'started',
260
+ jobId,
261
+ jobCollection,
262
+ kladosId: id,
263
+ expiresAt: new Date(Date.now() + (params.expiresIn ?? 3600) * 1000).toISOString(),
264
+ },
265
+ };
266
+ }
267
+ async verifyKlados(id, params) {
268
+ const klados = await this.getKlados(id);
269
+ if (klados.error)
270
+ return asError(klados);
271
+ const kladosProps = klados.data.properties;
272
+ if (!params?.confirm) {
273
+ // Phase 1: Generate verification token
274
+ const token = `vt_${this.generateId('verify')}`;
275
+ const expiresAt = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes
276
+ this.pendingVerifications.set(id, {
277
+ token,
278
+ kladosId: id,
279
+ endpoint: String(kladosProps.endpoint),
280
+ expiresAt,
281
+ });
282
+ return {
283
+ data: {
284
+ verificationToken: token,
285
+ kladosId: id,
286
+ endpoint: String(kladosProps.endpoint),
287
+ instructions: 'Return this token from your endpoint when called with verification=true',
288
+ expiresAt: expiresAt.toISOString(),
289
+ },
290
+ };
291
+ }
292
+ // Phase 2: Confirm verification
293
+ const pending = this.pendingVerifications.get(id);
294
+ if (!pending) {
295
+ return {
296
+ data: {
297
+ verified: false,
298
+ error: 'no_token',
299
+ message: 'No pending verification. Call without confirm first.',
300
+ },
301
+ };
302
+ }
303
+ if (new Date() > pending.expiresAt) {
304
+ this.pendingVerifications.delete(id);
305
+ return {
306
+ data: {
307
+ verified: false,
308
+ error: 'token_expired',
309
+ message: 'Verification token expired. Start again.',
310
+ },
311
+ };
312
+ }
313
+ // In real implementation: fetch endpoint and verify token response
314
+ // For mock, we'll simulate success
315
+ this.pendingVerifications.delete(id);
316
+ const verifiedAt = new Date().toISOString();
317
+ await this.updateEntity(id, {
318
+ properties: {
319
+ endpoint_verified_at: verifiedAt,
320
+ },
321
+ });
322
+ return {
323
+ data: {
324
+ verified: true,
325
+ verifiedAt,
326
+ },
327
+ };
328
+ }
329
+ // =========================================================================
330
+ // Rhiza Operations
331
+ // =========================================================================
332
+ async getRhiza(id) {
333
+ const entity = this.entities.get(id);
334
+ if (!entity) {
335
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
336
+ }
337
+ if (entity.type !== 'rhiza') {
338
+ return {
339
+ error: { code: 'TYPE_MISMATCH', message: `Entity ${id} is not a rhiza` },
340
+ };
341
+ }
342
+ return {
343
+ data: {
344
+ id: entity.id,
345
+ cid: entity.cid,
346
+ type: 'rhiza',
347
+ properties: entity.properties,
348
+ },
349
+ };
350
+ }
351
+ async createRhiza(params) {
352
+ // Validate rhiza properties
353
+ const validation = validateRhizaProperties(params.properties);
354
+ if (!validation.valid) {
355
+ return {
356
+ error: {
357
+ code: 'VALIDATION_ERROR',
358
+ message: validation.errors[0].message,
359
+ details: { errors: validation.errors },
360
+ },
361
+ };
362
+ }
363
+ // Create the entity
364
+ const result = await this.createEntity({
365
+ collectionId: params.collectionId,
366
+ type: 'rhiza',
367
+ properties: {
368
+ ...params.properties,
369
+ status: 'development',
370
+ created_at: new Date().toISOString(),
371
+ },
372
+ });
373
+ if (result.error)
374
+ return asError(result);
375
+ this.log('createRhiza', { id: result.data.id });
376
+ return {
377
+ data: {
378
+ id: result.data.id,
379
+ cid: result.data.cid,
380
+ type: 'rhiza',
381
+ properties: result.data.properties,
382
+ },
383
+ };
384
+ }
385
+ async updateRhiza(id, params) {
386
+ const current = await this.getRhiza(id);
387
+ if (current.error)
388
+ return current;
389
+ const properties = {};
390
+ if (params.label !== undefined)
391
+ properties.label = params.label;
392
+ if (params.description !== undefined)
393
+ properties.description = params.description;
394
+ if (params.version !== undefined)
395
+ properties.version = params.version;
396
+ if (params.entry !== undefined)
397
+ properties.entry = params.entry;
398
+ if (params.flow !== undefined)
399
+ properties.flow = params.flow;
400
+ if (params.status !== undefined)
401
+ properties.status = params.status;
402
+ // Validate if flow changed
403
+ if (params.entry !== undefined || params.flow !== undefined) {
404
+ const mergedProps = {
405
+ ...current.data.properties,
406
+ ...properties,
407
+ };
408
+ const validation = validateRhizaProperties(mergedProps);
409
+ if (!validation.valid) {
410
+ return {
411
+ error: {
412
+ code: 'VALIDATION_ERROR',
413
+ message: validation.errors[0].message,
414
+ details: { errors: validation.errors },
415
+ },
416
+ };
417
+ }
418
+ }
419
+ const result = await this.updateEntity(id, {
420
+ expectTip: params.expectTip,
421
+ properties,
422
+ });
423
+ if (result.error)
424
+ return asError(result);
425
+ this.log('updateRhiza', { id });
426
+ return this.getRhiza(id);
427
+ }
428
+ async invokeRhiza(id, params) {
429
+ const rhiza = await this.getRhiza(id);
430
+ if (rhiza.error)
431
+ return asError(rhiza);
432
+ const rhizaProps = rhiza.data.properties;
433
+ // In production: runtime validation of all kladoi
434
+ if (!params.confirm) {
435
+ // Collect all grants (all kladoi in flow)
436
+ const flow = rhizaProps.flow;
437
+ const grants = Object.keys(flow).map((kladosId) => ({
438
+ type: 'klados',
439
+ id: kladosId,
440
+ label: kladosId, // Would be fetched from actual klados
441
+ }));
442
+ return {
443
+ data: {
444
+ status: 'pending_confirmation',
445
+ message: `Workflow "${rhizaProps.label}" requires confirmation`,
446
+ grants,
447
+ expiresAt: new Date(Date.now() + (params.expiresIn ?? 3600) * 1000).toISOString(),
448
+ },
449
+ };
450
+ }
451
+ const jobId = this.generateId('job');
452
+ const jobCollection = this.generateId('job_collection');
453
+ // In real implementation: start workflow execution from entry klados
454
+ this.log('invokeRhiza', { id, jobId, jobCollection });
455
+ return {
456
+ data: {
457
+ status: 'started',
458
+ jobId,
459
+ jobCollection,
460
+ rhizaId: id,
461
+ expiresAt: new Date(Date.now() + (params.expiresIn ?? 3600) * 1000).toISOString(),
462
+ },
463
+ };
464
+ }
465
+ async getWorkflowStatus(_rhizaId, jobId) {
466
+ // Find the job collection for this job
467
+ const logs = await this.getJobLogs(jobId);
468
+ if (logs.error) {
469
+ return {
470
+ error: {
471
+ code: 'NOT_FOUND',
472
+ message: `No logs found for job ${jobId}`,
473
+ },
474
+ };
475
+ }
476
+ if (!logs.data || logs.data.length === 0) {
477
+ return {
478
+ error: {
479
+ code: 'NOT_FOUND',
480
+ message: `No logs found for job ${jobId}`,
481
+ },
482
+ };
483
+ }
484
+ // Use buildStatusFromLogs to compute status
485
+ const status = buildStatusFromLogs(logs.data);
486
+ // Map status - handle 'unknown' case
487
+ let mappedStatus = 'running';
488
+ if (status.status === 'done' ||
489
+ status.status === 'error' ||
490
+ status.status === 'running' ||
491
+ status.status === 'pending') {
492
+ mappedStatus = status.status;
493
+ }
494
+ return {
495
+ data: {
496
+ jobId,
497
+ rhizaId: status.rhizaId,
498
+ status: mappedStatus,
499
+ progress: {
500
+ total: status.progress.total,
501
+ pending: status.progress.pending,
502
+ running: status.progress.running,
503
+ done: status.progress.done,
504
+ error: status.progress.error,
505
+ },
506
+ currentKladoi: status.currentKladoi,
507
+ errors: status.errors?.map((e) => ({
508
+ kladosId: e.kladosId,
509
+ jobId: e.jobId,
510
+ code: e.code,
511
+ message: e.message,
512
+ retryable: e.retryable,
513
+ })),
514
+ startedAt: logs.data[0]?.started_at ?? new Date().toISOString(),
515
+ completedAt: mappedStatus === 'done' || mappedStatus === 'error'
516
+ ? new Date().toISOString()
517
+ : undefined,
518
+ },
519
+ };
520
+ }
521
+ async resumeWorkflow(_rhizaId, jobId, params) {
522
+ const logs = await this.getJobLogs(jobId);
523
+ if (logs.error) {
524
+ return {
525
+ error: {
526
+ code: 'NOT_FOUND',
527
+ message: `No logs found for job ${jobId}`,
528
+ },
529
+ };
530
+ }
531
+ if (!logs.data || logs.data.length === 0) {
532
+ return {
533
+ data: {
534
+ resumed: 0,
535
+ skipped: 0,
536
+ jobs: [],
537
+ },
538
+ };
539
+ }
540
+ // Find error leaves using traverse module
541
+ const errorLeaves = findErrorLeaves(logs.data);
542
+ // Filter by jobIds if provided
543
+ let candidates = errorLeaves;
544
+ if (params?.jobIds && params.jobIds.length > 0) {
545
+ const jobIdSet = new Set(params.jobIds);
546
+ candidates = errorLeaves.filter((e) => jobIdSet.has(e.log.job_id));
547
+ }
548
+ const result = {
549
+ resumed: 0,
550
+ skipped: 0,
551
+ jobs: [],
552
+ };
553
+ for (const errorLeaf of candidates) {
554
+ // Check maxJobs limit
555
+ if (params?.maxJobs !== undefined &&
556
+ result.resumed >= params.maxJobs) {
557
+ result.skipped++;
558
+ continue;
559
+ }
560
+ // Skip non-retryable
561
+ if (!errorLeaf.retryable) {
562
+ result.skipped++;
563
+ continue;
564
+ }
565
+ const newJobId = this.generateId('job_resumed');
566
+ result.resumed++;
567
+ result.jobs.push({
568
+ originalJobId: errorLeaf.log.job_id,
569
+ newJobId,
570
+ kladosId: errorLeaf.log.klados_id,
571
+ errorLogId: errorLeaf.log.id,
572
+ targetEntityId: String(errorLeaf.log.received?.target ?? ''),
573
+ });
574
+ }
575
+ this.log('resumeWorkflow', { jobId, resumed: result.resumed });
576
+ return { data: result };
577
+ }
578
+ // =========================================================================
579
+ // Log Operations
580
+ // =========================================================================
581
+ async createLog(params) {
582
+ const id = this.generateId('log');
583
+ const now = new Date().toISOString();
584
+ // Build log entry - simplified for mock
585
+ const logProperties = {
586
+ id,
587
+ klados_id: params.kladosId,
588
+ rhiza_id: params.rhizaId,
589
+ job_id: params.jobId,
590
+ status: 'running',
591
+ started_at: now,
592
+ received: {
593
+ target: params.received.target,
594
+ input: params.received.input,
595
+ from_logs: params.received.fromLogs,
596
+ },
597
+ };
598
+ const result = await this.createEntity({
599
+ collectionId: params.jobCollectionId,
600
+ type: 'klados_log',
601
+ properties: logProperties,
602
+ relationships: params.parentLogIds?.map((parentId) => ({
603
+ predicate: 'received_from',
604
+ peer: parentId,
605
+ })),
606
+ });
607
+ if (result.error)
608
+ return asError(result);
609
+ this.log('createLog', { id, kladosId: params.kladosId });
610
+ return { data: logProperties };
611
+ }
612
+ async updateLog(id, params) {
613
+ const entity = this.entities.get(id);
614
+ if (!entity) {
615
+ return { error: { code: 'NOT_FOUND', message: `Log ${id} not found` } };
616
+ }
617
+ const properties = {};
618
+ if (params.status !== undefined)
619
+ properties.status = params.status;
620
+ if (params.completedAt !== undefined)
621
+ properties.completed_at = params.completedAt;
622
+ if (params.produced !== undefined)
623
+ properties.produced = params.produced;
624
+ if (params.error !== undefined)
625
+ properties.error = params.error;
626
+ if (params.handoffs !== undefined)
627
+ properties.handoffs = params.handoffs;
628
+ const result = await this.updateEntity(id, {
629
+ expectTip: params.expectTip,
630
+ properties,
631
+ });
632
+ if (result.error)
633
+ return asError(result);
634
+ this.log('updateLog', { id, status: params.status });
635
+ return {
636
+ data: {
637
+ ...entity.properties,
638
+ ...properties,
639
+ },
640
+ };
641
+ }
642
+ async getJobLogs(jobCollectionId) {
643
+ const collectionEntities = this.collections.get(jobCollectionId);
644
+ if (!collectionEntities) {
645
+ return { data: [] };
646
+ }
647
+ const logs = [];
648
+ for (const entityId of collectionEntities) {
649
+ const entity = this.entities.get(entityId);
650
+ if (entity && entity.type === 'klados_log') {
651
+ logs.push(entity.properties);
652
+ }
653
+ }
654
+ return { data: logs };
655
+ }
656
+ // =========================================================================
657
+ // Batch Operations
658
+ // =========================================================================
659
+ async createBatch(params) {
660
+ const result = await this.createEntity({
661
+ collectionId: params.jobCollectionId,
662
+ type: 'batch',
663
+ properties: {
664
+ scatter_from: params.scatterFrom,
665
+ gather_target: params.gatherTarget,
666
+ total_slots: params.totalSlots,
667
+ completed_slots: 0,
668
+ status: 'pending',
669
+ slots: params.slots,
670
+ },
671
+ });
672
+ if (result.error)
673
+ return asError(result);
674
+ this.log('createBatch', {
675
+ id: result.data.id,
676
+ totalSlots: params.totalSlots,
677
+ });
678
+ return {
679
+ data: {
680
+ id: result.data.id,
681
+ cid: result.data.cid,
682
+ type: 'batch',
683
+ properties: result.data.properties,
684
+ },
685
+ };
686
+ }
687
+ async updateBatch(id, params) {
688
+ const current = await this.getBatch(id);
689
+ if (current.error)
690
+ return current;
691
+ const properties = {};
692
+ if (params.properties.completedSlots !== undefined)
693
+ properties.completed_slots = params.properties.completedSlots;
694
+ if (params.properties.status !== undefined)
695
+ properties.status = params.properties.status;
696
+ if (params.properties.slots !== undefined)
697
+ properties.slots = params.properties.slots;
698
+ if (params.properties.completedAt !== undefined)
699
+ properties.completed_at = params.properties.completedAt;
700
+ if (params.properties.error !== undefined)
701
+ properties.error = params.properties.error;
702
+ const result = await this.updateEntity(id, {
703
+ expectTip: params.expectTip,
704
+ properties,
705
+ });
706
+ if (result.error)
707
+ return asError(result);
708
+ this.log('updateBatch', { id, status: params.properties.status });
709
+ return this.getBatch(id);
710
+ }
711
+ async getBatch(id) {
712
+ const entity = this.entities.get(id);
713
+ if (!entity) {
714
+ return { error: { code: 'NOT_FOUND', message: `Entity ${id} not found` } };
715
+ }
716
+ if (entity.type !== 'batch') {
717
+ return {
718
+ error: { code: 'TYPE_MISMATCH', message: `Entity ${id} is not a batch` },
719
+ };
720
+ }
721
+ return {
722
+ data: {
723
+ id: entity.id,
724
+ cid: entity.cid,
725
+ type: 'batch',
726
+ properties: entity.properties,
727
+ },
728
+ };
729
+ }
730
+ // =========================================================================
731
+ // Test Helpers (not part of interface)
732
+ // =========================================================================
733
+ /**
734
+ * Reset all state (for testing)
735
+ */
736
+ reset() {
737
+ this.entities.clear();
738
+ this.collections.clear();
739
+ this.pendingVerifications.clear();
740
+ this.idCounter = 0;
741
+ }
742
+ /**
743
+ * Get all entities of a type (for testing)
744
+ */
745
+ getEntitiesByType(type) {
746
+ return Array.from(this.entities.values()).filter((e) => e.type === type);
747
+ }
748
+ /**
749
+ * Seed an entity directly (for testing)
750
+ */
751
+ seedEntity(entity) {
752
+ this.entities.set(entity.id, entity);
753
+ this.addToCollection(entity.collectionId, entity.id);
754
+ }
755
+ // =========================================================================
756
+ // Private Helpers
757
+ // =========================================================================
758
+ generateId(prefix) {
759
+ this.idCounter++;
760
+ return `${prefix}_mock_${this.idCounter}_${Date.now()}`;
761
+ }
762
+ generateCid() {
763
+ return `bafk_mock_${Date.now()}_${Math.random().toString(36).slice(2)}`;
764
+ }
765
+ addToCollection(collectionId, entityId) {
766
+ let collection = this.collections.get(collectionId);
767
+ if (!collection) {
768
+ collection = new Set();
769
+ this.collections.set(collectionId, collection);
770
+ }
771
+ collection.add(entityId);
772
+ }
773
+ toEntityResponse(entity) {
774
+ return {
775
+ id: entity.id,
776
+ cid: entity.cid,
777
+ type: entity.type,
778
+ properties: entity.properties,
779
+ created_at: entity.createdAt,
780
+ updated_at: entity.updatedAt,
781
+ };
782
+ }
783
+ log(method, data) {
784
+ if (this.debug) {
785
+ console.log(`[MockRhizaClient.${method}]`, data);
786
+ }
787
+ }
788
+ }
789
+ /**
790
+ * Create a mock client with optional pre-seeded data
791
+ */
792
+ export function createMockRhizaClient(config) {
793
+ return new MockRhizaClient(config);
794
+ }
795
+ //# sourceMappingURL=mock.js.map