@elevasis/core 0.45.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.
@@ -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]` (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)
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
  *
@@ -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.45.0",
3
+ "version": "0.46.0",
4
4
  "license": "MIT",
5
5
  "description": "Minimal shared constants across Elevasis monorepo",
6
6
  "sideEffects": false,
@@ -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
+ })
@@ -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
+ }
@@ -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'
@@ -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]` (e.g. `['sales.crm']`)
276
- * - `by-ontology`: `[ontologyId]` (e.g. `['sales.crm:object/deal']`)
277
- * - `by-kind`: `[kind]` (e.g. `['playbook']`)
278
- * - `by-owner`: `[ownerId]` (e.g. `['role.ops-lead']`)
279
- * - `graph`: `[nodeId, verb]` where verb is `'governs'` or `'governed-by'`
280
- * - `node`: `[nodeId]` (single node lookup, no sub-path)
281
- * - `all-systems`: `[]` (no args — returns all systems flattened)
282
- * - `all-resources`:`[]` (no args — returns all resources)
283
- * - `all-roles`: `[]` (no args — returns 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
+ }