@elevasis/core 0.15.0 → 0.16.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.
- package/dist/index.d.ts +1718 -19
- package/dist/index.js +369 -25
- package/dist/organization-model/index.d.ts +1718 -19
- package/dist/organization-model/index.js +369 -25
- package/dist/test-utils/index.d.ts +1108 -371
- package/dist/test-utils/index.js +357 -17
- package/package.json +5 -1
- package/src/__tests__/publish.test.ts +14 -13
- package/src/__tests__/template-core-compatibility.test.ts +4 -4
- package/src/_gen/__tests__/__snapshots__/contracts.md.snap +1109 -882
- package/src/auth/multi-tenancy/index.ts +3 -0
- package/src/auth/multi-tenancy/theme-presets.ts +45 -0
- package/src/auth/multi-tenancy/types.ts +57 -83
- package/src/auth/multi-tenancy/users/api-schemas.ts +165 -194
- package/src/business/acquisition/activity-events.ts +13 -4
- package/src/business/acquisition/api-schemas.test.ts +315 -4
- package/src/business/acquisition/api-schemas.ts +122 -8
- package/src/business/acquisition/build-templates.ts +44 -0
- package/src/business/acquisition/crm-next-action.test.ts +262 -0
- package/src/business/acquisition/crm-next-action.ts +220 -0
- package/src/business/acquisition/crm-priority.test.ts +216 -0
- package/src/business/acquisition/crm-priority.ts +349 -0
- package/src/business/acquisition/crm-state-actions.test.ts +151 -160
- package/src/business/acquisition/deal-ownership.test.ts +351 -0
- package/src/business/acquisition/deal-ownership.ts +120 -0
- package/src/business/acquisition/derive-actions.test.ts +101 -37
- package/src/business/acquisition/derive-actions.ts +102 -87
- package/src/business/acquisition/index.ts +10 -0
- package/src/business/acquisition/types.ts +400 -366
- package/src/business/crm/api-schemas.ts +40 -0
- package/src/business/crm/index.ts +1 -0
- package/src/business/deals/api-schemas.ts +79 -0
- package/src/business/deals/index.ts +1 -0
- package/src/business/projects/types.ts +124 -88
- package/src/execution/core/runner-types.ts +61 -80
- package/src/execution/engine/index.ts +4 -3
- package/src/execution/engine/tools/integration/server/adapters/gmail/gmail-tools.ts +105 -104
- package/src/execution/engine/tools/integration/server/adapters/instantly/instantly-tools.ts +1474 -1473
- package/src/execution/engine/tools/integration/server/adapters/millionverifier/millionverifier-tools.ts +103 -102
- package/src/execution/engine/tools/integration/server/adapters/signature-api/signature-api-tools.ts +182 -179
- package/src/execution/engine/tools/integration/server/adapters/stripe/stripe-tools.ts +310 -309
- package/src/execution/engine/tools/integration/tool.ts +255 -253
- package/src/execution/engine/tools/lead-service-types.ts +939 -924
- package/src/execution/engine/tools/messages.ts +43 -0
- package/src/execution/engine/tools/platform/acquisition/list-tools.ts +6 -5
- package/src/execution/engine/tools/platform/acquisition/types.ts +5 -2
- package/src/execution/engine/tools/platform/email/types.ts +97 -96
- package/src/execution/engine/tools/registry.ts +4 -3
- package/src/execution/engine/tools/tool-maps.ts +3 -1
- package/src/execution/engine/tools/types.ts +234 -233
- package/src/execution/engine/workflow/types.ts +195 -193
- package/src/execution/external/api-schemas.ts +40 -0
- package/src/execution/external/index.ts +1 -0
- package/src/knowledge/README.md +32 -0
- package/src/knowledge/__tests__/queries.test.ts +504 -0
- package/src/knowledge/format.ts +99 -0
- package/src/knowledge/index.ts +5 -0
- package/src/knowledge/queries.ts +256 -0
- package/src/organization-model/__tests__/defaults.test.ts +172 -172
- package/src/organization-model/__tests__/foundation.test.ts +7 -7
- package/src/organization-model/__tests__/icons.test.ts +27 -0
- package/src/organization-model/__tests__/knowledge.test.ts +214 -0
- package/src/organization-model/contracts.ts +17 -15
- package/src/organization-model/defaults.ts +74 -19
- package/src/organization-model/domains/knowledge.ts +53 -0
- package/src/organization-model/domains/navigation.ts +416 -399
- package/src/organization-model/domains/prospecting.ts +204 -1
- package/src/organization-model/domains/sales.test.ts +29 -0
- package/src/organization-model/domains/sales.ts +102 -0
- package/src/organization-model/domains/shared.ts +6 -5
- package/src/organization-model/foundation.ts +10 -6
- package/src/organization-model/graph/build.ts +209 -182
- package/src/organization-model/graph/schema.ts +37 -34
- package/src/organization-model/graph/types.ts +47 -31
- package/src/organization-model/icons.ts +81 -0
- package/src/organization-model/index.ts +8 -3
- package/src/organization-model/organization-model.mdx +1 -1
- package/src/organization-model/published.ts +103 -86
- package/src/organization-model/schema.ts +90 -85
- package/src/organization-model/types.ts +42 -35
- package/src/platform/constants/versions.ts +1 -1
- package/src/platform/index.ts +23 -27
- package/src/platform/registry/index.ts +0 -4
- package/src/platform/registry/resource-registry.ts +0 -77
- package/src/platform/registry/serialized-types.ts +148 -219
- package/src/platform/registry/stats-types.ts +60 -60
- package/src/reference/_generated/contracts.md +829 -595
- package/src/supabase/database.types.ts +2978 -2958
|
@@ -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'
|