@elevasis/core 0.15.1 → 0.17.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 (72) hide show
  1. package/dist/index.d.ts +1662 -23
  2. package/dist/index.js +171 -24
  3. package/dist/knowledge/index.d.ts +1340 -0
  4. package/dist/knowledge/index.js +138 -0
  5. package/dist/organization-model/index.d.ts +1662 -23
  6. package/dist/organization-model/index.js +171 -24
  7. package/dist/test-utils/index.d.ts +711 -10
  8. package/dist/test-utils/index.js +159 -16
  9. package/package.json +7 -3
  10. package/src/__tests__/publish.test.ts +14 -13
  11. package/src/__tests__/template-core-compatibility.test.ts +4 -4
  12. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1265 -1154
  13. package/src/auth/multi-tenancy/index.ts +3 -0
  14. package/src/auth/multi-tenancy/theme-presets.ts +45 -0
  15. package/src/auth/multi-tenancy/types.ts +57 -83
  16. package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
  17. package/src/business/acquisition/activity-events.ts +1 -1
  18. package/src/business/acquisition/api-schemas.ts +1196 -1177
  19. package/src/business/acquisition/crm-state-actions.test.ts +139 -139
  20. package/src/business/acquisition/types.ts +381 -390
  21. package/src/business/crm/api-schemas.ts +40 -0
  22. package/src/business/crm/index.ts +1 -0
  23. package/src/business/deals/api-schemas.ts +79 -0
  24. package/src/business/deals/index.ts +1 -0
  25. package/src/business/projects/types.ts +124 -88
  26. package/src/execution/core/runner-types.ts +61 -80
  27. package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
  28. package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
  29. package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
  30. package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
  31. package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
  32. package/src/execution/engine/tools/integration/tool.ts +255 -253
  33. package/src/execution/engine/tools/lead-service-types.ts +895 -894
  34. package/src/execution/engine/tools/messages.ts +43 -0
  35. package/src/execution/engine/tools/platform/acquisition/types.ts +2 -1
  36. package/src/execution/engine/tools/platform/email/types.ts +97 -96
  37. package/src/execution/engine/tools/types.ts +234 -233
  38. package/src/execution/engine/workflow/types.ts +195 -193
  39. package/src/execution/external/api-schemas.ts +40 -0
  40. package/src/execution/external/index.ts +1 -0
  41. package/src/knowledge/README.md +32 -0
  42. package/src/knowledge/__tests__/queries.test.ts +504 -0
  43. package/src/knowledge/format.ts +99 -0
  44. package/src/knowledge/index.ts +5 -0
  45. package/src/knowledge/published.ts +5 -0
  46. package/src/knowledge/queries.ts +256 -0
  47. package/src/organization-model/__tests__/defaults.test.ts +172 -172
  48. package/src/organization-model/__tests__/foundation.test.ts +7 -7
  49. package/src/organization-model/__tests__/icons.test.ts +27 -0
  50. package/src/organization-model/__tests__/knowledge.test.ts +214 -0
  51. package/src/organization-model/contracts.ts +17 -15
  52. package/src/organization-model/defaults.ts +74 -19
  53. package/src/organization-model/domains/knowledge.ts +53 -0
  54. package/src/organization-model/domains/navigation.ts +416 -399
  55. package/src/organization-model/domains/shared.ts +6 -5
  56. package/src/organization-model/foundation.ts +10 -6
  57. package/src/organization-model/graph/build.ts +209 -182
  58. package/src/organization-model/graph/schema.ts +37 -34
  59. package/src/organization-model/graph/types.ts +47 -31
  60. package/src/organization-model/icons.ts +81 -0
  61. package/src/organization-model/index.ts +8 -3
  62. package/src/organization-model/organization-model.mdx +1 -1
  63. package/src/organization-model/published.ts +103 -86
  64. package/src/organization-model/schema.ts +90 -85
  65. package/src/organization-model/types.ts +40 -33
  66. package/src/platform/index.ts +23 -27
  67. package/src/platform/registry/index.ts +0 -4
  68. package/src/platform/registry/resource-registry.ts +0 -77
  69. package/src/platform/registry/serialized-types.ts +148 -219
  70. package/src/platform/registry/stats-types.ts +60 -60
  71. package/src/reference/_generated/contracts.md +1265 -1154
  72. package/src/platform/registry/__tests__/resource-registry.list-executable.test.ts +0 -393
@@ -0,0 +1,504 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { byFeature, byKind, byOwner, governs, governedBy, parsePath } from '../queries'
3
+ import { formatText, formatJson, formatIdsOnly } from '../format'
4
+ import type { OrganizationGraph } from '../../organization-model/graph/types'
5
+ import type { OrgKnowledgeNode } from '../../organization-model/domains/knowledge'
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Synthetic fixtures
9
+ // ---------------------------------------------------------------------------
10
+
11
+ /**
12
+ * Three knowledge nodes covering all three kinds.
13
+ * - playbook-a governs feature:sales.crm and feature:sales.lead-gen
14
+ * - strategy-b governs feature:sales.lead-gen
15
+ * - reference-c has no links (no governs edges)
16
+ * - playbook-d is owned by "role.ops-lead"
17
+ */
18
+ const NODES: OrgKnowledgeNode[] = [
19
+ {
20
+ id: 'knowledge.playbook-a',
21
+ kind: 'playbook',
22
+ title: 'Playbook A',
23
+ summary: 'Playbook A summary.',
24
+ body: '## Playbook A\n\nContent.',
25
+ links: [{ nodeId: 'feature:sales.crm' }, { nodeId: 'feature:sales.lead-gen' }],
26
+ ownerIds: [],
27
+ updatedAt: '2026-01-01'
28
+ },
29
+ {
30
+ id: 'knowledge.strategy-b',
31
+ kind: 'strategy',
32
+ title: 'Strategy B',
33
+ summary: 'Strategy B summary.',
34
+ body: '## Strategy B\n\nContent.',
35
+ links: [{ nodeId: 'feature:sales.lead-gen' }],
36
+ ownerIds: ['role.ops-lead'],
37
+ updatedAt: '2026-01-02'
38
+ },
39
+ {
40
+ id: 'knowledge.reference-c',
41
+ kind: 'reference',
42
+ title: 'Reference C',
43
+ summary: 'Reference C summary.',
44
+ body: '## Reference C\n\nContent.',
45
+ links: [],
46
+ ownerIds: [],
47
+ updatedAt: '2026-01-03'
48
+ },
49
+ {
50
+ id: 'knowledge.playbook-d',
51
+ kind: 'playbook',
52
+ title: 'Playbook D',
53
+ summary: 'Playbook D summary.',
54
+ body: '## Playbook D\n\nContent.',
55
+ links: [],
56
+ ownerIds: ['role.ops-lead', 'role.ceo'],
57
+ updatedAt: '2026-01-04'
58
+ }
59
+ ]
60
+
61
+ /**
62
+ * Minimal synthetic OrganizationGraph derived from the above nodes.
63
+ * Graph node IDs follow the convention: `knowledge:<om-node-id>`
64
+ */
65
+ const GRAPH: OrganizationGraph = {
66
+ version: 1,
67
+ organizationModelVersion: 1,
68
+ nodes: [
69
+ { id: 'organization-model', kind: 'organization', label: 'Organization Model' },
70
+ { id: 'feature:sales.crm', kind: 'feature', label: 'CRM', sourceId: 'sales.crm', featureId: 'sales.crm' },
71
+ {
72
+ id: 'feature:sales.lead-gen',
73
+ kind: 'feature',
74
+ label: 'Lead Gen',
75
+ sourceId: 'sales.lead-gen',
76
+ featureId: 'sales.lead-gen'
77
+ },
78
+ {
79
+ id: 'knowledge:knowledge.playbook-a',
80
+ kind: 'knowledge',
81
+ label: 'Playbook A',
82
+ sourceId: 'knowledge.playbook-a'
83
+ },
84
+ {
85
+ id: 'knowledge:knowledge.strategy-b',
86
+ kind: 'knowledge',
87
+ label: 'Strategy B',
88
+ sourceId: 'knowledge.strategy-b'
89
+ },
90
+ {
91
+ id: 'knowledge:knowledge.reference-c',
92
+ kind: 'knowledge',
93
+ label: 'Reference C',
94
+ sourceId: 'knowledge.reference-c'
95
+ },
96
+ {
97
+ id: 'knowledge:knowledge.playbook-d',
98
+ kind: 'knowledge',
99
+ label: 'Playbook D',
100
+ sourceId: 'knowledge.playbook-d'
101
+ }
102
+ ],
103
+ edges: [
104
+ // contains edges (organization -> knowledge nodes)
105
+ {
106
+ id: 'edge:contains:organization-model:knowledge:knowledge.playbook-a',
107
+ kind: 'contains',
108
+ sourceId: 'organization-model',
109
+ targetId: 'knowledge:knowledge.playbook-a'
110
+ },
111
+ {
112
+ id: 'edge:contains:organization-model:knowledge:knowledge.strategy-b',
113
+ kind: 'contains',
114
+ sourceId: 'organization-model',
115
+ targetId: 'knowledge:knowledge.strategy-b'
116
+ },
117
+ {
118
+ id: 'edge:contains:organization-model:knowledge:knowledge.reference-c',
119
+ kind: 'contains',
120
+ sourceId: 'organization-model',
121
+ targetId: 'knowledge:knowledge.reference-c'
122
+ },
123
+ {
124
+ id: 'edge:contains:organization-model:knowledge:knowledge.playbook-d',
125
+ kind: 'contains',
126
+ sourceId: 'organization-model',
127
+ targetId: 'knowledge:knowledge.playbook-d'
128
+ },
129
+ // governs edges
130
+ {
131
+ id: 'edge:governs:knowledge:knowledge.playbook-a:feature:sales.crm',
132
+ kind: 'governs',
133
+ sourceId: 'knowledge:knowledge.playbook-a',
134
+ targetId: 'feature:sales.crm'
135
+ },
136
+ {
137
+ id: 'edge:governs:knowledge:knowledge.playbook-a:feature:sales.lead-gen',
138
+ kind: 'governs',
139
+ sourceId: 'knowledge:knowledge.playbook-a',
140
+ targetId: 'feature:sales.lead-gen'
141
+ },
142
+ {
143
+ id: 'edge:governs:knowledge:knowledge.strategy-b:feature:sales.lead-gen',
144
+ kind: 'governs',
145
+ sourceId: 'knowledge:knowledge.strategy-b',
146
+ targetId: 'feature:sales.lead-gen'
147
+ }
148
+ ]
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // byFeature
153
+ // ---------------------------------------------------------------------------
154
+
155
+ describe('byFeature', () => {
156
+ it('returns nodes that have governs edges to the given feature', () => {
157
+ const result = byFeature(GRAPH, 'sales.crm', NODES)
158
+ expect(result).toHaveLength(1)
159
+ expect(result[0].id).toBe('knowledge.playbook-a')
160
+ })
161
+
162
+ it('returns multiple nodes when multiple knowledge nodes govern the same feature', () => {
163
+ const result = byFeature(GRAPH, 'sales.lead-gen', NODES)
164
+ const ids = result.map((n) => n.id).sort()
165
+ expect(ids).toEqual(['knowledge.playbook-a', 'knowledge.strategy-b'].sort())
166
+ })
167
+
168
+ it('returns empty array when no node governs the feature', () => {
169
+ const result = byFeature(GRAPH, 'feature.does-not-exist', NODES)
170
+ expect(result).toHaveLength(0)
171
+ })
172
+
173
+ it('returns full OrgKnowledgeNode objects (not just graph stubs)', () => {
174
+ const result = byFeature(GRAPH, 'sales.crm', NODES)
175
+ expect(result[0].body).toBeDefined()
176
+ expect(result[0].links).toBeDefined()
177
+ expect(result[0].ownerIds).toBeDefined()
178
+ })
179
+ })
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // byKind
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe('byKind', () => {
186
+ it('returns all playbook nodes', () => {
187
+ const result = byKind(GRAPH, 'playbook', NODES)
188
+ expect(result).toHaveLength(2)
189
+ expect(result.every((n) => n.kind === 'playbook')).toBe(true)
190
+ })
191
+
192
+ it('returns strategy nodes', () => {
193
+ const result = byKind(GRAPH, 'strategy', NODES)
194
+ expect(result).toHaveLength(1)
195
+ expect(result[0].id).toBe('knowledge.strategy-b')
196
+ })
197
+
198
+ it('returns reference nodes', () => {
199
+ const result = byKind(GRAPH, 'reference', NODES)
200
+ expect(result).toHaveLength(1)
201
+ expect(result[0].id).toBe('knowledge.reference-c')
202
+ })
203
+
204
+ it('returns empty array when no nodes match the kind', () => {
205
+ const result = byKind(GRAPH, 'playbook', [])
206
+ expect(result).toHaveLength(0)
207
+ })
208
+ })
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // byOwner
212
+ // ---------------------------------------------------------------------------
213
+
214
+ describe('byOwner', () => {
215
+ it('returns nodes where ownerIds includes the given owner', () => {
216
+ const result = byOwner(GRAPH, 'role.ops-lead', NODES)
217
+ const ids = result.map((n) => n.id).sort()
218
+ expect(ids).toEqual(['knowledge.playbook-d', 'knowledge.strategy-b'].sort())
219
+ })
220
+
221
+ it('returns nodes for a second owner', () => {
222
+ const result = byOwner(GRAPH, 'role.ceo', NODES)
223
+ expect(result).toHaveLength(1)
224
+ expect(result[0].id).toBe('knowledge.playbook-d')
225
+ })
226
+
227
+ it('returns empty array when ownerId is not present in any node', () => {
228
+ const result = byOwner(GRAPH, 'role.unknown', NODES)
229
+ expect(result).toHaveLength(0)
230
+ })
231
+ })
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // governs
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe('governs', () => {
238
+ it('returns target graph node IDs for outgoing governs edges (OM node id input)', () => {
239
+ const result = governs(GRAPH, 'knowledge.playbook-a')
240
+ expect(result.sort()).toEqual(['feature:sales.crm', 'feature:sales.lead-gen'].sort())
241
+ })
242
+
243
+ it('accepts graph node ID format input', () => {
244
+ const result = governs(GRAPH, 'knowledge:knowledge.playbook-a')
245
+ expect(result.sort()).toEqual(['feature:sales.crm', 'feature:sales.lead-gen'].sort())
246
+ })
247
+
248
+ it('returns single target for a node with one link', () => {
249
+ const result = governs(GRAPH, 'knowledge.strategy-b')
250
+ expect(result).toEqual(['feature:sales.lead-gen'])
251
+ })
252
+
253
+ it('returns empty array for a node with no governs edges', () => {
254
+ const result = governs(GRAPH, 'knowledge.reference-c')
255
+ expect(result).toHaveLength(0)
256
+ })
257
+
258
+ it('returns empty array for an unknown nodeId', () => {
259
+ const result = governs(GRAPH, 'knowledge.does-not-exist')
260
+ expect(result).toHaveLength(0)
261
+ })
262
+ })
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // governedBy
266
+ // ---------------------------------------------------------------------------
267
+
268
+ describe('governedBy', () => {
269
+ it('returns knowledge graph node IDs for incoming governs edges (prefixed feature id)', () => {
270
+ const result = governedBy(GRAPH, 'feature:sales.lead-gen')
271
+ const sorted = result.sort()
272
+ expect(sorted).toEqual(['knowledge:knowledge.playbook-a', 'knowledge:knowledge.strategy-b'].sort())
273
+ })
274
+
275
+ it('accepts bare feature id (auto-prefixes with feature:)', () => {
276
+ const result = governedBy(GRAPH, 'sales.crm')
277
+ expect(result).toEqual(['knowledge:knowledge.playbook-a'])
278
+ })
279
+
280
+ it('returns empty array when no knowledge node governs the target', () => {
281
+ const result = governedBy(GRAPH, 'feature:dashboard')
282
+ expect(result).toHaveLength(0)
283
+ })
284
+
285
+ it('returns empty array for unknown target node id', () => {
286
+ const result = governedBy(GRAPH, 'feature:does-not-exist')
287
+ expect(result).toHaveLength(0)
288
+ })
289
+ })
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // parsePath — happy paths (all five mount axes)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ describe('parsePath — happy paths', () => {
296
+ it('/by-kind/playbook → mount:by-kind args:[playbook]', () => {
297
+ expect(parsePath('/by-kind/playbook')).toEqual({ mount: 'by-kind', args: ['playbook'] })
298
+ })
299
+
300
+ it('/by-kind/strategy → mount:by-kind args:[strategy]', () => {
301
+ expect(parsePath('/by-kind/strategy')).toEqual({ mount: 'by-kind', args: ['strategy'] })
302
+ })
303
+
304
+ it('/by-feature/sales.crm → mount:by-feature args:[sales.crm]', () => {
305
+ expect(parsePath('/by-feature/sales.crm')).toEqual({ mount: 'by-feature', args: ['sales.crm'] })
306
+ })
307
+
308
+ it('/by-feature/sales/crm (slash-delimited) → mount:by-feature args:[sales/crm]', () => {
309
+ // featureId with slashes is joined and passed as-is for the caller to interpret
310
+ expect(parsePath('/by-feature/sales/crm')).toEqual({ mount: 'by-feature', args: ['sales/crm'] })
311
+ })
312
+
313
+ it('/by-owner/role.ops-lead → mount:by-owner args:[role.ops-lead]', () => {
314
+ expect(parsePath('/by-owner/role.ops-lead')).toEqual({ mount: 'by-owner', args: ['role.ops-lead'] })
315
+ })
316
+
317
+ it('/graph/knowledge.outreach-playbook/governs → mount:graph args:[knowledge.outreach-playbook,governs]', () => {
318
+ expect(parsePath('/graph/knowledge.outreach-playbook/governs')).toEqual({
319
+ mount: 'graph',
320
+ args: ['knowledge.outreach-playbook', 'governs']
321
+ })
322
+ })
323
+
324
+ it('/graph/knowledge.outreach-playbook/governed-by → mount:graph args:[knowledge.outreach-playbook,governed-by]', () => {
325
+ expect(parsePath('/graph/knowledge.outreach-playbook/governed-by')).toEqual({
326
+ mount: 'graph',
327
+ args: ['knowledge.outreach-playbook', 'governed-by']
328
+ })
329
+ })
330
+
331
+ it('/<nodeId> single segment → mount:node args:[nodeId]', () => {
332
+ expect(parsePath('/knowledge.outreach-playbook')).toEqual({
333
+ mount: 'node',
334
+ args: ['knowledge.outreach-playbook']
335
+ })
336
+ })
337
+
338
+ it('trailing slash is stripped before parsing', () => {
339
+ expect(parsePath('/by-kind/playbook/')).toEqual({ mount: 'by-kind', args: ['playbook'] })
340
+ })
341
+ })
342
+
343
+ // ---------------------------------------------------------------------------
344
+ // parsePath — invalid inputs (5+)
345
+ // ---------------------------------------------------------------------------
346
+
347
+ describe('parsePath — invalid inputs', () => {
348
+ it('throws on empty string', () => {
349
+ expect(() => parsePath('')).toThrow('parsePath: path must be a non-empty string')
350
+ })
351
+
352
+ it('throws when path does not start with /', () => {
353
+ expect(() => parsePath('by-kind/playbook')).toThrow('parsePath: path must start with "/"')
354
+ })
355
+
356
+ it('throws on bare root path "/"', () => {
357
+ expect(() => parsePath('/')).toThrow('parsePath: path resolves to root with no mount')
358
+ })
359
+
360
+ it('throws on /by-feature with no featureId argument', () => {
361
+ expect(() => parsePath('/by-feature')).toThrow('/by-feature requires a featureId argument')
362
+ })
363
+
364
+ it('throws on /by-kind with no kind argument', () => {
365
+ expect(() => parsePath('/by-kind')).toThrow('/by-kind requires a kind argument')
366
+ })
367
+
368
+ it('throws on /by-owner with no ownerId argument', () => {
369
+ expect(() => parsePath('/by-owner')).toThrow('/by-owner requires an ownerId argument')
370
+ })
371
+
372
+ it('throws on /graph with only nodeId (missing verb)', () => {
373
+ expect(() => parsePath('/graph/knowledge.outreach-playbook')).toThrow('/graph requires <nodeId>/<verb>')
374
+ })
375
+
376
+ it('throws on /graph/<nodeId>/<unknown-verb>', () => {
377
+ expect(() => parsePath('/graph/knowledge.outreach-playbook/list')).toThrow(
378
+ 'verb must be "governs" or "governed-by"'
379
+ )
380
+ })
381
+
382
+ it('throws on multi-segment path with unrecognized first segment', () => {
383
+ expect(() => parsePath('/unknown-mount/some-arg')).toThrow('unrecognized path pattern')
384
+ })
385
+ })
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // formatText
389
+ // ---------------------------------------------------------------------------
390
+
391
+ describe('formatText', () => {
392
+ it('returns "(no results)" for empty array', () => {
393
+ expect(formatText([])).toBe('(no results)')
394
+ })
395
+
396
+ it('includes header row with KIND and ID columns', () => {
397
+ const output = formatText([NODES[0]])
398
+ expect(output).toContain('KIND')
399
+ expect(output).toContain('ID')
400
+ expect(output).toContain('TITLE')
401
+ })
402
+
403
+ it('includes node id and title in output', () => {
404
+ const output = formatText([NODES[0]])
405
+ expect(output).toContain('knowledge.playbook-a')
406
+ expect(output).toContain('Playbook A')
407
+ })
408
+
409
+ it('truncates long summaries', () => {
410
+ const longSummary = 'x'.repeat(200)
411
+ const node: OrgKnowledgeNode = { ...NODES[0], summary: longSummary }
412
+ const output = formatText([node])
413
+ expect(output).toContain('...')
414
+ })
415
+ })
416
+
417
+ // ---------------------------------------------------------------------------
418
+ // formatJson — envelope shape
419
+ // ---------------------------------------------------------------------------
420
+
421
+ describe('formatJson', () => {
422
+ it('returns a JSON string with top-level keys: path, mount, args, results', () => {
423
+ const raw = formatJson({
424
+ path: '/by-kind/playbook',
425
+ parsed: { mount: 'by-kind', args: ['playbook'] },
426
+ results: [NODES[0]]
427
+ })
428
+ const parsed = JSON.parse(raw) as Record<string, unknown>
429
+ expect(Object.keys(parsed).sort()).toEqual(['args', 'mount', 'path', 'results'].sort())
430
+ })
431
+
432
+ it('preserves path in envelope', () => {
433
+ const raw = formatJson({
434
+ path: '/by-kind/playbook',
435
+ parsed: { mount: 'by-kind', args: ['playbook'] },
436
+ results: []
437
+ })
438
+ expect(JSON.parse(raw)).toMatchObject({ path: '/by-kind/playbook' })
439
+ })
440
+
441
+ it('preserves mount in envelope', () => {
442
+ const raw = formatJson({
443
+ path: '/by-kind/strategy',
444
+ parsed: { mount: 'by-kind', args: ['strategy'] },
445
+ results: []
446
+ })
447
+ expect(JSON.parse(raw)).toMatchObject({ mount: 'by-kind' })
448
+ })
449
+
450
+ it('preserves args array in envelope', () => {
451
+ const raw = formatJson({
452
+ path: '/by-feature/sales.crm',
453
+ parsed: { mount: 'by-feature', args: ['sales.crm'] },
454
+ results: []
455
+ })
456
+ expect(JSON.parse(raw)).toMatchObject({ args: ['sales.crm'] })
457
+ })
458
+
459
+ it('includes results in envelope for string[] (governs/governedBy)', () => {
460
+ const raw = formatJson({
461
+ path: '/graph/knowledge.playbook-a/governs',
462
+ parsed: { mount: 'graph', args: ['knowledge.playbook-a', 'governs'] },
463
+ results: ['feature:sales.crm', 'feature:sales.lead-gen']
464
+ })
465
+ const parsed = JSON.parse(raw) as { results: string[] }
466
+ expect(parsed.results).toEqual(['feature:sales.crm', 'feature:sales.lead-gen'])
467
+ })
468
+
469
+ it('envelope is NOT flat { results } — must have path/mount/args siblings', () => {
470
+ const raw = formatJson({
471
+ path: '/by-kind/playbook',
472
+ parsed: { mount: 'by-kind', args: ['playbook'] },
473
+ results: [NODES[0]]
474
+ })
475
+ const parsed = JSON.parse(raw) as Record<string, unknown>
476
+ // Must NOT be flat (only results key)
477
+ expect(parsed).toHaveProperty('path')
478
+ expect(parsed).toHaveProperty('mount')
479
+ expect(parsed).toHaveProperty('args')
480
+ expect(parsed).toHaveProperty('results')
481
+ })
482
+ })
483
+
484
+ // ---------------------------------------------------------------------------
485
+ // formatIdsOnly
486
+ // ---------------------------------------------------------------------------
487
+
488
+ describe('formatIdsOnly', () => {
489
+ it('returns empty string for empty array', () => {
490
+ expect(formatIdsOnly([])).toBe('')
491
+ })
492
+
493
+ it('returns newline-separated ids for OrgKnowledgeNode array', () => {
494
+ const output = formatIdsOnly([NODES[0], NODES[1]])
495
+ const lines = output.split('\n')
496
+ expect(lines).toContain('knowledge.playbook-a')
497
+ expect(lines).toContain('knowledge.strategy-b')
498
+ })
499
+
500
+ it('returns newline-separated strings for string[] (governs results)', () => {
501
+ const output = formatIdsOnly(['feature:sales.crm', 'feature:sales.lead-gen'])
502
+ expect(output).toBe('feature:sales.crm\nfeature:sales.lead-gen')
503
+ })
504
+ })
@@ -0,0 +1,99 @@
1
+ import type { OrgKnowledgeNode } from '../organization-model/domains/knowledge'
2
+ import type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // formatText
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * Renders a list of `OrgKnowledgeNode` results as a human-friendly text table.
10
+ *
11
+ * Output format (one row per node):
12
+ * `<kind> <id> <title> — <summary (truncated to 80 chars)>`
13
+ *
14
+ * Returns `"(no results)"` when the array is empty.
15
+ */
16
+ export function formatText(results: OrgKnowledgeNode[]): string {
17
+ if (results.length === 0) {
18
+ return '(no results)'
19
+ }
20
+
21
+ const kindWidth = Math.max(...results.map((n) => n.kind.length), 8)
22
+ const idWidth = Math.max(...results.map((n) => n.id.length), 4)
23
+
24
+ const header = `${'KIND'.padEnd(kindWidth)} ${'ID'.padEnd(idWidth)} TITLE`
25
+ const divider = '-'.repeat(header.length + 20)
26
+
27
+ const rows = results.map((n) => {
28
+ const summary = n.summary.length > 80 ? n.summary.slice(0, 77) + '...' : n.summary
29
+ return `${n.kind.padEnd(kindWidth)} ${n.id.padEnd(idWidth)} ${n.title} — ${summary}`
30
+ })
31
+
32
+ return [header, divider, ...rows].join('\n')
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // formatJson
37
+ // ---------------------------------------------------------------------------
38
+
39
+ /**
40
+ * Wrapped JSON envelope for machine-readable output.
41
+ *
42
+ * Shape: `{ path, mount, args, results }`
43
+ *
44
+ * - `path` — the original path string passed to `parsePath`
45
+ * - `mount` — the resolved mount axis
46
+ * - `args` — the parsed arguments array
47
+ * - `results` — the query results (array of `OrgKnowledgeNode` or string IDs)
48
+ */
49
+ export interface KnowledgeJsonEnvelope {
50
+ path: string
51
+ mount: KnowledgeMount
52
+ args: string[]
53
+ results: OrgKnowledgeNode[] | string[]
54
+ }
55
+
56
+ /**
57
+ * Formats query results as a wrapped JSON envelope string.
58
+ *
59
+ * The envelope shape is `{ path, mount, args, results }`. This is intentionally
60
+ * NOT flat `{ results }` — consumers (agent skills, jq pipelines) need the
61
+ * mount + args to know how to interpret the results array.
62
+ *
63
+ * @param input.path - The original path string (e.g. `"/by-feature/sales.crm"`).
64
+ * @param input.mount - The resolved mount axis.
65
+ * @param input.args - The parsed argument array.
66
+ * @param input.results - The query results.
67
+ */
68
+ export function formatJson(input: {
69
+ path: string
70
+ parsed: ParsedKnowledgePath
71
+ results: OrgKnowledgeNode[] | string[]
72
+ }): string {
73
+ const envelope: KnowledgeJsonEnvelope = {
74
+ path: input.path,
75
+ mount: input.parsed.mount,
76
+ args: input.parsed.args,
77
+ results: input.results
78
+ }
79
+ return JSON.stringify(envelope, null, 2)
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // formatIdsOnly
84
+ // ---------------------------------------------------------------------------
85
+
86
+ /**
87
+ * Renders results as newline-separated IDs for piping.
88
+ *
89
+ * - For `OrgKnowledgeNode[]` results: emits `node.id` per line.
90
+ * - For `string[]` results (governs / governedBy): emits each string per line.
91
+ *
92
+ * Returns an empty string when the array is empty (suitable for `wc -l` piping).
93
+ */
94
+ export function formatIdsOnly(results: OrgKnowledgeNode[] | string[]): string {
95
+ if (results.length === 0) return ''
96
+
97
+ const ids = results.map((r) => (typeof r === 'string' ? r : r.id))
98
+ return ids.join('\n')
99
+ }
@@ -0,0 +1,5 @@
1
+ export { byFeature, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
+ export type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
+
4
+ export { formatText, formatJson, formatIdsOnly } from './format'
5
+ export type { KnowledgeJsonEnvelope } from './format'
@@ -0,0 +1,5 @@
1
+ export { byFeature, byKind, byOwner, governs, governedBy, parsePath } from './queries'
2
+ export type { KnowledgeMount, ParsedKnowledgePath } from './queries'
3
+
4
+ export { formatText, formatJson, formatIdsOnly } from './format'
5
+ export type { KnowledgeJsonEnvelope } from './format'