@elevasis/core 0.22.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 (112) hide show
  1. package/dist/index.d.ts +2330 -2391
  2. package/dist/index.js +2322 -1147
  3. package/dist/knowledge/index.d.ts +702 -1136
  4. package/dist/knowledge/index.js +9 -9
  5. package/dist/organization-model/index.d.ts +2330 -2391
  6. package/dist/organization-model/index.js +2322 -1147
  7. package/dist/test-utils/index.d.ts +703 -1106
  8. package/dist/test-utils/index.js +1735 -1089
  9. package/package.json +1 -1
  10. package/src/__tests__/template-core-compatibility.test.ts +11 -79
  11. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +360 -98
  12. package/src/business/acquisition/api-schemas.test.ts +2 -2
  13. package/src/business/acquisition/api-schemas.ts +7 -9
  14. package/src/business/acquisition/build-templates.test.ts +4 -4
  15. package/src/business/acquisition/build-templates.ts +72 -30
  16. package/src/business/acquisition/crm-state-actions.test.ts +13 -11
  17. package/src/business/acquisition/types.ts +7 -3
  18. package/src/execution/engine/agent/core/types.ts +1 -1
  19. package/src/execution/engine/workflow/types.ts +2 -2
  20. package/src/knowledge/README.md +8 -7
  21. package/src/knowledge/__tests__/queries.test.ts +74 -73
  22. package/src/knowledge/format.ts +10 -9
  23. package/src/knowledge/index.ts +1 -1
  24. package/src/knowledge/published.ts +1 -1
  25. package/src/knowledge/queries.ts +26 -25
  26. package/src/organization-model/README.md +66 -26
  27. package/src/organization-model/__tests__/content-kinds-registry.test.ts +210 -0
  28. package/src/organization-model/__tests__/defaults.test.ts +72 -98
  29. package/src/organization-model/__tests__/domains/actions.test.ts +56 -0
  30. package/src/organization-model/__tests__/domains/customers.test.ts +299 -295
  31. package/src/organization-model/__tests__/domains/entities.test.ts +56 -0
  32. package/src/organization-model/__tests__/domains/goals.test.ts +493 -479
  33. package/src/organization-model/__tests__/domains/identity.test.ts +280 -279
  34. package/src/organization-model/__tests__/domains/navigation.test.ts +268 -212
  35. package/src/organization-model/__tests__/domains/offerings.test.ts +414 -419
  36. package/src/organization-model/__tests__/domains/policies.test.ts +323 -0
  37. package/src/organization-model/__tests__/domains/resource-mappings.test.ts +271 -271
  38. package/src/organization-model/__tests__/domains/resources.test.ts +159 -37
  39. package/src/organization-model/__tests__/domains/roles.test.ts +147 -86
  40. package/src/organization-model/__tests__/domains/statuses.test.ts +246 -243
  41. package/src/organization-model/__tests__/domains/systems.test.ts +67 -51
  42. package/src/organization-model/__tests__/flatten-additive-merge.test.ts +361 -0
  43. package/src/organization-model/__tests__/foundation.test.ts +74 -102
  44. package/src/organization-model/__tests__/get-resources-for-system.test.ts +144 -0
  45. package/src/organization-model/__tests__/graph.test.ts +899 -71
  46. package/src/organization-model/__tests__/knowledge.test.ts +173 -52
  47. package/src/organization-model/__tests__/lookup-helpers.test.ts +438 -0
  48. package/src/organization-model/__tests__/migration-helpers.test.ts +591 -0
  49. package/src/organization-model/__tests__/prospecting-ssot.test.ts +36 -27
  50. package/src/organization-model/__tests__/recursive-system-schema.test.ts +520 -0
  51. package/src/organization-model/__tests__/resolve.test.ts +174 -23
  52. package/src/organization-model/__tests__/schema.test.ts +291 -114
  53. package/src/organization-model/__tests__/surface-projection.test.ts +207 -97
  54. package/src/organization-model/catalogs/lead-gen.ts +144 -0
  55. package/src/organization-model/content-kinds/config.ts +36 -0
  56. package/src/organization-model/content-kinds/index.ts +74 -0
  57. package/src/organization-model/content-kinds/pipeline.ts +68 -0
  58. package/src/organization-model/content-kinds/registry.ts +44 -0
  59. package/src/organization-model/content-kinds/status.ts +71 -0
  60. package/src/organization-model/content-kinds/template.ts +83 -0
  61. package/src/organization-model/content-kinds/types.ts +117 -0
  62. package/src/organization-model/contracts.ts +13 -3
  63. package/src/organization-model/defaults.ts +488 -96
  64. package/src/organization-model/domains/actions.ts +239 -0
  65. package/src/organization-model/domains/customers.ts +78 -75
  66. package/src/organization-model/domains/entities.ts +144 -0
  67. package/src/organization-model/domains/goals.ts +83 -80
  68. package/src/organization-model/domains/knowledge.ts +74 -16
  69. package/src/organization-model/domains/navigation.ts +107 -384
  70. package/src/organization-model/domains/offerings.ts +71 -66
  71. package/src/organization-model/domains/policies.ts +102 -0
  72. package/src/organization-model/domains/projects.ts +14 -48
  73. package/src/organization-model/domains/prospecting.ts +62 -181
  74. package/src/organization-model/domains/resources.ts +81 -24
  75. package/src/organization-model/domains/roles.ts +13 -10
  76. package/src/organization-model/domains/sales.ts +10 -219
  77. package/src/organization-model/domains/shared.ts +57 -57
  78. package/src/organization-model/domains/statuses.ts +339 -130
  79. package/src/organization-model/domains/systems.ts +186 -29
  80. package/src/organization-model/foundation.ts +54 -67
  81. package/src/organization-model/graph/build.ts +682 -54
  82. package/src/organization-model/graph/link.ts +1 -1
  83. package/src/organization-model/graph/schema.ts +24 -9
  84. package/src/organization-model/graph/types.ts +20 -7
  85. package/src/organization-model/helpers.ts +231 -26
  86. package/src/organization-model/index.ts +116 -5
  87. package/src/organization-model/migration-helpers.ts +249 -0
  88. package/src/organization-model/organization-graph.mdx +16 -15
  89. package/src/organization-model/organization-model.mdx +89 -41
  90. package/src/organization-model/published.ts +120 -18
  91. package/src/organization-model/resolve.ts +117 -54
  92. package/src/organization-model/schema.ts +561 -140
  93. package/src/organization-model/surface-projection.ts +116 -122
  94. package/src/organization-model/types.ts +102 -21
  95. package/src/platform/constants/versions.ts +1 -1
  96. package/src/platform/registry/__tests__/command-view.test.ts +6 -8
  97. package/src/platform/registry/__tests__/resource-link.test.ts +13 -8
  98. package/src/platform/registry/__tests__/resource-registry.integration.test.ts +16 -31
  99. package/src/platform/registry/__tests__/resource-registry.nested-systems.test.ts +245 -0
  100. package/src/platform/registry/__tests__/resource-registry.test.ts +9 -7
  101. package/src/platform/registry/__tests__/validation.test.ts +15 -11
  102. package/src/platform/registry/resource-registry.ts +20 -8
  103. package/src/platform/registry/serialization.ts +7 -7
  104. package/src/platform/registry/types.ts +3 -3
  105. package/src/platform/registry/validation.ts +17 -15
  106. package/src/reference/_generated/contracts.md +362 -99
  107. package/src/reference/glossary.md +18 -18
  108. package/src/supabase/database.types.ts +60 -0
  109. package/src/test-utils/test-utils.test.ts +1 -6
  110. package/src/organization-model/__tests__/domains/operations.test.ts +0 -203
  111. package/src/organization-model/domains/features.ts +0 -31
  112. package/src/organization-model/domains/operations.ts +0 -85
@@ -1,76 +1,86 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { DEFAULT_ORGANIZATION_MODEL } from '../defaults'
3
- import {
4
- projectOrganizationSurfaces,
5
- validateOrganizationSurfaceProjection
6
- } from '../surface-projection'
7
- import type { OrganizationModel, OrganizationModelFeature, OrganizationModelSurface } from '../types'
8
-
9
- function makeFeature(
10
- id: string,
11
- overrides: Partial<OrganizationModelFeature> = {}
12
- ): OrganizationModelFeature {
3
+ import { ancestorsOf, childrenOf, topLevel } from '../helpers'
4
+ import { projectOrganizationSurfaces, validateOrganizationSurfaceProjection } from '../surface-projection'
5
+ import type {
6
+ OrganizationModel,
7
+ OrganizationModelSidebarNode,
8
+ OrganizationModelSidebarSurfaceNode,
9
+ OrganizationModelSystemEntry
10
+ } from '../types'
11
+
12
+ function makeSystem(id: string, overrides: Partial<OrganizationModelSystemEntry> = {}): OrganizationModelSystemEntry {
13
13
  return {
14
14
  id,
15
+ order: 10,
15
16
  label: id,
17
+ lifecycle: 'active',
16
18
  enabled: true,
17
19
  ...overrides
18
20
  }
19
21
  }
20
22
 
21
23
  function makeSurface(
22
- id: string,
23
- overrides: Partial<OrganizationModelSurface> = {}
24
- ): OrganizationModelSurface {
24
+ label: string,
25
+ overrides: Partial<OrganizationModelSidebarSurfaceNode> = {}
26
+ ): OrganizationModelSidebarSurfaceNode {
25
27
  return {
26
- id,
27
- label: id,
28
- path: `/${id.replaceAll('.', '/')}`,
28
+ type: 'surface',
29
+ label,
30
+ path: `/${label.toLowerCase().replaceAll(' ', '-')}`,
29
31
  surfaceType: 'page',
30
- enabled: true,
31
- featureIds: [],
32
- entityIds: [],
33
- resourceIds: [],
34
- capabilityIds: [],
32
+ order: 10,
33
+ targets: {},
35
34
  ...overrides
36
35
  }
37
36
  }
38
37
 
39
38
  function makeModel(
40
- features: OrganizationModelFeature[],
41
- surfaces: OrganizationModelSurface[],
42
- navigation: Partial<OrganizationModel['navigation']> = {}
39
+ systems: OrganizationModelSystemEntry[],
40
+ primary: Record<string, OrganizationModelSidebarNode>,
41
+ bottom: Record<string, OrganizationModelSidebarNode> = {}
43
42
  ): OrganizationModel {
44
43
  return {
45
44
  ...DEFAULT_ORGANIZATION_MODEL,
46
- features,
45
+ systems: Object.fromEntries(
46
+ systems.map((system, index) => [system.id, { ...system, order: system.order ?? (index + 1) * 10 }])
47
+ ),
47
48
  navigation: {
48
- surfaces,
49
- groups: [],
50
- ...navigation
49
+ sidebar: {
50
+ primary,
51
+ bottom
52
+ }
51
53
  }
52
54
  }
53
55
  }
54
56
 
55
57
  describe('projectOrganizationSurfaces', () => {
56
- it('projects navigation surfaces into data-only DTOs with inherited flags', () => {
58
+ it('projects recursive sidebar surface leaves into data-only DTOs with inherited flags', () => {
57
59
  const model = makeModel(
58
60
  [
59
- makeFeature('sales', { requiresAdmin: true }),
60
- makeFeature('sales.crm', { devOnly: true })
61
+ makeSystem('sales', { requiresAdmin: true }),
62
+ makeSystem('sales.crm', { parentSystemId: 'sales', devOnly: true })
61
63
  ],
62
- [
63
- makeSurface('crm.pipeline', {
64
- label: 'Pipeline',
65
- path: '/crm/pipeline',
66
- surfaceType: 'graph',
67
- featureId: 'sales.crm',
68
- featureIds: ['sales.crm'],
69
- entityIds: ['crm.deal'],
70
- resourceIds: ['workflow.crm-sync'],
71
- capabilityIds: ['crm.pipeline.manage']
72
- })
73
- ]
64
+ {
65
+ business: {
66
+ type: 'group',
67
+ label: 'Business',
68
+ order: 20,
69
+ children: {
70
+ 'crm.pipeline': makeSurface('Pipeline', {
71
+ path: '/crm/pipeline',
72
+ surfaceType: 'graph',
73
+ icon: 'feature.crm',
74
+ targets: {
75
+ systems: ['sales.crm'],
76
+ entities: ['crm.deal'],
77
+ resources: ['workflow.crm-sync'],
78
+ actions: ['crm.pipeline.manage']
79
+ }
80
+ })
81
+ }
82
+ }
83
+ }
74
84
  )
75
85
 
76
86
  expect(projectOrganizationSurfaces(model)).toEqual([
@@ -79,11 +89,12 @@ describe('projectOrganizationSurfaces', () => {
79
89
  label: 'Pipeline',
80
90
  path: '/crm/pipeline',
81
91
  surfaceType: 'graph',
82
- featureId: 'sales.crm',
83
- featureIds: ['sales.crm'],
92
+ icon: 'feature.crm',
93
+ order: 10,
94
+ systemIds: ['sales.crm'],
84
95
  entityIds: ['crm.deal'],
85
96
  resourceIds: ['workflow.crm-sync'],
86
- capabilityIds: ['crm.pipeline.manage'],
97
+ actionIds: ['crm.pipeline.manage'],
87
98
  enabled: true,
88
99
  devOnly: true,
89
100
  requiresAdmin: true
@@ -91,84 +102,183 @@ describe('projectOrganizationSurfaces', () => {
91
102
  ])
92
103
  })
93
104
 
94
- it('canonicalizes legacy feature aliases', () => {
105
+ it('sorts sidebar records by order before projecting leaves', () => {
106
+ const model = makeModel([makeSystem('a'), makeSystem('b'), makeSystem('c')], {
107
+ third: makeSurface('Third', { order: 30, targets: { systems: ['c'] } }),
108
+ first: makeSurface('First', { order: 10, targets: { systems: ['a'] } }),
109
+ second: makeSurface('Second', { order: 20, targets: { systems: ['b'] } })
110
+ })
111
+
112
+ expect(projectOrganizationSurfaces(model).map((surface) => surface.id)).toEqual(['first', 'second', 'third'])
113
+ })
114
+
115
+ it('uses canonical semantic system IDs directly', () => {
95
116
  const model = makeModel(
96
117
  [
97
- makeFeature('sales'),
98
- makeFeature('sales.crm'),
99
- makeFeature('sales.lead-gen'),
100
- makeFeature('monitoring'),
101
- makeFeature('monitoring.submitted-requests')
118
+ makeSystem('revenue'),
119
+ makeSystem('revenue.crm'),
120
+ makeSystem('revenue.lead-gen'),
121
+ makeSystem('delivery'),
122
+ makeSystem('delivery.projects')
102
123
  ],
103
- [
104
- makeSurface('legacy.sales', {
105
- featureId: 'crm',
106
- featureIds: ['crm', 'lead-gen', 'submitted-requests']
124
+ {
125
+ 'canonical.sales': makeSurface('Canonical Sales', {
126
+ targets: { systems: ['revenue.crm', 'revenue.lead-gen', 'delivery.projects'] }
107
127
  })
108
- ]
128
+ }
109
129
  )
110
130
 
111
131
  expect(projectOrganizationSurfaces(model)[0]).toMatchObject({
112
- featureId: 'sales.crm',
113
- featureIds: ['sales.crm', 'sales.lead-gen', 'monitoring.submitted-requests']
132
+ systemIds: ['revenue.crm', 'revenue.lead-gen', 'delivery.projects']
114
133
  })
115
134
  })
116
135
 
117
- it('disables projected surfaces when feature lineage is disabled', () => {
136
+ it('disables projected surfaces when system lineage is disabled', () => {
118
137
  const model = makeModel(
119
- [
120
- makeFeature('sales', { enabled: false }),
121
- makeFeature('sales.crm')
122
- ],
123
- [
124
- makeSurface('crm.pipeline', {
125
- enabled: true,
126
- featureId: 'sales.crm',
127
- featureIds: ['sales.crm']
138
+ [makeSystem('sales', { enabled: false }), makeSystem('sales.crm', { parentSystemId: 'sales' })],
139
+ {
140
+ 'crm.pipeline': makeSurface('Pipeline', {
141
+ targets: { systems: ['sales.crm'] }
128
142
  })
129
- ]
143
+ }
130
144
  )
131
145
 
132
- expect(projectOrganizationSurfaces(model)[0].enabled).toBe(false)
146
+ expect(projectOrganizationSurfaces(model)[0]?.enabled).toBe(false)
133
147
  })
134
148
  })
135
149
 
136
150
  describe('validateOrganizationSurfaceProjection', () => {
137
- it('returns issue codes for duplicate and unknown surface references', () => {
151
+ it('returns issue codes for duplicate sidebar leaf ids, duplicate paths, and unknown system targets', () => {
138
152
  const model = makeModel(
139
- [makeFeature('sales'), makeFeature('sales.crm')],
140
- [
141
- makeSurface('crm.pipeline', {
142
- path: '/crm/pipeline',
143
- featureId: 'sales.crm',
144
- featureIds: ['sales.crm']
145
- }),
146
- makeSurface('crm.pipeline', {
147
- path: '/crm/pipeline/',
148
- featureId: 'missing.primary',
149
- featureIds: ['missing.related']
150
- })
151
- ],
153
+ [makeSystem('sales'), makeSystem('sales.crm')],
152
154
  {
153
- defaultSurfaceId: 'missing.default',
154
- groups: [
155
- {
156
- id: 'primary',
157
- label: 'Primary',
158
- placement: 'primary',
159
- surfaceIds: ['missing.group']
155
+ business: {
156
+ type: 'group',
157
+ label: 'Business',
158
+ children: {
159
+ 'crm.pipeline': makeSurface('Pipeline', {
160
+ path: '/crm/pipeline',
161
+ targets: { systems: ['sales.crm'] }
162
+ })
160
163
  }
161
- ]
164
+ }
165
+ },
166
+ {
167
+ 'crm.pipeline': makeSurface('Pipeline Duplicate Id', {
168
+ path: '/crm/pipeline/',
169
+ targets: { systems: ['missing.related'] }
170
+ })
162
171
  }
163
172
  )
164
173
 
165
174
  expect(validateOrganizationSurfaceProjection(model).map((issue) => issue.code)).toEqual([
166
175
  'duplicate-surface-id',
167
176
  'duplicate-surface-path',
168
- 'unknown-surface-feature',
169
- 'unknown-surface-feature-reference',
170
- 'unknown-default-surface',
171
- 'unknown-group-surface'
177
+ 'unknown-surface-system'
172
178
  ])
173
179
  })
180
+
181
+ it('emits unknown-surface-system with surfaceId and systemId for unrecognised system references', () => {
182
+ const model = makeModel([makeSystem('exists')], {
183
+ 'test.surface': makeSurface('Test', {
184
+ targets: { systems: ['exists', 'does.not.exist'] }
185
+ })
186
+ })
187
+
188
+ const issues = validateOrganizationSurfaceProjection(model)
189
+ expect(issues).toHaveLength(1)
190
+ expect(issues[0]).toMatchObject({
191
+ code: 'unknown-surface-system',
192
+ surfaceId: 'test.surface',
193
+ systemId: 'does.not.exist'
194
+ })
195
+ })
196
+ })
197
+
198
+ describe('projectOrganizationSurfaces - flag inheritance', () => {
199
+ it('inherits requiresAdmin transitively from grandparent via parentSystemId chain', () => {
200
+ const model = makeModel(
201
+ [
202
+ makeSystem('gp', { requiresAdmin: true }),
203
+ makeSystem('gp.parent', { parentSystemId: 'gp' }),
204
+ makeSystem('gp.parent.leaf', { parentSystemId: 'gp.parent' })
205
+ ],
206
+ { 'test.surface': makeSurface('Test', { targets: { systems: ['gp.parent.leaf'] } }) }
207
+ )
208
+
209
+ expect(projectOrganizationSurfaces(model)[0]).toMatchObject({ requiresAdmin: true })
210
+ })
211
+
212
+ it('computes devOnly from lifecycle beta even when devOnly is not set', () => {
213
+ const model = makeModel([makeSystem('beta-sys', { lifecycle: 'beta' })], {
214
+ 'test.surface': makeSurface('Test', { targets: { systems: ['beta-sys'] } })
215
+ })
216
+
217
+ expect(projectOrganizationSurfaces(model)[0]).toMatchObject({ devOnly: true })
218
+ })
219
+
220
+ it('preserves explicit sidebar devOnly and requiresAdmin flags', () => {
221
+ const model = makeModel([makeSystem('active-sys', { enabled: true, lifecycle: 'active' })], {
222
+ 'test.surface': makeSurface('Test', {
223
+ devOnly: true,
224
+ requiresAdmin: true,
225
+ targets: { systems: ['active-sys'] }
226
+ })
227
+ })
228
+
229
+ expect(projectOrganizationSurfaces(model)[0]).toMatchObject({ devOnly: true, requiresAdmin: true })
230
+ })
231
+
232
+ it('disables surface and preserves both systemIds when any referenced system is disabled', () => {
233
+ const model = makeModel([makeSystem('sys-a', { enabled: true }), makeSystem('sys-b', { enabled: false })], {
234
+ 'test.surface': makeSurface('Test', { targets: { systems: ['sys-a', 'sys-b'] } })
235
+ })
236
+
237
+ const projection = projectOrganizationSurfaces(model)[0]
238
+ expect(projection?.enabled).toBe(false)
239
+ expect(projection?.systemIds).toEqual(['sys-a', 'sys-b'])
240
+ })
241
+
242
+ it('disables surface for deprecated lifecycle and archived lifecycle even when enabled is true', () => {
243
+ for (const lifecycle of ['deprecated', 'archived'] as const) {
244
+ const model = makeModel([makeSystem('gated-sys', { lifecycle, enabled: true })], {
245
+ 'test.surface': makeSurface('Test', { targets: { systems: ['gated-sys'] } })
246
+ })
247
+
248
+ expect(projectOrganizationSurfaces(model)[0]?.enabled, `lifecycle: ${lifecycle}`).toBe(false)
249
+ }
250
+ })
251
+
252
+ it('filters unknown system references from projection systemIds', () => {
253
+ const model = makeModel([makeSystem('exists')], {
254
+ 'test.surface': makeSurface('Test', { targets: { systems: ['exists', 'does.not.exist'] } })
255
+ })
256
+
257
+ expect(projectOrganizationSurfaces(model)[0]?.systemIds).toEqual(['exists'])
258
+ })
259
+ })
260
+
261
+ describe('helpers - hierarchy traversal', () => {
262
+ const systems: Record<string, OrganizationModelSystemEntry> = {
263
+ a: makeSystem('a'),
264
+ 'a.b': makeSystem('a.b'),
265
+ 'a.b.c': makeSystem('a.b.c'),
266
+ x: makeSystem('x')
267
+ }
268
+
269
+ it('ancestorsOf returns all path-segment ancestors including self in root-to-leaf order', () => {
270
+ expect(ancestorsOf(systems, 'a.b.c').map((s) => s.id)).toEqual(['a', 'a.b', 'a.b.c'])
271
+ })
272
+
273
+ it('childrenOf returns only direct children, not grandchildren', () => {
274
+ expect(childrenOf(systems, 'a').map((s) => s.id)).toEqual(['a.b'])
275
+ })
276
+
277
+ it('topLevel returns only root-level systems with no dot in their id', () => {
278
+ expect(
279
+ topLevel(systems)
280
+ .map((s) => s.id)
281
+ .sort()
282
+ ).toEqual(['a', 'x'])
283
+ })
174
284
  })
@@ -0,0 +1,144 @@
1
+ // ============================================================================
2
+ // Lead-Gen Stage Catalog (OM Spine processing-state model)
3
+ //
4
+ // Canonical set of processing stage keys for acq_companies.processing_state and
5
+ // acq_contacts.processing_state. These keys coordinate build templates, workflow
6
+ // factory validation, API filters, and UI progress projections.
7
+ //
8
+ // State is sparse: absent keys mean "not attempted"; present keys hold terminal
9
+ // status entries such as success, no_result, skipped, or error.
10
+ //
11
+ // Historical sources:
12
+ // ACQ_LIST_MEMBERS_LEAD_GEN_PIPELINE -> personalized, uploaded, interested,
13
+ // discovered, verified
14
+ // ACQ_LIST_COMPANIES_LEAD_GEN_PIPELINE -> populated, extracted, qualified
15
+ // Design plan hint (lead-gen-domain-cleanup.mdx section 4) -> scraped, enriched
16
+ //
17
+ // ============================================================================
18
+
19
+ /** One entry in the lead-gen stage catalog. */
20
+ export interface LeadGenStageCatalogEntry {
21
+ /** Matches the status key written into processing_state jsonb (e.g. 'scraped'). */
22
+ key: string
23
+ /** Human-readable label for UI display. */
24
+ label: string
25
+ /** Short description of what this stage represents. */
26
+ description: string
27
+ /** Canonical pipeline order for UI sorting. Lower = earlier in the funnel. */
28
+ order: number
29
+ /** Which entity's processing_state jsonb carries this stage status. */
30
+ entity: 'company' | 'contact'
31
+ /** Additional entities allowed to write/read this processing_state key. */
32
+ additionalEntities?: Array<'company' | 'contact'>
33
+ /**
34
+ * Optional read-side override for Records views when a company-scoped step
35
+ * produces records on a different entity.
36
+ */
37
+ recordEntity?: 'company' | 'contact'
38
+ /** Stage key to read from recordEntity.processing_state for Records views. */
39
+ recordStageKey?: string
40
+ }
41
+
42
+ /**
43
+ * Canonical lead-gen processing stage catalog.
44
+ * Keys are the stage names written by workflow steps into processing_state jsonb.
45
+ *
46
+ * Ordered roughly by pipeline progression (prospecting -> outreach -> qualification).
47
+ */
48
+ export const LEAD_GEN_STAGE_CATALOG: Record<string, LeadGenStageCatalogEntry> = {
49
+ // Prospecting - company population
50
+ scraped: {
51
+ key: 'scraped',
52
+ label: 'Scraped',
53
+ description: 'Company was scraped from a source directory (Apify actor run).',
54
+ order: 1,
55
+ entity: 'company'
56
+ },
57
+ populated: {
58
+ key: 'populated',
59
+ label: 'Companies found',
60
+ description: 'Companies have been found and added to the lead-gen list.',
61
+ order: 2,
62
+ entity: 'company'
63
+ },
64
+ crawled: {
65
+ key: 'crawled',
66
+ label: 'Websites crawled',
67
+ description:
68
+ 'Company websites have been crawled (e.g. via Apify) and raw page content stored for downstream LLM analysis.',
69
+ order: 2.5,
70
+ entity: 'company'
71
+ },
72
+ extracted: {
73
+ key: 'extracted',
74
+ label: 'Websites analyzed',
75
+ description: 'Company websites have been analyzed for business signals.',
76
+ order: 3,
77
+ entity: 'company'
78
+ },
79
+ enriched: {
80
+ key: 'enriched',
81
+ label: 'Enriched',
82
+ description: 'Company or contact enriched with third-party data (e.g. Tomba, Anymailfinder).',
83
+ order: 4,
84
+ entity: 'company'
85
+ },
86
+ 'decision-makers-enriched': {
87
+ key: 'decision-makers-enriched',
88
+ label: 'Decision-makers found',
89
+ description: 'Decision-maker contacts discovered and attached to a qualified company.',
90
+ order: 6,
91
+ entity: 'company',
92
+ recordEntity: 'contact',
93
+ recordStageKey: 'discovered'
94
+ },
95
+
96
+ // Prospecting - contact discovery
97
+ discovered: {
98
+ key: 'discovered',
99
+ label: 'Decision-makers found',
100
+ description: 'Decision-maker contact details have been found.',
101
+ order: 5,
102
+ entity: 'contact'
103
+ },
104
+ verified: {
105
+ key: 'verified',
106
+ label: 'Emails verified',
107
+ description: 'Contact email addresses have been checked for deliverability.',
108
+ order: 7,
109
+ entity: 'contact'
110
+ },
111
+
112
+ // Qualification
113
+ qualified: {
114
+ key: 'qualified',
115
+ label: 'Companies qualified',
116
+ description: 'Companies have been scored against the qualification criteria.',
117
+ order: 8,
118
+ entity: 'company'
119
+ },
120
+
121
+ // Outreach
122
+ personalized: {
123
+ key: 'personalized',
124
+ label: 'Personalized',
125
+ description: 'Outreach message personalized for the contact (Instantly personalization workflow).',
126
+ order: 9,
127
+ entity: 'contact'
128
+ },
129
+ uploaded: {
130
+ key: 'uploaded',
131
+ label: 'Reviewed and exported',
132
+ description: 'Approved records have been reviewed and exported for handoff.',
133
+ order: 10,
134
+ entity: 'company',
135
+ additionalEntities: ['contact']
136
+ },
137
+ interested: {
138
+ key: 'interested',
139
+ label: 'Interested',
140
+ description: 'Contact replied with a positive signal (Instantly reply-handler transition).',
141
+ order: 11,
142
+ entity: 'contact'
143
+ }
144
+ }
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod'
2
+ import { defineContentType } from './registry'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // config kinds (Phase 3, Wave 2A)
6
+ // ---------------------------------------------------------------------------
7
+ //
8
+ // `config:kv` — a flat key-value configuration store co-located with a system.
9
+ // Per L15: single starter config type; feature-flag semantics are
10
+ // a usage pattern over kv, not a separate type.
11
+ //
12
+ // Per L15: `config` is a first-class registered meta-kind. Scope: NEW
13
+ // tenant-defined settings co-located with a system. Strongly-typed platform
14
+ // fields (system.ui.devOnly, system.ui.requiresAdmin, etc.) stay where they
15
+ // are and are NOT hoisting destinations for existing typed fields.
16
+
17
+ const ConfigKvPayloadSchema = z.object({
18
+ /**
19
+ * Flat key-value entries. Values are JSON primitives.
20
+ * Keys are short identifiers (e.g. 'maxBatchSize', 'featureEnabled').
21
+ */
22
+ entries: z
23
+ .record(z.string().trim().min(1).max(200), z.union([z.string(), z.number(), z.boolean(), z.null()]))
24
+ .meta({ label: 'Entries', hint: 'Key-value configuration entries (string, number, boolean, or null values)' })
25
+ })
26
+
27
+ export type ConfigKvPayload = z.infer<typeof ConfigKvPayloadSchema>
28
+
29
+ export const configKvKind = defineContentType({
30
+ kind: 'config',
31
+ type: 'kv',
32
+ label: 'Key-Value Config',
33
+ description: 'A flat key-value configuration store co-located with a system. Values are JSON primitives.',
34
+ payloadSchema: ConfigKvPayloadSchema,
35
+ parentTypes: []
36
+ })
@@ -0,0 +1,74 @@
1
+ // ---------------------------------------------------------------------------
2
+ // content-kinds barrel (Phase 3, Wave 2A)
3
+ // ---------------------------------------------------------------------------
4
+ //
5
+ // Per D8 (locked): CONTENT_KIND_REGISTRY is populated via static import, not
6
+ // side-effecting registration. This file is the single source of truth for
7
+ // the populated registry. `registry.ts` exports the EMPTY registry constant
8
+ // (kept for historical/import-chain reasons) — consumers MUST import from
9
+ // this barrel, not from `registry.ts` directly, to get the populated registry.
10
+ //
11
+ // Registry keys follow the `<kind>:<type>` ContentTypeKey format (per L4, D8).
12
+
13
+ export { defineContentType } from './registry'
14
+ export type { ContentTypeKey, ContentTypeDefinition } from './types'
15
+ export { ContentNodeBaseSchema, ContentNodeSchema, ExtensionNodeSchema } from './types'
16
+ export type { ContentNodeBase, ContentNode, ExtensionNode } from './types'
17
+
18
+ export { pipelineKind, stageKind } from './pipeline'
19
+ export type { PipelinePayload, StagePayload } from './pipeline'
20
+
21
+ export { templateKind, templateStepKind } from './template'
22
+ export type { TemplatePayload, TemplateStepPayload } from './template'
23
+
24
+ export { statusFlowKind, statusKind } from './status'
25
+ export type { StatusFlowPayload, StatusPayload } from './status'
26
+
27
+ export { configKvKind } from './config'
28
+ export type { ConfigKvPayload } from './config'
29
+
30
+ import { pipelineKind, stageKind } from './pipeline'
31
+ import { templateKind, templateStepKind } from './template'
32
+ import { statusFlowKind, statusKind } from './status'
33
+ import { configKvKind } from './config'
34
+ import type { ContentTypeDefinition, ContentTypeKey } from './types'
35
+
36
+ /**
37
+ * The populated content-kind registry for all platform-shipped kinds.
38
+ * Per D8: static const object keyed by `'<kind>:<type>'` composite key.
39
+ *
40
+ * Keys:
41
+ * 'schema:pipeline' — pipeline applying to an entity type
42
+ * 'schema:stage' — stage within a pipeline (parentTypes: ['schema:pipeline'])
43
+ * 'schema:template' — named build template
44
+ * 'schema:template-step' — step within a template (parentTypes: ['schema:template'])
45
+ * 'schema:status-flow' — status set for a project/milestone/task scope
46
+ * 'schema:status' — single status within a flow (parentTypes: ['schema:status-flow'])
47
+ * 'config:kv' — flat key-value config store co-located with a system
48
+ *
49
+ * Wave 3A's `ContentNodeDescribeView` consumes `lookupContentType(kind, type)`
50
+ * which indexes into this registry. Import `lookupContentType` from this barrel
51
+ * to get the populated version.
52
+ */
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ export const CONTENT_KIND_REGISTRY: Readonly<Record<ContentTypeKey, ContentTypeDefinition<any>>> = {
55
+ 'schema:pipeline': pipelineKind,
56
+ 'schema:stage': stageKind,
57
+ 'schema:template': templateKind,
58
+ 'schema:template-step': templateStepKind,
59
+ 'schema:status-flow': statusFlowKind,
60
+ 'schema:status': statusKind,
61
+ 'config:kv': configKvKind
62
+ } as const
63
+
64
+ /**
65
+ * Look up a registered content-type definition by (kind, type).
66
+ * Uses the populated CONTENT_KIND_REGISTRY from this barrel — wave 2A override
67
+ * of the empty registry exported from registry.ts.
68
+ * Returns `undefined` when the pair is not registered — per D2, this is not
69
+ * an error; unregistered pairs render generically and skip payload validation.
70
+ */
71
+ export function lookupContentType(kind: string, type: string): ContentTypeDefinition | undefined {
72
+ const key: ContentTypeKey = `${kind}:${type}`
73
+ return CONTENT_KIND_REGISTRY[key]
74
+ }