@elevasis/core 0.24.1 → 0.25.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 (32) hide show
  1. package/dist/index.d.ts +75 -3
  2. package/dist/index.js +332 -4
  3. package/dist/knowledge/index.d.ts +30 -1
  4. package/dist/organization-model/index.d.ts +75 -3
  5. package/dist/organization-model/index.js +332 -4
  6. package/dist/test-utils/index.d.ts +1 -0
  7. package/dist/test-utils/index.js +4 -3
  8. package/package.json +1 -1
  9. package/src/_gen/__tests__/__snapshots__/contracts.md.snap +94 -94
  10. package/src/execution/engine/llm/adapters/__tests__/openrouter.integration.test.ts +10 -10
  11. package/src/knowledge/__tests__/queries.test.ts +960 -546
  12. package/src/knowledge/format.ts +322 -100
  13. package/src/knowledge/index.ts +18 -5
  14. package/src/knowledge/queries.ts +1004 -239
  15. package/src/organization-model/__tests__/deprecate-helpers.test.ts +71 -0
  16. package/src/organization-model/__tests__/resolve.test.ts +9 -7
  17. package/src/organization-model/__tests__/scaffolders.test.ts +93 -0
  18. package/src/organization-model/defaults.ts +3 -3
  19. package/src/organization-model/helpers.ts +76 -9
  20. package/src/organization-model/icons.ts +1 -0
  21. package/src/organization-model/index.ts +3 -2
  22. package/src/organization-model/published.ts +15 -2
  23. package/src/organization-model/scaffolders/helpers.ts +84 -0
  24. package/src/organization-model/scaffolders/index.ts +19 -0
  25. package/src/organization-model/scaffolders/scaffoldKnowledgeNode.ts +48 -0
  26. package/src/organization-model/scaffolders/scaffoldOntologyRecord.ts +38 -0
  27. package/src/organization-model/scaffolders/scaffoldResource.ts +59 -0
  28. package/src/organization-model/scaffolders/scaffoldSystem.ts +110 -0
  29. package/src/organization-model/scaffolders/types.ts +81 -0
  30. package/src/platform/constants/versions.ts +1 -1
  31. package/src/reference/_generated/contracts.md +94 -94
  32. package/src/reference/glossary.md +71 -69
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ getSystemDeprecationDependents,
4
+ type OrganizationModel,
5
+ type OmTopologyDomain
6
+ } from '../index'
7
+
8
+ const model = {
9
+ systems: {
10
+ sales: {
11
+ id: 'sales',
12
+ order: 1,
13
+ label: 'Sales',
14
+ lifecycle: 'active',
15
+ systems: {
16
+ crm: {
17
+ id: 'sales.crm',
18
+ order: 2,
19
+ label: 'CRM',
20
+ lifecycle: 'active'
21
+ }
22
+ }
23
+ }
24
+ },
25
+ resources: {
26
+ 'crm-active-workflow': {
27
+ id: 'crm-active-workflow',
28
+ order: 1,
29
+ kind: 'workflow',
30
+ systemPath: 'sales.crm',
31
+ status: 'active',
32
+ codeRefs: []
33
+ },
34
+ 'crm-archived-workflow': {
35
+ id: 'crm-archived-workflow',
36
+ order: 2,
37
+ kind: 'workflow',
38
+ systemPath: 'sales.crm',
39
+ status: 'archived',
40
+ codeRefs: []
41
+ }
42
+ },
43
+ knowledge: {},
44
+ topology: {
45
+ version: 1,
46
+ relationships: {
47
+ 'crm-active-uses-sales': {
48
+ from: { kind: 'resource', id: 'crm-active-workflow' },
49
+ kind: 'uses',
50
+ to: { kind: 'system', id: 'sales' }
51
+ },
52
+ 'archived-resource-edge': {
53
+ from: { kind: 'resource', id: 'crm-archived-workflow' },
54
+ kind: 'uses',
55
+ to: { kind: 'system', id: 'sales' }
56
+ }
57
+ }
58
+ } satisfies OmTopologyDomain
59
+ } as OrganizationModel
60
+
61
+ describe('getSystemDeprecationDependents', () => {
62
+ it('enumerates active resources and topology edges for a system tree', () => {
63
+ const dependents = getSystemDeprecationDependents(model, 'sales', { includeDescendants: true })
64
+
65
+ expect(dependents.resources.map((resource) => resource.id)).toEqual(['crm-active-workflow'])
66
+ expect(dependents.topologyEdges.map((edge) => edge.id).sort()).toEqual([
67
+ 'archived-resource-edge',
68
+ 'crm-active-uses-sales'
69
+ ])
70
+ })
71
+ })
@@ -31,13 +31,15 @@ describe('organization-model resolve', () => {
31
31
  const business = model.navigation.sidebar.primary.business
32
32
  expect(business?.type).toBe('group')
33
33
  if (business?.type === 'group') {
34
- expect(business.children.clients).toMatchObject({
35
- type: 'surface',
36
- path: '/clients',
37
- targets: { systems: ['clients'] }
38
- })
39
- }
40
- })
34
+ expect(business.children.clients).toMatchObject({
35
+ type: 'surface',
36
+ path: '/clients',
37
+ icon: 'clients',
38
+ targets: { systems: ['clients'] }
39
+ })
40
+ }
41
+ expect(model.systems.clients?.icon).toBe('clients')
42
+ })
41
43
 
42
44
  it('adds tenant systems additively to the merged record', () => {
43
45
  const baseSize = Object.keys(resolveOrganizationModel().systems).length
@@ -0,0 +1,93 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { DEFAULT_ORGANIZATION_MODEL, scaffoldOrganizationModel, type OrganizationModel } from '..'
3
+
4
+ const BASE_MODEL: OrganizationModel = {
5
+ ...DEFAULT_ORGANIZATION_MODEL,
6
+ systems: {
7
+ sales: {
8
+ id: 'sales',
9
+ order: 10,
10
+ label: 'Sales',
11
+ lifecycle: 'active',
12
+ systems: {
13
+ crm: {
14
+ id: 'sales.crm',
15
+ order: 20,
16
+ label: 'CRM',
17
+ lifecycle: 'active'
18
+ }
19
+ }
20
+ }
21
+ },
22
+ resources: {},
23
+ knowledge: {}
24
+ }
25
+
26
+ describe('organization-model scaffolders', () => {
27
+ it('plans the minimal system scaffold without route files', () => {
28
+ const plan = scaffoldOrganizationModel(BASE_MODEL, {
29
+ intent: 'system',
30
+ id: 'sales.partners',
31
+ label: 'Partners',
32
+ description: 'Partner operations.',
33
+ noProject: true
34
+ })
35
+
36
+ expect(plan.intent).toBe('system')
37
+ expect(plan.id).toBe('sales.partners')
38
+ expect(plan.edits.map((edit) => edit.path)).toEqual(
39
+ expect.arrayContaining([
40
+ 'packages/elevasis-core/src/organization-model/systems.ts',
41
+ 'packages/elevasis-core/src/organization-model/navigation.ts',
42
+ 'packages/elevasis-core/src/organization-model/roles.ts',
43
+ 'packages/elevasis-core/src/organization-model/assembly.ts',
44
+ 'packages/elevasis-core/src/organization-model/knowledge.ts'
45
+ ])
46
+ )
47
+ expect(plan.writes.some((write) => write.path.endsWith('manifest.stub.ts'))).toBe(true)
48
+ expect(plan.writes.some((write) => write.path.includes('/routes/'))).toBe(false)
49
+ expect(plan.projectCommand).toBeUndefined()
50
+ })
51
+
52
+ it('defaults system scaffolds to a project chain unless skipped', () => {
53
+ const plan = scaffoldOrganizationModel(BASE_MODEL, {
54
+ intent: 'system',
55
+ id: 'sales.enablement',
56
+ label: 'Enablement'
57
+ })
58
+
59
+ expect(plan.projectCommand).toContain('project:create --link-om sales.enablement')
60
+ })
61
+
62
+ it('keeps ontology/resource/knowledge project chains opt-in', () => {
63
+ const ontology = scaffoldOrganizationModel(BASE_MODEL, {
64
+ intent: 'ontology',
65
+ id: 'deal-score',
66
+ systemPath: 'sales.crm',
67
+ kind: 'object'
68
+ })
69
+ const resource = scaffoldOrganizationModel(BASE_MODEL, {
70
+ intent: 'resource',
71
+ id: 'crm-score-workflow',
72
+ systemPath: 'sales.crm'
73
+ })
74
+ const knowledge = scaffoldOrganizationModel(BASE_MODEL, {
75
+ intent: 'knowledge',
76
+ id: 'knowledge.crm-scoring',
77
+ systemPath: 'sales.crm'
78
+ })
79
+
80
+ expect(ontology.projectCommand).toBeUndefined()
81
+ expect(resource.projectCommand).toBeUndefined()
82
+ expect(knowledge.projectCommand).toBeUndefined()
83
+ })
84
+
85
+ it('rejects duplicate system paths during planning', () => {
86
+ expect(() =>
87
+ scaffoldOrganizationModel(BASE_MODEL, {
88
+ intent: 'system',
89
+ id: 'sales.crm'
90
+ })
91
+ ).toThrow('system already exists: sales.crm')
92
+ })
93
+ })
@@ -46,7 +46,7 @@ const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: OrganizationModelNavigation = {
46
46
  business: {
47
47
  type: 'group',
48
48
  label: 'Business',
49
- icon: 'business',
49
+ icon: 'briefcase',
50
50
  order: 20,
51
51
  children: {
52
52
  sales: {
@@ -63,7 +63,7 @@ const DEFAULT_ORGANIZATION_MODEL_NAVIGATION: OrganizationModelNavigation = {
63
63
  label: 'Clients',
64
64
  path: '/clients',
65
65
  surfaceType: 'list',
66
- icon: 'projects',
66
+ icon: 'clients',
67
67
  order: 20,
68
68
  targets: { systems: ['clients'] }
69
69
  },
@@ -438,7 +438,7 @@ export const DEFAULT_ORGANIZATION_MODEL: OrganizationModel = {
438
438
  enabled: true,
439
439
  lifecycle: 'active',
440
440
  color: 'orange',
441
- icon: 'projects',
441
+ icon: 'clients',
442
442
  path: '/clients'
443
443
  },
444
444
  operations: {
@@ -2,6 +2,7 @@ import type { OrganizationModel, OrganizationModelSystemEntry } from './types'
2
2
  import type { ContentNode } from './content-kinds/types'
3
3
  import type { JsonValue } from './domains/systems'
4
4
  import type { ResourceEntry } from './domains/resources'
5
+ import type { OmTopologyRelationship } from './domains/topology'
5
6
 
6
7
  // W1A has landed: ContentNode and SystemEntry (with content + subsystems) are now
7
8
  // defined in their canonical locations.
@@ -314,14 +315,80 @@ export function resolveSystemConfig(model: OrganizationModel, path: string): Rec
314
315
  * getResourcesForSystem(model, 'sales', { includeDescendants: true })
315
316
  * // → resources where systemPath === 'sales' OR systemPath starts with 'sales.'
316
317
  */
317
- export function getResourcesForSystem(
318
- model: OrganizationModel,
319
- systemPath: string,
320
- options: { includeDescendants?: boolean } = {}
321
- ): ResourceEntry[] {
318
+ export function getResourcesForSystem(
319
+ model: OrganizationModel,
320
+ systemPath: string,
321
+ options: { includeDescendants?: boolean } = {}
322
+ ): ResourceEntry[] {
322
323
  const { includeDescendants = false } = options
323
324
  const prefix = systemPath + '.'
324
- return Object.values(model.resources ?? {}).filter(
325
- (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
326
- )
327
- }
325
+ return Object.values(model.resources ?? {}).filter(
326
+ (r) => r.systemPath === systemPath || (includeDescendants && r.systemPath.startsWith(prefix))
327
+ )
328
+ }
329
+
330
+ export interface SystemTopologyEdge {
331
+ id: string
332
+ relationship: OmTopologyRelationship
333
+ }
334
+
335
+ export interface SystemDeprecationDependents {
336
+ resources: ResourceEntry[]
337
+ topologyEdges: SystemTopologyEdge[]
338
+ }
339
+
340
+ function systemPathIsInScope(candidate: string, systemPath: string, includeDescendants: boolean): boolean {
341
+ return candidate === systemPath || (includeDescendants && candidate.startsWith(`${systemPath}.`))
342
+ }
343
+
344
+ function topologyRelationshipTouchesSystem(
345
+ relationship: OmTopologyRelationship,
346
+ systemPath: string,
347
+ resourceIds: Set<string>,
348
+ includeDescendants: boolean
349
+ ): boolean {
350
+ if (
351
+ relationship.systemPath !== undefined &&
352
+ systemPathIsInScope(relationship.systemPath, systemPath, includeDescendants)
353
+ ) {
354
+ return true
355
+ }
356
+
357
+ for (const ref of [relationship.from, relationship.to]) {
358
+ if (ref.kind === 'system' && systemPathIsInScope(ref.id, systemPath, includeDescendants)) return true
359
+ if (ref.kind === 'resource' && resourceIds.has(ref.id)) return true
360
+ }
361
+
362
+ return false
363
+ }
364
+
365
+ export function getTopologyEdgesForSystem(
366
+ model: OrganizationModel,
367
+ systemPath: string,
368
+ options: { includeDescendants?: boolean; resourceIds?: Iterable<string> } = {}
369
+ ): SystemTopologyEdge[] {
370
+ const { includeDescendants = false } = options
371
+ const resourceIds = new Set(
372
+ options.resourceIds ?? getResourcesForSystem(model, systemPath, { includeDescendants }).map((r) => r.id)
373
+ )
374
+
375
+ return Object.entries(model.topology?.relationships ?? {})
376
+ .filter(([, relationship]) =>
377
+ topologyRelationshipTouchesSystem(relationship, systemPath, resourceIds, includeDescendants)
378
+ )
379
+ .map(([id, relationship]) => ({ id, relationship }))
380
+ }
381
+
382
+ export function getSystemDeprecationDependents(
383
+ model: OrganizationModel,
384
+ systemPath: string,
385
+ options: { includeDescendants?: boolean } = {}
386
+ ): SystemDeprecationDependents {
387
+ const resources = getResourcesForSystem(model, systemPath, options).filter((resource) => resource.status === 'active')
388
+ const resourceIds = new Set(resources.map((resource) => resource.id))
389
+
390
+ return {
391
+ resources,
392
+ topologyEdges: getTopologyEdgesForSystem(model, systemPath, { ...options, resourceIds })
393
+ }
394
+ }
@@ -8,6 +8,7 @@ export const ORGANIZATION_MODEL_ICON_TOKENS = [
8
8
  'crm',
9
9
  'lead-gen',
10
10
  'projects',
11
+ 'clients',
11
12
  'operations',
12
13
  'monitoring',
13
14
  'knowledge',
@@ -13,8 +13,9 @@ export * from './resolve'
13
13
  export * from './foundation'
14
14
  export * from './surface-projection'
15
15
  export * from './helpers'
16
- export * from './migration-helpers'
17
- export * from './graph'
16
+ export * from './migration-helpers'
17
+ export * from './scaffolders'
18
+ export * from './graph'
18
19
  export * from './catalogs/lead-gen'
19
20
  export * from './domains/branding'
20
21
  // Phase 4: OrganizationModelSalesSchema, OrganizationModelProspectingSchema,
@@ -201,8 +201,21 @@ export {
201
201
  } from './domains/knowledge'
202
202
  export { defineOrganizationModel, resolveOrganizationModel, resolveOrganizationModelWithResources } from './resolve'
203
203
  export type { ResolvedSystemEntry, ResolvedOrganizationModel } from './resolve'
204
- export { createFoundationOrganizationModel } from './foundation'
205
-
204
+ export { createFoundationOrganizationModel } from './foundation'
205
+ export { scaffoldOrganizationModel } from './scaffolders'
206
+ export type {
207
+ BaseOmScaffoldSpec,
208
+ KnowledgeNodeScaffoldSpec,
209
+ OmScaffoldEdit,
210
+ OmScaffoldIntent,
211
+ OmScaffoldPlan,
212
+ OmScaffoldSpec,
213
+ OmScaffoldWrite,
214
+ OntologyRecordScaffoldSpec,
215
+ ResourceScaffoldSpec,
216
+ SystemScaffoldSpec
217
+ } from './scaffolders'
218
+
206
219
  export type {
207
220
  OntologyActionType,
208
221
  OntologyCatalogType,
@@ -0,0 +1,84 @@
1
+ import { formatOntologyId, type OntologyKind, type OrganizationModel } from '..'
2
+ import { getSystem, listAllSystems } from '../helpers'
3
+ import type { OmScaffoldPlan } from './types'
4
+
5
+ export function slugify(input: string): string {
6
+ return input
7
+ .trim()
8
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, '-')
11
+ .replace(/^-+|-+$/g, '')
12
+ }
13
+
14
+ export function titleize(input: string): string {
15
+ return input
16
+ .split(/[-._\s]+/)
17
+ .filter(Boolean)
18
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
19
+ .join(' ')
20
+ }
21
+
22
+ export function assertSystemPathAvailable(model: OrganizationModel, path: string): void {
23
+ if (getSystem(model, path) !== undefined) {
24
+ throw new Error(`system already exists: ${path}`)
25
+ }
26
+
27
+ const parent = parentPathOf(path)
28
+ if (parent !== undefined && getSystem(model, parent) === undefined) {
29
+ throw new Error(`parent system does not exist: ${parent}`)
30
+ }
31
+ }
32
+
33
+ export function assertSystemExists(model: OrganizationModel, path: string): void {
34
+ if (getSystem(model, path) === undefined) {
35
+ throw new Error(`system does not exist: ${path}`)
36
+ }
37
+ }
38
+
39
+ export function parentPathOf(path: string): string | undefined {
40
+ const index = path.lastIndexOf('.')
41
+ return index === -1 ? undefined : path.slice(0, index)
42
+ }
43
+
44
+ export function localIdOf(path: string): string {
45
+ return path.split('.').at(-1) ?? path
46
+ }
47
+
48
+ export function nextSystemOrder(model: OrganizationModel, parentPath?: string): number {
49
+ const siblings =
50
+ parentPath === undefined
51
+ ? listAllSystems(model).filter(({ path }) => !path.includes('.'))
52
+ : listAllSystems(model).filter(({ path }) => parentPathOf(path) === parentPath)
53
+ const maxOrder = siblings.reduce((max, { system }) => Math.max(max, system.order), 0)
54
+ return Math.ceil((maxOrder + 10) / 10) * 10
55
+ }
56
+
57
+ export function ontologyMapName(kind: OntologyKind): string {
58
+ const map: Record<OntologyKind, string> = {
59
+ object: 'objectTypes',
60
+ link: 'linkTypes',
61
+ action: 'actionTypes',
62
+ catalog: 'catalogTypes',
63
+ event: 'eventTypes',
64
+ interface: 'interfaceTypes',
65
+ 'value-type': 'valueTypes',
66
+ property: 'sharedProperties',
67
+ group: 'groups',
68
+ surface: 'surfaces'
69
+ }
70
+ return map[kind]
71
+ }
72
+
73
+ export function makeOntologyId(systemPath: string, kind: OntologyKind, localId: string): string {
74
+ return formatOntologyId({ scope: systemPath, kind, localId: slugify(localId) })
75
+ }
76
+
77
+ export function addProjectNextStep(plan: OmScaffoldPlan, enabled: boolean, command: string): OmScaffoldPlan {
78
+ if (!enabled) return plan
79
+ return {
80
+ ...plan,
81
+ projectCommand: command,
82
+ nextSteps: [...plan.nextSteps, `Run project handoff: ${command}`]
83
+ }
84
+ }
@@ -0,0 +1,19 @@
1
+ import type { OrganizationModel } from '..'
2
+ import { scaffoldKnowledgeNode } from './scaffoldKnowledgeNode'
3
+ import { scaffoldOntologyRecord } from './scaffoldOntologyRecord'
4
+ import { scaffoldResource } from './scaffoldResource'
5
+ import { scaffoldSystem } from './scaffoldSystem'
6
+ import type { OmScaffoldPlan, OmScaffoldSpec } from './types'
7
+
8
+ export function scaffoldOrganizationModel(model: OrganizationModel, spec: OmScaffoldSpec): OmScaffoldPlan {
9
+ if (spec.intent === 'system') return scaffoldSystem(model, spec)
10
+ if (spec.intent === 'ontology') return scaffoldOntologyRecord(model, spec)
11
+ if (spec.intent === 'resource') return scaffoldResource(model, spec)
12
+ return scaffoldKnowledgeNode(model, spec)
13
+ }
14
+
15
+ export * from './types'
16
+ export { scaffoldKnowledgeNode } from './scaffoldKnowledgeNode'
17
+ export { scaffoldOntologyRecord } from './scaffoldOntologyRecord'
18
+ export { scaffoldResource } from './scaffoldResource'
19
+ export { scaffoldSystem } from './scaffoldSystem'
@@ -0,0 +1,48 @@
1
+ import type { OrganizationModel } from '..'
2
+ import { assertSystemExists, slugify, titleize } from './helpers'
3
+ import type { KnowledgeNodeScaffoldSpec, OmScaffoldPlan } from './types'
4
+
5
+ export function scaffoldKnowledgeNode(model: OrganizationModel, spec: KnowledgeNodeScaffoldSpec): OmScaffoldPlan {
6
+ if (spec.systemPath !== undefined) assertSystemExists(model, spec.systemPath)
7
+ if (model.knowledge?.[spec.id] !== undefined) {
8
+ throw new Error(`knowledge node already exists: ${spec.id}`)
9
+ }
10
+
11
+ const title = spec.label ?? titleize(spec.id.replace(/^knowledge\./, ''))
12
+ const slug = slugify(spec.id.replace(/^knowledge\./, ''))
13
+ const kind = spec.kind ?? 'reference'
14
+ const links = spec.systemPath === undefined ? '' : `links:\n - system:${spec.systemPath}\n`
15
+ const ownerIds = spec.ownerRoleId === undefined ? '' : `ownerIds:\n - ${spec.ownerRoleId}\n`
16
+
17
+ return {
18
+ intent: 'knowledge',
19
+ id: spec.id,
20
+ summary: `Scaffold ${kind} knowledge node "${spec.id}".`,
21
+ writes: [
22
+ {
23
+ path: `packages/elevasis-core/src/knowledge/nodes/${slug}.mdx.stub`,
24
+ mode: 'create',
25
+ description: 'Knowledge node MDX skeleton.',
26
+ content: `---\nid: ${spec.id}\nkind: ${kind}\ntitle: ${JSON.stringify(title)}\nsummary: ${JSON.stringify(spec.description ?? `${title} knowledge node.`)}\n${links}${ownerIds}updatedAt: 2026-05-15\n---\n\n## Overview\n\nCapture the durable operating knowledge here.\n`
27
+ }
28
+ ],
29
+ edits: [
30
+ {
31
+ path: 'packages/elevasis-core/src/organization-model/knowledge.ts',
32
+ description: 'Register this generated node after replacing the .stub suffix.',
33
+ snippet: `${JSON.stringify(spec.id)}`
34
+ }
35
+ ],
36
+ warnings: [],
37
+ nextSteps: [
38
+ 'Replace the .stub suffix after the node is ready to become canonical.',
39
+ spec.systemPath === undefined
40
+ ? 'Add links to the OM nodes this knowledge governs.'
41
+ : `Run pnpm exec elevasis om:verify --scope ${spec.systemPath}.`
42
+ ],
43
+ projectCommand:
44
+ spec.withProject && spec.systemPath !== undefined
45
+ ? `pnpm exec elevasis project:create --link-om ${spec.systemPath} --title "Add ${title} knowledge" --kind om-change`
46
+ : undefined
47
+ }
48
+ }
@@ -0,0 +1,38 @@
1
+ import type { OrganizationModel } from '..'
2
+ import { assertSystemExists, makeOntologyId, ontologyMapName, titleize } from './helpers'
3
+ import type { OmScaffoldPlan, OntologyRecordScaffoldSpec } from './types'
4
+
5
+ export function scaffoldOntologyRecord(model: OrganizationModel, spec: OntologyRecordScaffoldSpec): OmScaffoldPlan {
6
+ assertSystemExists(model, spec.systemPath)
7
+ const localId = spec.localId ?? spec.id
8
+ const ontologyId = makeOntologyId(spec.systemPath, spec.kind, localId)
9
+ const label = spec.label ?? titleize(localId)
10
+ const mapName = ontologyMapName(spec.kind)
11
+
12
+ return {
13
+ intent: 'ontology',
14
+ id: ontologyId,
15
+ summary: `Scaffold ${spec.kind} ontology record "${ontologyId}".`,
16
+ writes: [],
17
+ edits: [
18
+ {
19
+ path: 'packages/elevasis-core/src/organization-model/systems.ts',
20
+ description: `Add this record under ${spec.systemPath}.ontology.${mapName}.`,
21
+ snippet: `${JSON.stringify(ontologyId)}: {
22
+ id: ${JSON.stringify(ontologyId)},
23
+ label: ${JSON.stringify(label)},
24
+ ownerSystemId: ${JSON.stringify(spec.systemPath)}${spec.description === undefined ? '' : `,
25
+ description: ${JSON.stringify(spec.description)}`}
26
+ }`
27
+ }
28
+ ],
29
+ warnings: [],
30
+ nextSteps: [
31
+ 'Wire any cross-references using canonical ontology IDs.',
32
+ `Run pnpm exec elevasis om:verify --scope ${spec.systemPath}.`
33
+ ],
34
+ projectCommand: spec.withProject
35
+ ? `pnpm exec elevasis project:create --link-om ${spec.systemPath} --title "Add ${label} ontology" --kind om-change`
36
+ : undefined
37
+ }
38
+ }
@@ -0,0 +1,59 @@
1
+ import type { OrganizationModel } from '..'
2
+ import { assertSystemExists, slugify, titleize } from './helpers'
3
+ import type { OmScaffoldPlan, ResourceScaffoldSpec } from './types'
4
+
5
+ export function scaffoldResource(model: OrganizationModel, spec: ResourceScaffoldSpec): OmScaffoldPlan {
6
+ assertSystemExists(model, spec.systemPath)
7
+ if (model.resources?.[spec.id] !== undefined) {
8
+ throw new Error(`resource already exists: ${spec.id}`)
9
+ }
10
+
11
+ const label = spec.label ?? titleize(spec.id)
12
+ const kind = spec.kind ?? 'workflow'
13
+ const slug = slugify(spec.id)
14
+ const writes = spec.withStubWorkflow
15
+ ? [
16
+ {
17
+ path: `packages/elevasis-core/src/workflows/${slug}.stub.ts`,
18
+ mode: 'create' as const,
19
+ description: 'Optional workflow implementation stub. Runtime wiring remains manual.',
20
+ content: `// Stub only. Replace with a real workflow implementation before registering the resource.\nexport const ${slug.replaceAll('-', '_')}Workflow = {\n id: ${JSON.stringify(spec.id)}\n}\n`
21
+ }
22
+ ]
23
+ : []
24
+
25
+ return {
26
+ intent: 'resource',
27
+ id: spec.id,
28
+ summary: `Scaffold ${kind} Resource "${spec.id}" for System "${spec.systemPath}".`,
29
+ writes,
30
+ edits: [
31
+ {
32
+ path: 'packages/elevasis-core/src/organization-model/resources.ts',
33
+ description: 'Add this descriptor to platformResources/platformResourceDescriptors.',
34
+ snippet: `${JSON.stringify(spec.id)}: {
35
+ id: ${JSON.stringify(spec.id)},
36
+ order: 0,
37
+ kind: ${JSON.stringify(kind)},
38
+ systemPath: ${JSON.stringify(spec.systemPath)},
39
+ title: ${JSON.stringify(label)},
40
+ ${spec.description === undefined ? '' : `description: ${JSON.stringify(spec.description)},\n `}${spec.ownerRoleId === undefined ? '' : `ownerRoleId: ${JSON.stringify(spec.ownerRoleId)},\n `}status: 'active',
41
+ codeRefs: []
42
+ }`
43
+ },
44
+ {
45
+ path: 'packages/elevasis-core/src/organization-model/topology.ts',
46
+ description: 'Add topology relationships only when this resource triggers, uses, or requires approval from another OM node.',
47
+ snippet: `// topologyRelationship({ from: topologyRef('resource', ${JSON.stringify(spec.id)}), kind: 'uses', to: ... })`
48
+ }
49
+ ],
50
+ warnings: ['Resource ontology bindings are intentionally omitted until action/read/write contracts are known.'],
51
+ nextSteps: [
52
+ 'Bind resource.ontology.actions/reads/writes after the ontology records exist.',
53
+ `Run pnpm exec elevasis om:verify --scope ${spec.systemPath}.`
54
+ ],
55
+ projectCommand: spec.withProject
56
+ ? `pnpm exec elevasis project:create --link-om ${spec.systemPath} --title "Add ${label} resource" --kind om-change`
57
+ : undefined
58
+ }
59
+ }