@elevasis/core 0.21.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/dist/index.d.ts +2518 -2169
  2. package/dist/index.js +2495 -1095
  3. package/dist/knowledge/index.d.ts +706 -1044
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2518 -2169
  6. package/dist/organization-model/index.js +2495 -1095
  7. package/dist/test-utils/index.d.ts +826 -1014
  8. package/dist/test-utils/index.js +1894 -1032
  9. package/package.json +3 -3
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +852 -397
  12. package/src/auth/multi-tenancy/permissions.ts +20 -8
  13. package/src/business/README.md +2 -2
  14. package/src/business/acquisition/api-schemas.test.ts +175 -2
  15. package/src/business/acquisition/api-schemas.ts +132 -16
  16. package/src/business/acquisition/build-templates.test.ts +4 -4
  17. package/src/business/acquisition/build-templates.ts +72 -30
  18. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  19. package/src/business/acquisition/index.ts +12 -0
  20. package/src/business/acquisition/types.ts +7 -3
  21. package/src/business/clients/api-schemas.test.ts +115 -0
  22. package/src/business/clients/api-schemas.ts +158 -0
  23. package/src/business/clients/index.ts +1 -0
  24. package/src/business/deals/api-schemas.ts +8 -0
  25. package/src/business/index.ts +5 -2
  26. package/src/business/projects/types.ts +19 -0
  27. package/src/execution/engine/__tests__/fixtures/test-agents.ts +10 -8
  28. package/src/execution/engine/agent/core/__tests__/agent.test.ts +16 -12
  29. package/src/execution/engine/agent/core/__tests__/error-passthrough.test.ts +4 -3
  30. package/src/execution/engine/agent/core/types.ts +25 -15
  31. package/src/execution/engine/agent/index.ts +6 -4
  32. package/src/execution/engine/agent/reasoning/__tests__/request-builder.test.ts +24 -18
  33. package/src/execution/engine/index.ts +3 -0
  34. package/src/execution/engine/workflow/types.ts +9 -2
  35. package/src/knowledge/README.md +8 -7
  36. package/src/knowledge/__tests__/queries.test.ts +74 -73
  37. package/src/knowledge/format.ts +10 -9
  38. package/src/knowledge/index.ts +1 -1
  39. package/src/knowledge/published.ts +1 -1
  40. package/src/knowledge/queries.ts +26 -25
  41. package/src/organization-model/README.md +73 -26
  42. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  43. package/src/organization-model/__tests__/defaults.test.ts +76 -96
  44. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  45. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  46. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  47. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  48. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  49. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  50. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  51. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  52. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  53. package/src/organization-model/__tests__/domains/resources.test.ts +310 -0
  54. package/src/organization-model/__tests__/domains/roles.test.ts +463 -347
  55. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  56. package/src/organization-model/__tests__/domains/systems.test.ts +209 -0
  57. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  58. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  59. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  60. package/src/organization-model/__tests__/graph.test.ts +899 -71
  61. package/src/organization-model/__tests__/knowledge.test.ts +209 -49
  62. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  63. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  64. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  65. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  66. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  67. package/src/organization-model/__tests__/schema.test.ts +291 -114
  68. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  69. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  70. package/src/organization-model/content-kinds/config.ts +36 -0
  71. package/src/organization-model/content-kinds/index.ts +74 -0
  72. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  73. package/src/organization-model/content-kinds/registry.ts +44 -0
  74. package/src/organization-model/content-kinds/status.ts +71 -0
  75. package/src/organization-model/content-kinds/template.ts +83 -0
  76. package/src/organization-model/content-kinds/types.ts +117 -0
  77. package/src/organization-model/contracts.ts +13 -3
  78. package/src/organization-model/defaults.ts +499 -86
  79. package/src/organization-model/domains/actions.ts +239 -0
  80. package/src/organization-model/domains/customers.ts +78 -75
  81. package/src/organization-model/domains/entities.ts +144 -0
  82. package/src/organization-model/domains/goals.ts +83 -80
  83. package/src/organization-model/domains/knowledge.ts +76 -17
  84. package/src/organization-model/domains/navigation.ts +107 -384
  85. package/src/organization-model/domains/offerings.ts +71 -66
  86. package/src/organization-model/domains/policies.ts +102 -0
  87. package/src/organization-model/domains/projects.ts +14 -48
  88. package/src/organization-model/domains/prospecting.ts +62 -181
  89. package/src/organization-model/domains/resources.ts +145 -0
  90. package/src/organization-model/domains/roles.ts +96 -55
  91. package/src/organization-model/domains/sales.ts +10 -219
  92. package/src/organization-model/domains/shared.ts +57 -57
  93. package/src/organization-model/domains/statuses.ts +339 -130
  94. package/src/organization-model/domains/systems.ts +203 -0
  95. package/src/organization-model/foundation.ts +54 -67
  96. package/src/organization-model/graph/build.ts +682 -54
  97. package/src/organization-model/graph/link.ts +1 -1
  98. package/src/organization-model/graph/schema.ts +24 -9
  99. package/src/organization-model/graph/types.ts +20 -7
  100. package/src/organization-model/helpers.ts +231 -26
  101. package/src/organization-model/icons.ts +1 -0
  102. package/src/organization-model/index.ts +118 -5
  103. package/src/organization-model/migration-helpers.ts +249 -0
  104. package/src/organization-model/organization-graph.mdx +16 -15
  105. package/src/organization-model/organization-model.mdx +111 -44
  106. package/src/organization-model/published.ts +172 -19
  107. package/src/organization-model/resolve.ts +117 -54
  108. package/src/organization-model/schema.ts +654 -112
  109. package/src/organization-model/surface-projection.ts +116 -122
  110. package/src/organization-model/types.ts +146 -20
  111. package/src/platform/api/types.ts +38 -35
  112. package/src/platform/constants/versions.ts +1 -1
  113. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  114. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  115. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  116. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  117. package/src/platform/registry/__tests__/resource-registry.test.ts +2053 -2005
  118. package/src/platform/registry/__tests__/validation.test.ts +1347 -1086
  119. package/src/platform/registry/index.ts +14 -0
  120. package/src/platform/registry/resource-registry.ts +52 -2
  121. package/src/platform/registry/serialization.ts +241 -202
  122. package/src/platform/registry/serialized-types.ts +1 -0
  123. package/src/platform/registry/types.ts +411 -361
  124. package/src/platform/registry/validation.ts +745 -513
  125. package/src/projects/api-schemas.ts +290 -267
  126. package/src/reference/_generated/contracts.md +853 -397
  127. package/src/reference/glossary.md +23 -18
  128. package/src/supabase/database.types.ts +181 -0
  129. package/src/test-utils/test-utils.test.ts +1 -6
  130. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  131. package/src/organization-model/domains/features.ts +0 -31
  132. package/src/organization-model/domains/operations.ts +0 -85
@@ -3,11 +3,14 @@ import {
3
3
  OrgKnowledgeNodeSchema,
4
4
  OrgKnowledgeKindSchema,
5
5
  KnowledgeDomainSchema,
6
- KnowledgeLinkSchema
6
+ KnowledgeLinkSchema,
7
+ KnowledgeTargetKindSchema,
8
+ KnowledgeTargetRefSchema
7
9
  } from '../domains/knowledge'
8
10
  import { OrganizationGraphEdgeKindSchema } from '../graph/schema'
9
11
  import { buildOrganizationGraph } from '../graph/build'
10
12
  import { DEFAULT_ORGANIZATION_MODEL, DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE } from '../defaults'
13
+ import { resolveOrganizationModel } from '../resolve'
11
14
  import type { OrganizationModel } from '../types'
12
15
 
13
16
  // ---------------------------------------------------------------------------
@@ -74,6 +77,15 @@ describe('OrgKnowledgeNodeSchema', () => {
74
77
  expect(node.ownerIds).toEqual([])
75
78
  })
76
79
 
80
+ it('accepts role ids as knowledge owners', () => {
81
+ const node = OrgKnowledgeNodeSchema.parse({
82
+ ...makeNode('strategy'),
83
+ ownerIds: ['role.ops-lead', 'role.ceo']
84
+ })
85
+
86
+ expect(node.ownerIds).toEqual(['role.ops-lead', 'role.ceo'])
87
+ })
88
+
77
89
  it('accepts an optional semantic icon token', () => {
78
90
  const node = OrgKnowledgeNodeSchema.parse({
79
91
  ...makeNode('playbook'),
@@ -91,6 +103,54 @@ describe('OrgKnowledgeNodeSchema', () => {
91
103
 
92
104
  expect(result.success).toBe(false)
93
105
  })
106
+
107
+ it('accepts an optional externalUrl', () => {
108
+ const node = OrgKnowledgeNodeSchema.parse({
109
+ ...makeNode('reference'),
110
+ externalUrl: 'https://docs.elevasis.com/technical/architecture'
111
+ })
112
+
113
+ expect(node.externalUrl).toBe('https://docs.elevasis.com/technical/architecture')
114
+ })
115
+
116
+ it('rejects an invalid externalUrl', () => {
117
+ const result = OrgKnowledgeNodeSchema.safeParse({
118
+ ...makeNode('reference'),
119
+ externalUrl: 'not-a-url'
120
+ })
121
+
122
+ expect(result.success).toBe(false)
123
+ })
124
+ })
125
+
126
+ describe('resolveOrganizationModel with knowledge owner roles', () => {
127
+ const node = {
128
+ id: 'knowledge.test-owner',
129
+ kind: 'playbook' as const,
130
+ title: 'Owned Knowledge',
131
+ summary: 'A knowledge node with role ownership.',
132
+ body: '## Body\n\nContent.',
133
+ ownerIds: ['role.ops-lead'],
134
+ updatedAt: '2026-05-08'
135
+ }
136
+
137
+ it('passes when ownerIds reference declared role ids', () => {
138
+ expect(() =>
139
+ resolveOrganizationModel({
140
+ roles: { 'role.ops-lead': { id: 'role.ops-lead', order: 10, title: 'Ops Lead' } },
141
+ // Phase 4: knowledge is a flat Record<id, OrgKnowledgeNode>
142
+ knowledge: { 'knowledge.test-owner': node }
143
+ })
144
+ ).not.toThrow()
145
+ })
146
+
147
+ it('throws when ownerIds reference unknown role ids', () => {
148
+ expect(() =>
149
+ resolveOrganizationModel({
150
+ knowledge: { 'knowledge.test-owner': node }
151
+ })
152
+ ).toThrow(/unknown owner role/)
153
+ })
94
154
  })
95
155
 
96
156
  // ---------------------------------------------------------------------------
@@ -121,60 +181,79 @@ describe('OrganizationGraphEdgeKindSchema', () => {
121
181
  })
122
182
 
123
183
  // ---------------------------------------------------------------------------
124
- // DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE — post Wave 2a: nodes is empty array
184
+ // DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE — Phase 4: flat Record<id, OrgKnowledgeNode> (D3).
185
+ // The wrapper shape { version, lastModified, nodes[] } was removed; versioning
186
+ // is in domainMetadata.knowledge. The default is an empty map {}.
125
187
  // ---------------------------------------------------------------------------
126
188
 
127
189
  describe('DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE', () => {
128
- it('has an empty nodes array after Wave 2a pilot literal removal', () => {
129
- expect(DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE.nodes).toEqual([])
190
+ it('is an empty object (flat map with no pre-seeded nodes)', () => {
191
+ expect(DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE).toEqual({})
130
192
  })
131
193
 
132
194
  it('passes KnowledgeDomainSchema.parse without throwing', () => {
133
195
  expect(() => KnowledgeDomainSchema.parse(DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE)).not.toThrow()
134
196
  })
197
+
198
+ it('does not have a .nodes array (wrapper shape was removed in Phase 4)', () => {
199
+ expect(DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE).not.toHaveProperty('nodes')
200
+ })
201
+
202
+ it('does not have a .version field (moved to domainMetadata.knowledge)', () => {
203
+ expect(DEFAULT_ORGANIZATION_MODEL_KNOWLEDGE).not.toHaveProperty('version')
204
+ })
135
205
  })
136
206
 
137
207
  // ---------------------------------------------------------------------------
138
208
  // buildOrganizationGraph — knowledge nodes + governs edges
139
- // Synthetic OM used so tests are independent of DEFAULT_ORGANIZATION_MODEL
140
- // knowledge nodes (empty after Wave 2a, Decision 2-B).
209
+ // Synthetic OM authored via flat Record<id, OrgKnowledgeNode> per Phase 4 schema.
141
210
  // ---------------------------------------------------------------------------
142
211
 
212
+ // Phase 4: knowledge is a flat Record<id, OrgKnowledgeNode> — no .nodes array wrapper.
143
213
  const SYNTHETIC_OM: OrganizationModel = {
144
214
  ...DEFAULT_ORGANIZATION_MODEL,
215
+ systems: Object.fromEntries(
216
+ Object.entries(DEFAULT_ORGANIZATION_MODEL.systems).map(([key, system]) => [
217
+ key,
218
+ key === 'sales.crm' || key === 'sales.lead-gen'
219
+ ? { ...system, governedByKnowledge: ['knowledge.test-playbook-1'] }
220
+ : system
221
+ ])
222
+ ),
145
223
  knowledge: {
146
- nodes: [
147
- {
148
- id: 'knowledge.test-playbook-1',
149
- kind: 'playbook',
150
- title: 'Test Playbook',
151
- summary: 'Test playbook summary.',
152
- body: '## Overview\n\nContent.',
153
- links: [{ nodeId: 'feature:sales.crm' }, { nodeId: 'feature:sales.lead-gen' }],
154
- ownerIds: [],
155
- updatedAt: '2026-01-01'
156
- },
157
- {
158
- id: 'knowledge.test-strategy-1',
159
- kind: 'strategy',
160
- title: 'Test Strategy',
161
- summary: 'Test strategy summary.',
162
- body: '## Strategy\n\nContent.',
163
- links: [{ nodeId: 'feature:sales.lead-gen' }],
164
- ownerIds: [],
165
- updatedAt: '2026-01-02'
166
- },
167
- {
168
- id: 'knowledge.test-reference-1',
169
- kind: 'reference',
170
- title: 'Test Reference',
171
- summary: 'Test reference summary.',
172
- body: '## Reference\n\nContent.',
173
- links: [],
174
- ownerIds: [],
175
- updatedAt: '2026-01-03'
176
- }
177
- ]
224
+ 'knowledge.test-playbook-1': {
225
+ id: 'knowledge.test-playbook-1',
226
+ kind: 'playbook',
227
+ title: 'Test Playbook',
228
+ summary: 'Test playbook summary.',
229
+ body: '## Overview\n\nContent.',
230
+ links: [
231
+ { target: { kind: 'system', id: 'sales.crm' }, nodeId: 'system:sales.crm' },
232
+ { target: { kind: 'system', id: 'sales.lead-gen' }, nodeId: 'system:sales.lead-gen' }
233
+ ],
234
+ ownerIds: [],
235
+ updatedAt: '2026-01-01'
236
+ },
237
+ 'knowledge.test-strategy-1': {
238
+ id: 'knowledge.test-strategy-1',
239
+ kind: 'strategy',
240
+ title: 'Test Strategy',
241
+ summary: 'Test strategy summary.',
242
+ body: '## Strategy\n\nContent.',
243
+ links: [],
244
+ ownerIds: [],
245
+ updatedAt: '2026-01-02'
246
+ },
247
+ 'knowledge.test-reference-1': {
248
+ id: 'knowledge.test-reference-1',
249
+ kind: 'reference',
250
+ title: 'Test Reference',
251
+ summary: 'Test reference summary.',
252
+ body: '## Reference\n\nContent.',
253
+ links: [],
254
+ ownerIds: [],
255
+ updatedAt: '2026-01-03'
256
+ }
178
257
  }
179
258
  }
180
259
 
@@ -183,11 +262,11 @@ describe('buildOrganizationGraph with knowledge', () => {
183
262
 
184
263
  it('emits a knowledge node for each synthetic node', () => {
185
264
  const knowledgeNodes = graph.nodes.filter((n) => n.kind === 'knowledge')
186
- expect(knowledgeNodes.length).toBe(SYNTHETIC_OM.knowledge.nodes.length)
265
+ expect(knowledgeNodes.length).toBe(Object.keys(SYNTHETIC_OM.knowledge).length)
187
266
  })
188
267
 
189
268
  it('knowledge nodes carry sourceId matching the OM node id', () => {
190
- for (const omNode of SYNTHETIC_OM.knowledge.nodes) {
269
+ for (const omNode of Object.values(SYNTHETIC_OM.knowledge)) {
191
270
  const graphNode = graph.nodes.find((n) => n.sourceId === omNode.id)
192
271
  expect(graphNode).toBeDefined()
193
272
  expect(graphNode?.kind).toBe('knowledge')
@@ -197,12 +276,12 @@ describe('buildOrganizationGraph with knowledge', () => {
197
276
 
198
277
  it('emits governs edges for nodes that have links', () => {
199
278
  const governsEdges = graph.edges.filter((e) => e.kind === 'governs')
200
- const totalLinks = SYNTHETIC_OM.knowledge.nodes.reduce((sum, n) => sum + n.links.length, 0)
279
+ const totalLinks = Object.values(SYNTHETIC_OM.knowledge).reduce((sum, n) => sum + n.links.length, 0)
201
280
  expect(governsEdges.length).toBe(totalLinks)
202
281
  })
203
282
 
204
283
  it('links targetId resolves to an existing graph node id for at least one node', () => {
205
- const nodeWithLinks = SYNTHETIC_OM.knowledge.nodes.find((n) => n.links.length > 0)
284
+ const nodeWithLinks = Object.values(SYNTHETIC_OM.knowledge).find((n) => n.links.length > 0)
206
285
  expect(nodeWithLinks).toBeDefined()
207
286
 
208
287
  const graphNodeIds = new Set(graph.nodes.map((n) => n.id))
@@ -223,9 +302,22 @@ describe('buildOrganizationGraph with knowledge', () => {
223
302
  // ---------------------------------------------------------------------------
224
303
 
225
304
  describe('KnowledgeLinkSchema', () => {
226
- it('accepts a valid kind:dotted-path nodeId', () => {
227
- expect(() => KnowledgeLinkSchema.parse({ nodeId: 'feature:sales.crm' })).not.toThrow()
228
- expect(() => KnowledgeLinkSchema.parse({ nodeId: 'feature:sales.lead-gen' })).not.toThrow()
305
+ it('accepts and normalizes a canonical typed target', () => {
306
+ const link = KnowledgeLinkSchema.parse({ target: { kind: 'system', id: 'sales.crm' } })
307
+
308
+ expect(link).toEqual({
309
+ target: { kind: 'system', id: 'sales.crm' },
310
+ nodeId: 'system:sales.crm'
311
+ })
312
+ })
313
+
314
+ it('accepts and normalizes a legacy kind:dotted-path nodeId', () => {
315
+ const link = KnowledgeLinkSchema.parse({ nodeId: 'system:sales.lead-gen' })
316
+
317
+ expect(link).toEqual({
318
+ target: { kind: 'system', id: 'sales.lead-gen' },
319
+ nodeId: 'system:sales.lead-gen'
320
+ })
229
321
  })
230
322
 
231
323
  it('accepts resource: and knowledge: prefixed node IDs', () => {
@@ -233,8 +325,11 @@ describe('KnowledgeLinkSchema', () => {
233
325
  expect(() => KnowledgeLinkSchema.parse({ nodeId: 'knowledge:knowledge.test-doc' })).not.toThrow()
234
326
  })
235
327
 
328
+ it('accepts content-node: prefixed node IDs (D4 — KnowledgeTargetKindSchema includes content-node)', () => {
329
+ expect(() => KnowledgeLinkSchema.parse({ nodeId: 'content-node:sales.crm:default-pipeline' })).not.toThrow()
330
+ })
331
+
236
332
  it('rejects a bare dotted ID with no kind: prefix', () => {
237
- // NodeIdStringSchema requires kind:dotted-path format
238
333
  expect(KnowledgeLinkSchema.safeParse({ nodeId: 'sales.crm' }).success).toBe(false)
239
334
  })
240
335
 
@@ -243,8 +338,7 @@ describe('KnowledgeLinkSchema', () => {
243
338
  })
244
339
 
245
340
  it('rejects a nodeId with uppercase characters', () => {
246
- // NodeIdStringSchema regex requires lowercase
247
- expect(KnowledgeLinkSchema.safeParse({ nodeId: 'Feature:Sales.CRM' }).success).toBe(false)
341
+ expect(KnowledgeLinkSchema.safeParse({ nodeId: 'system:Sales.CRM' }).success).toBe(false)
248
342
  })
249
343
 
250
344
  it('rejects a link missing the nodeId field', () => {
@@ -256,6 +350,73 @@ describe('KnowledgeLinkSchema', () => {
256
350
  })
257
351
  })
258
352
 
353
+ describe('KnowledgeTargetRefSchema', () => {
354
+ it('accepts modeled target kinds', () => {
355
+ expect(KnowledgeTargetRefSchema.safeParse({ kind: 'system', id: 'sales.crm' }).success).toBe(true)
356
+ expect(KnowledgeTargetRefSchema.safeParse({ kind: 'customer-segment', id: 'customer.enterprise' }).success).toBe(
357
+ true
358
+ )
359
+ })
360
+
361
+ it('accepts content-node as a target kind (D4)', () => {
362
+ expect(KnowledgeTargetKindSchema.safeParse('content-node').success).toBe(true)
363
+ expect(KnowledgeTargetRefSchema.safeParse({ kind: 'content-node', id: 'sales.crm:default-pipeline' }).success).toBe(
364
+ true
365
+ )
366
+ })
367
+
368
+ it('rejects unknown target kinds', () => {
369
+ expect(KnowledgeTargetKindSchema.safeParse('integration').success).toBe(false)
370
+ expect(KnowledgeTargetRefSchema.safeParse({ kind: 'integration', id: 'hubspot' }).success).toBe(false)
371
+ })
372
+ })
373
+
374
+ // ---------------------------------------------------------------------------
375
+ // KnowledgeDomainSchema — flat Record shape (Phase 4 D3)
376
+ // ---------------------------------------------------------------------------
377
+
378
+ describe('KnowledgeDomainSchema — flat Record<id, OrgKnowledgeNode>', () => {
379
+ it('accepts an empty map', () => {
380
+ expect(KnowledgeDomainSchema.safeParse({}).success).toBe(true)
381
+ })
382
+
383
+ it('defaults to empty map when undefined', () => {
384
+ const result = KnowledgeDomainSchema.safeParse(undefined)
385
+ expect(result.success).toBe(true)
386
+ if (result.success) {
387
+ expect(result.data).toEqual({})
388
+ }
389
+ })
390
+
391
+ it('accepts a Record keyed by knowledge node ids', () => {
392
+ const result = KnowledgeDomainSchema.safeParse({
393
+ 'knowledge.my-playbook': {
394
+ id: 'knowledge.my-playbook',
395
+ kind: 'playbook',
396
+ title: 'My Playbook',
397
+ summary: 'A test playbook.',
398
+ body: '## Content',
399
+ links: [],
400
+ ownerIds: [],
401
+ updatedAt: '2026-05-01'
402
+ }
403
+ })
404
+ expect(result.success).toBe(true)
405
+ })
406
+
407
+ it('rejects the old wrapper shape { version, lastModified, nodes[] }', () => {
408
+ // The old wrapper-with-array shape does not parse as a Record<id, OrgKnowledgeNode>
409
+ const result = KnowledgeDomainSchema.safeParse({
410
+ version: 1,
411
+ lastModified: '2026-05-10',
412
+ nodes: []
413
+ })
414
+ // Each key ('version', 'lastModified', 'nodes') would be parsed as a knowledge node id,
415
+ // and the values are not valid OrgKnowledgeNodeSchema — so this should fail
416
+ expect(result.success).toBe(false)
417
+ })
418
+ })
419
+
259
420
  // ---------------------------------------------------------------------------
260
421
  // OrgKnowledgeNodeSchema — field length constraints
261
422
  // ---------------------------------------------------------------------------
@@ -308,7 +469,6 @@ describe('OrgKnowledgeNodeSchema length constraints', () => {
308
469
  })
309
470
 
310
471
  it('rejects id with uppercase characters', () => {
311
- // ModelIdSchema requires lowercase kebab/dot/underscore separated IDs
312
472
  const result = OrgKnowledgeNodeSchema.safeParse({ ...BASE, id: 'Knowledge.Test' })
313
473
  expect(result.success).toBe(false)
314
474
  })