@elevasis/core 0.44.0 → 0.46.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/knowledge/index.d.ts +14 -10
- package/dist/knowledge/index.js +23 -1
- package/package.json +3 -3
- package/src/knowledge/__tests__/queries.test.ts +174 -0
- package/src/knowledge/format.ts +39 -0
- package/src/knowledge/index.ts +7 -2
- package/src/knowledge/queries.ts +269 -13
|
@@ -1597,22 +1597,24 @@ declare function listAllResources(model: OrganizationModel): NonNullable<Organiz
|
|
|
1597
1597
|
*/
|
|
1598
1598
|
declare function listAllRoles(model: OrganizationModel): NonNullable<OrganizationModel['roles']>[string][];
|
|
1599
1599
|
/** The recognized mount axes for Knowledge Map paths. */
|
|
1600
|
-
type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node' | 'all-systems' | 'all-resources' | 'all-roles';
|
|
1600
|
+
type KnowledgeMount = 'by-system' | 'by-ontology' | 'by-kind' | 'by-owner' | 'graph' | 'node' | 'all-systems' | 'all-resources' | 'all-roles' | 'by-domain' | 'by-item';
|
|
1601
1601
|
/**
|
|
1602
1602
|
* The result of parsing a Knowledge Map path string.
|
|
1603
1603
|
*
|
|
1604
1604
|
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
1605
1605
|
*
|
|
1606
1606
|
* Per-mount arg arrays:
|
|
1607
|
-
* - `by-system`: `[systemId]`
|
|
1608
|
-
* - `by-ontology`: `[ontologyId]`
|
|
1609
|
-
* - `by-kind`: `[kind]`
|
|
1610
|
-
* - `by-owner`: `[ownerId]`
|
|
1611
|
-
* - `graph`: `[nodeId, verb]`
|
|
1612
|
-
* - `node`: `[nodeId]`
|
|
1613
|
-
* - `all-systems`: `[]`
|
|
1614
|
-
* - `all-resources`:`[]`
|
|
1615
|
-
* - `all-roles`: `[]`
|
|
1607
|
+
* - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
|
|
1608
|
+
* - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
|
|
1609
|
+
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
1610
|
+
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
1611
|
+
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
1612
|
+
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
1613
|
+
* - `all-systems`: `[]` (no args — returns all systems flattened)
|
|
1614
|
+
* - `all-resources`:`[]` (no args — returns all resources)
|
|
1615
|
+
* - `all-roles`: `[]` (no args — returns all roles)
|
|
1616
|
+
* - `by-domain`: `[domain]` (e.g. `['clients']`) — enumerate all items in domain
|
|
1617
|
+
* - `by-item`: `[domain, itemId]` (e.g. `['clients', '<uuid>']`) — single domain-item
|
|
1616
1618
|
*/
|
|
1617
1619
|
interface ParsedKnowledgePath {
|
|
1618
1620
|
mount: KnowledgeMount;
|
|
@@ -1632,6 +1634,8 @@ interface ParsedKnowledgePath {
|
|
|
1632
1634
|
* `/all-systems` -> `{ mount: ‘all-systems’, args: [] }`
|
|
1633
1635
|
* `/all-resources` -> `{ mount: ‘all-resources’,args: [] }`
|
|
1634
1636
|
* `/all-roles` -> `{ mount: ‘all-roles’, args: [] }`
|
|
1637
|
+
* `/by-domain/<domain>` -> `{ mount: ‘by-domain’, args: [‘<domain>’] }`
|
|
1638
|
+
* `/by-item/<domain>/<itemId>` -> `{ mount: ‘by-item’, args: [‘<domain>’, ‘<itemId>’] }`
|
|
1635
1639
|
*
|
|
1636
1640
|
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
1637
1641
|
*
|
package/dist/knowledge/index.js
CHANGED
|
@@ -101,6 +101,7 @@ function getSystem(model, path) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// src/knowledge/queries.ts
|
|
104
|
+
var DOMAIN_ITEM_KINDS = ["clients", "roles", "policies", "customers", "offerings", "goals"];
|
|
104
105
|
function toGraphNodeId(omNodeId) {
|
|
105
106
|
return `knowledge:${omNodeId}`;
|
|
106
107
|
}
|
|
@@ -224,11 +225,32 @@ function parsePath(pathString) {
|
|
|
224
225
|
if (first === "all-roles" && rest.length === 0) {
|
|
225
226
|
return { mount: "all-roles", args: [] };
|
|
226
227
|
}
|
|
228
|
+
if (first === "by-domain") {
|
|
229
|
+
if (rest.length === 0) {
|
|
230
|
+
throw new Error(`parsePath: /by-domain requires a domain argument, got: "${pathString}"`);
|
|
231
|
+
}
|
|
232
|
+
const domain = rest[0];
|
|
233
|
+
if (!DOMAIN_ITEM_KINDS.includes(domain)) {
|
|
234
|
+
throw new Error(`parsePath: /by-domain unknown domain "${domain}". Supported: ${DOMAIN_ITEM_KINDS.join(", ")}`);
|
|
235
|
+
}
|
|
236
|
+
return { mount: "by-domain", args: [domain] };
|
|
237
|
+
}
|
|
238
|
+
if (first === "by-item") {
|
|
239
|
+
if (rest.length < 2) {
|
|
240
|
+
throw new Error(`parsePath: /by-item requires <domain>/<itemId>, got: "${pathString}"`);
|
|
241
|
+
}
|
|
242
|
+
const domain = rest[0];
|
|
243
|
+
if (!DOMAIN_ITEM_KINDS.includes(domain)) {
|
|
244
|
+
throw new Error(`parsePath: /by-item unknown domain "${domain}". Supported: ${DOMAIN_ITEM_KINDS.join(", ")}`);
|
|
245
|
+
}
|
|
246
|
+
const itemId = rest.slice(1).join("/");
|
|
247
|
+
return { mount: "by-item", args: [domain, itemId] };
|
|
248
|
+
}
|
|
227
249
|
if (segments.length === 1) {
|
|
228
250
|
return { mount: "node", args: [first] };
|
|
229
251
|
}
|
|
230
252
|
throw new Error(
|
|
231
|
-
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
|
|
253
|
+
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /by-domain/<domain>, /by-item/<domain>/<itemId>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
|
|
232
254
|
);
|
|
233
255
|
}
|
|
234
256
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elevasis/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Minimal shared constants across Elevasis monorepo",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"rollup-plugin-dts": "^6.3.0",
|
|
46
46
|
"tsup": "^8.0.0",
|
|
47
47
|
"typescript": "5.9.2",
|
|
48
|
-
"@repo/
|
|
49
|
-
"@repo/
|
|
48
|
+
"@repo/eslint-config": "0.0.0",
|
|
49
|
+
"@repo/typescript-config": "0.0.0"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
52
|
"@anthropic-ai/sdk": "^0.62.0",
|
|
@@ -1097,3 +1097,177 @@ describe('formatOmDescribe', () => {
|
|
|
1097
1097
|
expect(output).toContain('System: sales.lead-gen')
|
|
1098
1098
|
})
|
|
1099
1099
|
})
|
|
1100
|
+
|
|
1101
|
+
// ---------------------------------------------------------------------------
|
|
1102
|
+
// Domain-item resolution (item:<domain>:<id> + bare-id back-compat)
|
|
1103
|
+
// ---------------------------------------------------------------------------
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* Fixture exercising all six graph-projected domain-item kinds.
|
|
1107
|
+
* `knowledge.brand-guide` governs the client via a `client:client-1` link.
|
|
1108
|
+
*/
|
|
1109
|
+
const DOMAIN_ITEM_MODEL = {
|
|
1110
|
+
knowledge: {
|
|
1111
|
+
'knowledge.brand-guide': {
|
|
1112
|
+
id: 'knowledge.brand-guide',
|
|
1113
|
+
kind: 'reference',
|
|
1114
|
+
title: 'Brand Guide',
|
|
1115
|
+
summary: 'Branding rules for the client.',
|
|
1116
|
+
body: '## Brand Guide\n\nVoice and palette.',
|
|
1117
|
+
links: [{ nodeId: 'client:client-1' }],
|
|
1118
|
+
ownerIds: [],
|
|
1119
|
+
updatedAt: '2026-01-01'
|
|
1120
|
+
}
|
|
1121
|
+
},
|
|
1122
|
+
clients: {
|
|
1123
|
+
'client-1': {
|
|
1124
|
+
id: 'client-1',
|
|
1125
|
+
name: 'Byron for Irvine',
|
|
1126
|
+
organizationName: 'Byron for Irvine Campaign',
|
|
1127
|
+
shortName: 'Byron',
|
|
1128
|
+
clientBrief: 'Local city-council campaign.',
|
|
1129
|
+
candidateName: 'Byron'
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
roles: {
|
|
1133
|
+
'role.ops-lead': {
|
|
1134
|
+
id: 'role.ops-lead',
|
|
1135
|
+
order: 10,
|
|
1136
|
+
title: 'Ops Lead',
|
|
1137
|
+
responsibilities: ['Run operations.'],
|
|
1138
|
+
responsibleFor: []
|
|
1139
|
+
}
|
|
1140
|
+
},
|
|
1141
|
+
policies: {
|
|
1142
|
+
'policy.review-spend': {
|
|
1143
|
+
id: 'policy.review-spend',
|
|
1144
|
+
order: 10,
|
|
1145
|
+
label: 'Review Spend',
|
|
1146
|
+
description: 'Review spend over threshold.'
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
customers: {
|
|
1150
|
+
'cust-1': {
|
|
1151
|
+
id: 'cust-1',
|
|
1152
|
+
name: 'SMB Automation Segment',
|
|
1153
|
+
description: 'Small businesses adopting automation.'
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1156
|
+
offerings: {
|
|
1157
|
+
'off-1': {
|
|
1158
|
+
id: 'off-1',
|
|
1159
|
+
name: 'Automation Starter Package',
|
|
1160
|
+
description: 'Entry automation engagement.'
|
|
1161
|
+
}
|
|
1162
|
+
},
|
|
1163
|
+
goals: {
|
|
1164
|
+
'goal-1': {
|
|
1165
|
+
id: 'goal-1',
|
|
1166
|
+
description: 'Double recurring revenue this year.'
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
} as unknown as OrganizationModel
|
|
1170
|
+
|
|
1171
|
+
describe('omDescribe — domain items', () => {
|
|
1172
|
+
it('resolves item:clients:<id> to the full client profile + location + governing knowledge', () => {
|
|
1173
|
+
const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
|
|
1174
|
+
expect(result).toBeDefined()
|
|
1175
|
+
if (!result || result.kind !== 'domain-item') throw new Error('expected domain-item result')
|
|
1176
|
+
expect(result.domain).toBe('clients')
|
|
1177
|
+
expect(result.id).toBe('item:clients:client-1')
|
|
1178
|
+
expect(result.name).toBe('Byron for Irvine')
|
|
1179
|
+
// Full ClientProfile payload is carried in `record`.
|
|
1180
|
+
expect(result.record).toMatchObject({
|
|
1181
|
+
organizationName: 'Byron for Irvine Campaign',
|
|
1182
|
+
candidateName: 'Byron'
|
|
1183
|
+
})
|
|
1184
|
+
// Location.
|
|
1185
|
+
expect(result.location.domainLabel).toBe('Clients')
|
|
1186
|
+
expect(result.location.graphNodeId).toBe('client:client-1')
|
|
1187
|
+
expect(result.location.containsParentIds).toContain('organization-model')
|
|
1188
|
+
// Governing knowledge.
|
|
1189
|
+
expect(result.governingKnowledge.map((g) => g.id)).toContain('knowledge.brand-guide')
|
|
1190
|
+
})
|
|
1191
|
+
|
|
1192
|
+
it('resolves bare client:<id> identically to the item: form (back-compat)', () => {
|
|
1193
|
+
const bare = omDescribe(DOMAIN_ITEM_MODEL, 'client:client-1')
|
|
1194
|
+
const canonical = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
|
|
1195
|
+
expect(bare).toEqual(canonical)
|
|
1196
|
+
})
|
|
1197
|
+
|
|
1198
|
+
it('resolves the other five domain kinds via item: and bare ids', () => {
|
|
1199
|
+
const cases: Array<{ item: string; bare: string; domain: string; name: string }> = [
|
|
1200
|
+
{ item: 'item:roles:role.ops-lead', bare: 'role:role.ops-lead', domain: 'roles', name: 'Ops Lead' },
|
|
1201
|
+
{
|
|
1202
|
+
item: 'item:policies:policy.review-spend',
|
|
1203
|
+
bare: 'policy:policy.review-spend',
|
|
1204
|
+
domain: 'policies',
|
|
1205
|
+
name: 'Review Spend'
|
|
1206
|
+
},
|
|
1207
|
+
{ item: 'item:customers:cust-1', bare: 'customer:cust-1', domain: 'customers', name: 'SMB Automation Segment' },
|
|
1208
|
+
{ item: 'item:offerings:off-1', bare: 'offering:off-1', domain: 'offerings', name: 'Automation Starter Package' },
|
|
1209
|
+
{ item: 'item:goals:goal-1', bare: 'goal:goal-1', domain: 'goals', name: 'Double recurring revenue this year.' }
|
|
1210
|
+
]
|
|
1211
|
+
for (const c of cases) {
|
|
1212
|
+
const viaItem = omDescribe(DOMAIN_ITEM_MODEL, c.item)
|
|
1213
|
+
expect(viaItem, `item form ${c.item}`).toBeDefined()
|
|
1214
|
+
if (!viaItem || viaItem.kind !== 'domain-item') throw new Error(`expected domain-item for ${c.item}`)
|
|
1215
|
+
expect(viaItem.domain).toBe(c.domain)
|
|
1216
|
+
expect(viaItem.name).toBe(c.name)
|
|
1217
|
+
// Bare form resolves to the same result.
|
|
1218
|
+
expect(omDescribe(DOMAIN_ITEM_MODEL, c.bare)).toEqual(viaItem)
|
|
1219
|
+
}
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
it('returns undefined for an unknown item id', () => {
|
|
1223
|
+
expect(omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:does-not-exist')).toBeUndefined()
|
|
1224
|
+
expect(omDescribe(DOMAIN_ITEM_MODEL, 'client:does-not-exist')).toBeUndefined()
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it('returns undefined for an item ref with an unknown domain', () => {
|
|
1228
|
+
expect(omDescribe(DOMAIN_ITEM_MODEL, 'item:widgets:thing-1')).toBeUndefined()
|
|
1229
|
+
})
|
|
1230
|
+
})
|
|
1231
|
+
|
|
1232
|
+
describe('parsePath — domain-item mounts', () => {
|
|
1233
|
+
it('parses /by-domain/<domain> into a by-domain mount', () => {
|
|
1234
|
+
expect(parsePath('/by-domain/clients')).toEqual({ mount: 'by-domain', args: ['clients'] })
|
|
1235
|
+
})
|
|
1236
|
+
|
|
1237
|
+
it('parses /by-item/<domain>/<itemId> into a by-item mount', () => {
|
|
1238
|
+
expect(parsePath('/by-item/clients/client-1')).toEqual({ mount: 'by-item', args: ['clients', 'client-1'] })
|
|
1239
|
+
})
|
|
1240
|
+
|
|
1241
|
+
it('throws on an unknown domain for /by-domain', () => {
|
|
1242
|
+
expect(() => parsePath('/by-domain/widgets')).toThrow(/unknown domain/i)
|
|
1243
|
+
})
|
|
1244
|
+
|
|
1245
|
+
it('throws on an unknown domain for /by-item', () => {
|
|
1246
|
+
expect(() => parsePath('/by-item/widgets/thing-1')).toThrow(/unknown domain/i)
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
it('throws when /by-item is missing the itemId', () => {
|
|
1250
|
+
expect(() => parsePath('/by-item/clients')).toThrow(/by-item/i)
|
|
1251
|
+
})
|
|
1252
|
+
})
|
|
1253
|
+
|
|
1254
|
+
describe('formatOmDescribe — domain items', () => {
|
|
1255
|
+
it('renders a domain-item header, location, record, and governing knowledge', () => {
|
|
1256
|
+
const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
|
|
1257
|
+
const output = formatOmDescribe(result)
|
|
1258
|
+
expect(output).toContain('Domain Item: item:clients:client-1')
|
|
1259
|
+
expect(output).toContain('Domain: Clients')
|
|
1260
|
+
expect(output).toContain('Name: Byron for Irvine')
|
|
1261
|
+
expect(output).toContain('Graph node: client:client-1')
|
|
1262
|
+
expect(output).toContain('Governing knowledge:')
|
|
1263
|
+
expect(output).toContain('knowledge.brand-guide')
|
|
1264
|
+
})
|
|
1265
|
+
|
|
1266
|
+
it('serializes the domain-item result directly via JSON.stringify', () => {
|
|
1267
|
+
const result = omDescribe(DOMAIN_ITEM_MODEL, 'item:clients:client-1')
|
|
1268
|
+
const json = JSON.parse(JSON.stringify(result))
|
|
1269
|
+
expect(json.kind).toBe('domain-item')
|
|
1270
|
+
expect(json.domain).toBe('clients')
|
|
1271
|
+
expect(json.id).toBe('item:clients:client-1')
|
|
1272
|
+
})
|
|
1273
|
+
})
|
package/src/knowledge/format.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OrgKnowledgeNode } from '../organization-model/domains/knowledge'
|
|
2
2
|
import type {
|
|
3
3
|
KnowledgeMount,
|
|
4
|
+
OmDescribeDomainItem,
|
|
4
5
|
OmDescribeKnowledge,
|
|
5
6
|
OmDescribeOntology,
|
|
6
7
|
OmDescribePolicy,
|
|
@@ -170,6 +171,8 @@ export function formatOmDescribe(result: OmDescribeResult | undefined): string {
|
|
|
170
171
|
return formatRoleDescribe(result)
|
|
171
172
|
case 'policy':
|
|
172
173
|
return formatPolicyDescribe(result)
|
|
174
|
+
case 'domain-item':
|
|
175
|
+
return formatDomainItemDescribe(result)
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
@@ -320,3 +323,39 @@ function formatPolicyDescribe(r: OmDescribePolicy): string {
|
|
|
320
323
|
|
|
321
324
|
return join([header, effectSection, appliesSection])
|
|
322
325
|
}
|
|
326
|
+
|
|
327
|
+
function formatDomainItemDescribe(r: OmDescribeDomainItem): string {
|
|
328
|
+
const header = join([
|
|
329
|
+
`Domain Item: ${r.id}`,
|
|
330
|
+
[
|
|
331
|
+
field('Domain', r.location.domainLabel),
|
|
332
|
+
field('Name', r.name),
|
|
333
|
+
field('Graph node', r.location.graphNodeId),
|
|
334
|
+
field('Parent nodes', r.location.containsParentIds.join(', '))
|
|
335
|
+
]
|
|
336
|
+
.filter((s) => s.length > 0)
|
|
337
|
+
.join('\n')
|
|
338
|
+
])
|
|
339
|
+
|
|
340
|
+
// Key record fields — emit a compact subset of the record payload.
|
|
341
|
+
const recordLines: string[] = []
|
|
342
|
+
for (const [k, v] of Object.entries(r.record)) {
|
|
343
|
+
if (v === undefined || v === null) continue
|
|
344
|
+
if (typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length === 0) continue
|
|
345
|
+
const display = Array.isArray(v)
|
|
346
|
+
? v.length === 0
|
|
347
|
+
? null
|
|
348
|
+
: `[${(v as unknown[]).slice(0, 5).join(', ')}${v.length > 5 ? `, ...+${v.length - 5}` : ''}]`
|
|
349
|
+
: typeof v === 'object'
|
|
350
|
+
? JSON.stringify(v).slice(0, 120)
|
|
351
|
+
: String(v)
|
|
352
|
+
if (display === null) continue
|
|
353
|
+
recordLines.push(` ${k}: ${display}`)
|
|
354
|
+
}
|
|
355
|
+
const recordSection = recordLines.length ? `Record:\n${recordLines.join('\n')}` : ''
|
|
356
|
+
|
|
357
|
+
const govLines = r.governingKnowledge.map((g) => ` - ${g.id} [${g.knowledgeKind}] ${g.title}`)
|
|
358
|
+
const govSection = govLines.length ? `Governing knowledge:\n${govLines.join('\n')}` : 'Governing knowledge:\n (none)'
|
|
359
|
+
|
|
360
|
+
return join([header, recordSection, govSection])
|
|
361
|
+
}
|
package/src/knowledge/index.ts
CHANGED
|
@@ -10,7 +10,8 @@ export {
|
|
|
10
10
|
omDescribe,
|
|
11
11
|
listAllSystemsFlat,
|
|
12
12
|
listAllResources,
|
|
13
|
-
listAllRoles
|
|
13
|
+
listAllRoles,
|
|
14
|
+
DOMAIN_ITEM_KINDS
|
|
14
15
|
} from './queries'
|
|
15
16
|
export type {
|
|
16
17
|
KnowledgeMount,
|
|
@@ -24,7 +25,11 @@ export type {
|
|
|
24
25
|
OmDescribeKnowledge,
|
|
25
26
|
OmDescribeOntology,
|
|
26
27
|
OmDescribeRole,
|
|
27
|
-
OmDescribePolicy
|
|
28
|
+
OmDescribePolicy,
|
|
29
|
+
OmDescribeDomainItem,
|
|
30
|
+
OmDescribeDomainItemLocation,
|
|
31
|
+
OmDescribeDomainItemKnowledgeRef,
|
|
32
|
+
DomainItemKind
|
|
28
33
|
} from './queries'
|
|
29
34
|
|
|
30
35
|
export { formatText, formatJson, formatIdsOnly, formatOmSearchHits, formatOmDescribe } from './format'
|
package/src/knowledge/queries.ts
CHANGED
|
@@ -4,6 +4,30 @@ import { OntologyIdSchema, ontologyGraphNodeId } from '../organization-model/ont
|
|
|
4
4
|
import type { OrganizationModel } from '../organization-model/types'
|
|
5
5
|
import { listAllSystems } from '../organization-model/helpers'
|
|
6
6
|
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Domain-item constants
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The six graph-projected domain-item kinds supported by `omDescribe`.
|
|
13
|
+
* Each maps to a collection on `OrganizationModel` and a graph node kind.
|
|
14
|
+
*/
|
|
15
|
+
export const DOMAIN_ITEM_KINDS = ['clients', 'roles', 'policies', 'customers', 'offerings', 'goals'] as const
|
|
16
|
+
export type DomainItemKind = (typeof DOMAIN_ITEM_KINDS)[number]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Bare-id prefix to domain map used for back-compat resolution.
|
|
20
|
+
* e.g. `client:<uuid>` → `clients`
|
|
21
|
+
*/
|
|
22
|
+
const BARE_PREFIX_TO_DOMAIN: Record<string, DomainItemKind> = {
|
|
23
|
+
client: 'clients',
|
|
24
|
+
role: 'roles',
|
|
25
|
+
policy: 'policies',
|
|
26
|
+
customer: 'customers',
|
|
27
|
+
offering: 'offerings',
|
|
28
|
+
goal: 'goals'
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
// ---------------------------------------------------------------------------
|
|
8
32
|
// Internal helpers
|
|
9
33
|
// ---------------------------------------------------------------------------
|
|
@@ -265,6 +289,8 @@ export type KnowledgeMount =
|
|
|
265
289
|
| 'all-systems'
|
|
266
290
|
| 'all-resources'
|
|
267
291
|
| 'all-roles'
|
|
292
|
+
| 'by-domain'
|
|
293
|
+
| 'by-item'
|
|
268
294
|
|
|
269
295
|
/**
|
|
270
296
|
* The result of parsing a Knowledge Map path string.
|
|
@@ -272,15 +298,17 @@ export type KnowledgeMount =
|
|
|
272
298
|
* Shape: `{ mount: KnowledgeMount, args: string[] }`
|
|
273
299
|
*
|
|
274
300
|
* Per-mount arg arrays:
|
|
275
|
-
* - `by-system`: `[systemId]`
|
|
276
|
-
* - `by-ontology`: `[ontologyId]`
|
|
277
|
-
* - `by-kind`: `[kind]`
|
|
278
|
-
* - `by-owner`: `[ownerId]`
|
|
279
|
-
* - `graph`: `[nodeId, verb]`
|
|
280
|
-
* - `node`: `[nodeId]`
|
|
281
|
-
* - `all-systems`: `[]`
|
|
282
|
-
* - `all-resources`:`[]`
|
|
283
|
-
* - `all-roles`: `[]`
|
|
301
|
+
* - `by-system`: `[systemId]` (e.g. `['sales.crm']`)
|
|
302
|
+
* - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
|
|
303
|
+
* - `by-kind`: `[kind]` (e.g. `['playbook']`)
|
|
304
|
+
* - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
|
|
305
|
+
* - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
|
|
306
|
+
* - `node`: `[nodeId]` (single node lookup, no sub-path)
|
|
307
|
+
* - `all-systems`: `[]` (no args — returns all systems flattened)
|
|
308
|
+
* - `all-resources`:`[]` (no args — returns all resources)
|
|
309
|
+
* - `all-roles`: `[]` (no args — returns all roles)
|
|
310
|
+
* - `by-domain`: `[domain]` (e.g. `['clients']`) — enumerate all items in domain
|
|
311
|
+
* - `by-item`: `[domain, itemId]` (e.g. `['clients', '<uuid>']`) — single domain-item
|
|
284
312
|
*/
|
|
285
313
|
export interface ParsedKnowledgePath {
|
|
286
314
|
mount: KnowledgeMount
|
|
@@ -301,6 +329,8 @@ export interface ParsedKnowledgePath {
|
|
|
301
329
|
* `/all-systems` -> `{ mount: ‘all-systems’, args: [] }`
|
|
302
330
|
* `/all-resources` -> `{ mount: ‘all-resources’,args: [] }`
|
|
303
331
|
* `/all-roles` -> `{ mount: ‘all-roles’, args: [] }`
|
|
332
|
+
* `/by-domain/<domain>` -> `{ mount: ‘by-domain’, args: [‘<domain>’] }`
|
|
333
|
+
* `/by-item/<domain>/<itemId>` -> `{ mount: ‘by-item’, args: [‘<domain>’, ‘<itemId>’] }`
|
|
304
334
|
*
|
|
305
335
|
* The path MUST start with `/`. Trailing slashes are stripped before parsing.
|
|
306
336
|
*
|
|
@@ -384,6 +414,31 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
|
384
414
|
return { mount: 'all-roles', args: [] }
|
|
385
415
|
}
|
|
386
416
|
|
|
417
|
+
// /by-domain/<domain> — enumerate all items in a domain
|
|
418
|
+
if (first === 'by-domain') {
|
|
419
|
+
if (rest.length === 0) {
|
|
420
|
+
throw new Error(`parsePath: /by-domain requires a domain argument, got: "${pathString}"`)
|
|
421
|
+
}
|
|
422
|
+
const domain = rest[0]
|
|
423
|
+
if (!(DOMAIN_ITEM_KINDS as readonly string[]).includes(domain)) {
|
|
424
|
+
throw new Error(`parsePath: /by-domain unknown domain "${domain}". Supported: ${DOMAIN_ITEM_KINDS.join(', ')}`)
|
|
425
|
+
}
|
|
426
|
+
return { mount: 'by-domain', args: [domain] }
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// /by-item/<domain>/<itemId> — single domain-item resolution
|
|
430
|
+
if (first === 'by-item') {
|
|
431
|
+
if (rest.length < 2) {
|
|
432
|
+
throw new Error(`parsePath: /by-item requires <domain>/<itemId>, got: "${pathString}"`)
|
|
433
|
+
}
|
|
434
|
+
const domain = rest[0]
|
|
435
|
+
if (!(DOMAIN_ITEM_KINDS as readonly string[]).includes(domain)) {
|
|
436
|
+
throw new Error(`parsePath: /by-item unknown domain "${domain}". Supported: ${DOMAIN_ITEM_KINDS.join(', ')}`)
|
|
437
|
+
}
|
|
438
|
+
const itemId = rest.slice(1).join('/')
|
|
439
|
+
return { mount: 'by-item', args: [domain, itemId] }
|
|
440
|
+
}
|
|
441
|
+
|
|
387
442
|
// /<nodeId> (single node)
|
|
388
443
|
// first must not be a recognized mount prefix
|
|
389
444
|
if (segments.length === 1) {
|
|
@@ -391,7 +446,7 @@ export function parsePath(pathString: string): ParsedKnowledgePath {
|
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
throw new Error(
|
|
394
|
-
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
|
|
449
|
+
`parsePath: unrecognized path pattern "${pathString}". Supported: /by-system/<id>, /by-kind/<kind>, /by-owner/<id>, /by-domain/<domain>, /by-item/<domain>/<itemId>, /graph/<nodeId>/governs, /graph/<nodeId>/governed-by, /<nodeId>, /all-systems, /all-resources, /all-roles`
|
|
395
450
|
)
|
|
396
451
|
}
|
|
397
452
|
|
|
@@ -763,6 +818,58 @@ function collectOntologyEntries(scope: {
|
|
|
763
818
|
// omDescribe — neighborhood view for any OM node ID
|
|
764
819
|
// ---------------------------------------------------------------------------
|
|
765
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Location context for a domain-item node in the organization graph.
|
|
823
|
+
*/
|
|
824
|
+
export interface OmDescribeDomainItemLocation {
|
|
825
|
+
/** Human-readable label for the domain (e.g. "Clients"). */
|
|
826
|
+
domainLabel: string
|
|
827
|
+
/** Graph node id for this item (e.g. `client:<uuid>`). */
|
|
828
|
+
graphNodeId: string
|
|
829
|
+
/** Parent graph node ids reachable via `contains` edges (e.g. `['organization-model']`). */
|
|
830
|
+
containsParentIds: string[]
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* A single governing knowledge node reference returned by domain-item describe.
|
|
835
|
+
*/
|
|
836
|
+
export interface OmDescribeDomainItemKnowledgeRef {
|
|
837
|
+
id: string
|
|
838
|
+
title: string
|
|
839
|
+
knowledgeKind: string
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Structured neighborhood view of a domain-item node.
|
|
844
|
+
*
|
|
845
|
+
* Returned by `omDescribe` for `item:<domain>:<id>` and bare-id forms
|
|
846
|
+
* (e.g. `client:<uuid>`). Carries the full domain record payload, graph
|
|
847
|
+
* location, and any governing knowledge nodes.
|
|
848
|
+
*/
|
|
849
|
+
export interface OmDescribeDomainItem {
|
|
850
|
+
kind: 'domain-item'
|
|
851
|
+
/** The `item:<domain>:<id>` canonical reference string. */
|
|
852
|
+
id: string
|
|
853
|
+
/** The domain collection this item belongs to (e.g. `'clients'`). */
|
|
854
|
+
domain: DomainItemKind
|
|
855
|
+
/** Human-readable name or label for this item. */
|
|
856
|
+
name: string
|
|
857
|
+
/** Graph node location: graph id, domain label, and parentage. */
|
|
858
|
+
location: OmDescribeDomainItemLocation
|
|
859
|
+
/**
|
|
860
|
+
* Full domain record payload. Shape varies by domain:
|
|
861
|
+
* - `clients`: full `ClientProfile` (identity, branding, links, prompts, config, etc.)
|
|
862
|
+
* - `roles`: id, title, responsibilities, responsibleFor, reportsToId
|
|
863
|
+
* - `policies`: id, label, description, trigger, appliesTo, actions
|
|
864
|
+
* - `customers`: id, name, description, jobsToBeDone, pains, gains, firmographics
|
|
865
|
+
* - `offerings`: id, name, description, pricingModel, price, targetSegmentIds
|
|
866
|
+
* - `goals`: id, description, periodStart, periodEnd, keyResults
|
|
867
|
+
*/
|
|
868
|
+
record: Record<string, unknown>
|
|
869
|
+
/** Knowledge nodes that govern this domain item via `governs` edges. */
|
|
870
|
+
governingKnowledge: OmDescribeDomainItemKnowledgeRef[]
|
|
871
|
+
}
|
|
872
|
+
|
|
766
873
|
/**
|
|
767
874
|
* Structured neighborhood view of a single OM node.
|
|
768
875
|
*
|
|
@@ -778,6 +885,7 @@ export type OmDescribeResult =
|
|
|
778
885
|
| OmDescribeOntology
|
|
779
886
|
| OmDescribeRole
|
|
780
887
|
| OmDescribePolicy
|
|
888
|
+
| OmDescribeDomainItem
|
|
781
889
|
|
|
782
890
|
export interface OmDescribeSystem {
|
|
783
891
|
kind: 'system'
|
|
@@ -868,18 +976,40 @@ export interface OmDescribePolicy {
|
|
|
868
976
|
* Detection rules (highest priority first):
|
|
869
977
|
* - Contains `:<ontology-kind>/<local-id>` → ontology
|
|
870
978
|
* - Starts with `knowledge.` → knowledge
|
|
871
|
-
* - Starts with `role.` → role
|
|
872
|
-
* - Starts with `policy.` → policy
|
|
979
|
+
* - Starts with `role.` (dotted) → role (OM role, NOT domain-item bare form)
|
|
980
|
+
* - Starts with `policy.` (dotted) → policy (OM policy, NOT domain-item bare form)
|
|
981
|
+
* - Starts with `item:<domain>:` → domain-item (canonical form)
|
|
982
|
+
* - Starts with a bare domain prefix followed by `:` → domain-item (back-compat)
|
|
873
983
|
* - Resolves via `getSystem()` → system
|
|
874
984
|
* - Matches a key in `model.resources` → resource
|
|
875
985
|
*/
|
|
876
|
-
function detectKind(model: OrganizationModel, id: string): OmSearchHitKind | undefined {
|
|
986
|
+
function detectKind(model: OrganizationModel, id: string): OmSearchHitKind | 'domain-item' | undefined {
|
|
877
987
|
if (/:(object|action|event|catalog|link|interface|value-type|property|group|endpoint)\//.test(id)) {
|
|
878
988
|
return 'ontology'
|
|
879
989
|
}
|
|
880
990
|
if (id.startsWith('knowledge.')) return 'knowledge'
|
|
991
|
+
// Dotted role/policy forms (e.g. role.ops-lead, policy.approve-large-deals) — OM search kinds.
|
|
992
|
+
// These must be checked BEFORE the bare colon forms so 'role.' does not collide with 'role:'.
|
|
881
993
|
if (id.startsWith('role.')) return 'role'
|
|
882
994
|
if (id.startsWith('policy.')) return 'policy'
|
|
995
|
+
// Canonical domain-item form: item:<domain>:<id>
|
|
996
|
+
if (id.startsWith('item:')) {
|
|
997
|
+
const afterItem = id.slice('item:'.length)
|
|
998
|
+
const colonIdx = afterItem.indexOf(':')
|
|
999
|
+
if (colonIdx > 0) {
|
|
1000
|
+
const domain = afterItem.slice(0, colonIdx)
|
|
1001
|
+
if ((DOMAIN_ITEM_KINDS as readonly string[]).includes(domain)) return 'domain-item'
|
|
1002
|
+
}
|
|
1003
|
+
return undefined
|
|
1004
|
+
}
|
|
1005
|
+
// Bare domain-item prefix forms: client:<id>, role:<id>, policy:<id>,
|
|
1006
|
+
// customer:<id>, offering:<id>, goal:<id>.
|
|
1007
|
+
// These use a colon separator (NOT a dot), so 'role:' is distinct from 'role.'.
|
|
1008
|
+
const firstColon = id.indexOf(':')
|
|
1009
|
+
if (firstColon > 0) {
|
|
1010
|
+
const prefix = id.slice(0, firstColon)
|
|
1011
|
+
if (prefix in BARE_PREFIX_TO_DOMAIN) return 'domain-item'
|
|
1012
|
+
}
|
|
883
1013
|
if (model.systems && getSystemByPath(model.systems, id)) return 'system'
|
|
884
1014
|
if (model.resources?.[id]) return 'resource'
|
|
885
1015
|
return undefined
|
|
@@ -929,6 +1059,8 @@ export function omDescribe(model: OrganizationModel, id: string): OmDescribeResu
|
|
|
929
1059
|
return describeRole(model, id)
|
|
930
1060
|
case 'policy':
|
|
931
1061
|
return describePolicy(model, id)
|
|
1062
|
+
case 'domain-item':
|
|
1063
|
+
return describeDomainItem(model, id)
|
|
932
1064
|
}
|
|
933
1065
|
}
|
|
934
1066
|
|
|
@@ -1129,3 +1261,127 @@ function describePolicy(model: OrganizationModel, id: string): OmDescribePolicy
|
|
|
1129
1261
|
effectKinds: (policy.actions ?? []).map((a) => a.kind)
|
|
1130
1262
|
}
|
|
1131
1263
|
}
|
|
1264
|
+
|
|
1265
|
+
// ---------------------------------------------------------------------------
|
|
1266
|
+
// Domain-item describe helpers
|
|
1267
|
+
// ---------------------------------------------------------------------------
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Human-readable labels for each domain kind.
|
|
1271
|
+
*/
|
|
1272
|
+
const DOMAIN_LABEL: Record<DomainItemKind, string> = {
|
|
1273
|
+
clients: 'Clients',
|
|
1274
|
+
roles: 'Roles',
|
|
1275
|
+
policies: 'Policies',
|
|
1276
|
+
customers: 'Customers',
|
|
1277
|
+
offerings: 'Offerings',
|
|
1278
|
+
goals: 'Goals'
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Graph node id prefix for each domain kind. Mirrors the convention used by
|
|
1283
|
+
* `buildOrganizationGraph` in `organization-model/graph/build.ts`:
|
|
1284
|
+
* client:<id>, role:<id>, policy:<id>, customer-segment:<id>, offering:<id>, goal:<id>
|
|
1285
|
+
*/
|
|
1286
|
+
const DOMAIN_GRAPH_PREFIX: Record<DomainItemKind, string> = {
|
|
1287
|
+
clients: 'client',
|
|
1288
|
+
roles: 'role',
|
|
1289
|
+
policies: 'policy',
|
|
1290
|
+
customers: 'customer-segment',
|
|
1291
|
+
offerings: 'offering',
|
|
1292
|
+
goals: 'goal'
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Parse a domain-item ref (canonical `item:<domain>:<id>` or bare `<prefix>:<id>`)
|
|
1297
|
+
* into `{ domain, itemId }`. Returns `undefined` when the ref cannot be parsed.
|
|
1298
|
+
*/
|
|
1299
|
+
function parseDomainItemRef(id: string): { domain: DomainItemKind; itemId: string } | undefined {
|
|
1300
|
+
// Canonical: item:<domain>:<id>
|
|
1301
|
+
if (id.startsWith('item:')) {
|
|
1302
|
+
const afterItem = id.slice('item:'.length)
|
|
1303
|
+
const colonIdx = afterItem.indexOf(':')
|
|
1304
|
+
if (colonIdx <= 0) return undefined
|
|
1305
|
+
const domain = afterItem.slice(0, colonIdx) as DomainItemKind
|
|
1306
|
+
const itemId = afterItem.slice(colonIdx + 1)
|
|
1307
|
+
if (!(DOMAIN_ITEM_KINDS as readonly string[]).includes(domain) || !itemId) return undefined
|
|
1308
|
+
return { domain, itemId }
|
|
1309
|
+
}
|
|
1310
|
+
// Bare prefix: client:<id>, role:<id>, etc.
|
|
1311
|
+
const firstColon = id.indexOf(':')
|
|
1312
|
+
if (firstColon <= 0) return undefined
|
|
1313
|
+
const prefix = id.slice(0, firstColon)
|
|
1314
|
+
const domain = BARE_PREFIX_TO_DOMAIN[prefix]
|
|
1315
|
+
if (!domain) return undefined
|
|
1316
|
+
const itemId = id.slice(firstColon + 1)
|
|
1317
|
+
if (!itemId) return undefined
|
|
1318
|
+
return { domain, itemId }
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Build a structured neighborhood view of a domain-item node.
|
|
1323
|
+
*
|
|
1324
|
+
* Accepts canonical `item:<domain>:<id>` or bare back-compat forms
|
|
1325
|
+
* (`client:<uuid>`, `role:<id>`, etc.).
|
|
1326
|
+
*/
|
|
1327
|
+
function describeDomainItem(model: OrganizationModel, id: string): OmDescribeDomainItem | undefined {
|
|
1328
|
+
const parsed = parseDomainItemRef(id)
|
|
1329
|
+
if (!parsed) return undefined
|
|
1330
|
+
|
|
1331
|
+
const { domain, itemId } = parsed
|
|
1332
|
+
|
|
1333
|
+
// Look up the record in the appropriate domain collection.
|
|
1334
|
+
const collection = model[domain] as Record<string, Record<string, unknown>> | undefined
|
|
1335
|
+
const record = collection?.[itemId]
|
|
1336
|
+
if (!record) return undefined
|
|
1337
|
+
|
|
1338
|
+
// Derive the item's name. Each domain uses a different field for the display name.
|
|
1339
|
+
const name =
|
|
1340
|
+
domain === 'clients'
|
|
1341
|
+
? String((record['name'] as string | undefined) ?? itemId)
|
|
1342
|
+
: domain === 'roles'
|
|
1343
|
+
? String((record['title'] as string | undefined) ?? itemId)
|
|
1344
|
+
: domain === 'policies'
|
|
1345
|
+
? String((record['label'] as string | undefined) ?? itemId)
|
|
1346
|
+
: domain === 'goals'
|
|
1347
|
+
? String((record['description'] as string | undefined) ?? itemId)
|
|
1348
|
+
: String((record['name'] as string | undefined) ?? itemId)
|
|
1349
|
+
|
|
1350
|
+
// Graph node id follows the build.ts convention.
|
|
1351
|
+
const graphNodeId = `${DOMAIN_GRAPH_PREFIX[domain]}:${itemId}`
|
|
1352
|
+
|
|
1353
|
+
// Contains parentage — all domain items have a single `contains` edge from
|
|
1354
|
+
// `organization-model`. We derive this from the known convention without
|
|
1355
|
+
// building the full graph (avoids heavy dependency).
|
|
1356
|
+
const containsParentIds = ['organization-model']
|
|
1357
|
+
|
|
1358
|
+
// Governing knowledge: knowledge nodes whose links target this item's graph node.
|
|
1359
|
+
const governingKnowledge: OmDescribeDomainItemKnowledgeRef[] = []
|
|
1360
|
+
for (const node of Object.values(model.knowledge ?? {})) {
|
|
1361
|
+
const governs = (node.links ?? []).some((link) => link.nodeId === graphNodeId)
|
|
1362
|
+
if (governs) {
|
|
1363
|
+
governingKnowledge.push({
|
|
1364
|
+
id: node.id,
|
|
1365
|
+
title: node.title,
|
|
1366
|
+
knowledgeKind: node.kind
|
|
1367
|
+
})
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Canonical id is always the `item:<domain>:<id>` form.
|
|
1372
|
+
const canonicalId = `item:${domain}:${itemId}`
|
|
1373
|
+
|
|
1374
|
+
return {
|
|
1375
|
+
kind: 'domain-item',
|
|
1376
|
+
id: canonicalId,
|
|
1377
|
+
domain,
|
|
1378
|
+
name,
|
|
1379
|
+
location: {
|
|
1380
|
+
domainLabel: DOMAIN_LABEL[domain],
|
|
1381
|
+
graphNodeId,
|
|
1382
|
+
containsParentIds
|
|
1383
|
+
},
|
|
1384
|
+
record: record as Record<string, unknown>,
|
|
1385
|
+
governingKnowledge
|
|
1386
|
+
}
|
|
1387
|
+
}
|