@astrale-os/cli 1.0.0-beta.26 → 1.0.0-beta.28

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 (73) hide show
  1. package/README.md +31 -5
  2. package/dist/astrale.js +1201 -358
  3. package/package.json +3 -2
  4. package/src/commands/__tests__/domain-install-operation.test.ts +23 -0
  5. package/src/commands/__tests__/install-identity-override.test.ts +3 -3
  6. package/src/commands/domain/install.ts +19 -11
  7. package/src/commands/get.ts +1 -1
  8. package/src/commands/identity/register.ts +3 -4
  9. package/src/commands/logs.ts +2 -5
  10. package/src/commands/session/analyze.ts +26 -4
  11. package/src/commands/update.ts +61 -40
  12. package/src/lib/__tests__/skills.test.ts +809 -0
  13. package/src/lib/skills/lock.ts +117 -0
  14. package/src/lib/skills/sync.ts +814 -0
  15. package/src/lib/skills.ts +1 -52
  16. package/src/program/__tests__/program.test.ts +1 -1
  17. package/src/setup/steps/skills.ts +40 -38
  18. package/src/telemetry/__tests__/retention.test.ts +180 -0
  19. package/src/telemetry/__tests__/settings.test.ts +102 -0
  20. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  21. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  22. package/src/telemetry/recorder.ts +6 -3
  23. package/src/telemetry/retention.ts +129 -0
  24. package/src/telemetry/session.ts +18 -12
  25. package/src/telemetry/settings.ts +64 -11
  26. package/src/telemetry/store.ts +87 -9
  27. package/src/telemetry/trigger.ts +16 -28
  28. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  29. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  30. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  31. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  32. package/studio/client/dist/index.html +3 -3
  33. package/studio/server/agent/prompts/anchors.test.ts +17 -4
  34. package/studio/server/agent/prompts/anchors.ts +3 -2
  35. package/studio/server/agent/prompts/system.test.ts +2 -3
  36. package/studio/server/agent/prompts/system.ts +3 -3
  37. package/studio/server/agent/run/preparation.ts +1 -1
  38. package/studio/server/api/context.ts +1 -1
  39. package/studio/server/api/deployment.ts +1 -1
  40. package/studio/server/api/views.ts +2 -2
  41. package/studio/server/api/workspace.ts +1 -1
  42. package/studio/server/cache.test.ts +3 -8
  43. package/studio/server/cache.ts +4 -45
  44. package/studio/server/cli-consumers.test.ts +31 -2
  45. package/studio/server/handoff/copy.ts +1 -1
  46. package/studio/server/introspect/canonical-schema.test.ts +154 -128
  47. package/studio/server/introspect/canonical-schema.ts +183 -302
  48. package/studio/server/introspect/core.ts +14 -21
  49. package/studio/server/introspect/extractor.ts +11 -15
  50. package/studio/server/introspect/overlay-tsmorph.test.ts +1 -5
  51. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  52. package/studio/server/introspect/overlay.ts +3 -24
  53. package/studio/server/introspect/revision.test.ts +24 -28
  54. package/studio/server/introspect/revision.ts +10 -21
  55. package/studio/server/introspect/runtime.test.ts +14 -14
  56. package/studio/server/introspect/runtime.ts +1 -46
  57. package/studio/server/lifecycle.ts +4 -1
  58. package/studio/server/state/documents.test.ts +69 -0
  59. package/studio/server/state/documents.ts +57 -10
  60. package/studio/server/workspace/updates.ts +23 -1
  61. package/studio/shared/contracts/runtime.ts +4 -0
  62. package/studio/shared/contracts/schema.ts +10 -26
  63. package/studio/shared/contracts/surface.test.ts +2 -2
  64. package/studio/shared/contracts/workspace.ts +3 -0
  65. package/studio/shared/schema/identity.ts +0 -1
  66. package/tsconfig.json +1 -1
  67. package/studio/client/dist/assets/index-C0FAnwNw.css +0 -2
  68. package/studio/client/dist/assets/index-OeJK1TjG.js +0 -81
  69. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +0 -1
  70. package/studio/client/dist/assets/schema-studio-DRdfu_cQ.js +0 -8
  71. package/studio/server/introspect/core-extractor.ts +0 -30
  72. package/studio/server/introspect/overlay.test.ts +0 -44
  73. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Pure projection from an SDK-admitted portable DomainSchema into Studio's
3
- * intentionally lossy render model. The extractor calls the authored Domain's
4
- * own installed SDK for admission before this module interprets the document.
2
+ * Project the DSL's portable Schema into Studio's deliberately smaller render
3
+ * model. Admission, dependency reachability, and reference resolution remain
4
+ * owned by the Domain's installed DSL.
5
5
  */
6
6
  import type {
7
7
  IrCallableAuth,
@@ -17,37 +17,78 @@ import type {
17
17
  IrView,
18
18
  JsonSchema,
19
19
  SchemaIR,
20
+ SchemaRevision,
20
21
  StudioCore,
21
22
  } from '../../shared/types'
22
23
 
23
- import { classRefKey, isIrClassRef, schemaRefKey } from '../../shared/schema/identity'
24
24
  import { isSchemaRevision } from '../../shared/types'
25
25
 
26
26
  type AnyRecord = Record<string, unknown>
27
27
 
28
- export type CanonicalDomainSchemaV1 = AnyRecord & {
28
+ /** The portable V1 value transported across the extractor process boundary. */
29
+ export interface CanonicalDomainSchemaV1 {
29
30
  readonly format: 'astrale.dsl'
30
31
  readonly version: 'v1'
31
32
  readonly origin: string
33
+ readonly dependencies: Readonly<
34
+ Record<string, { readonly origin: string; readonly revision: string }>
35
+ >
36
+ readonly classes: Readonly<Record<string, unknown>>
37
+ readonly functions: Readonly<Record<string, unknown>>
38
+ readonly policies: Readonly<Record<string, unknown>>
39
+ readonly views: Readonly<Record<string, unknown>>
40
+ readonly core: unknown
41
+ }
42
+
43
+ interface ResolvedSchemaDomain {
44
+ readonly source: CanonicalDomainSchemaV1
45
+ readonly revision: unknown
46
+ definition(ref: IrClassRef): { readonly definition: unknown } | undefined
47
+ }
48
+
49
+ /** Runtime facade supplied by the inspected Domain, not by Studio's package. */
50
+ export interface SchemaSdk {
51
+ readonly bundle: {
52
+ create(input: any): unknown
53
+ accept(input: any): {
54
+ readonly root: CanonicalDomainSchemaV1
55
+ readonly closure: readonly CanonicalDomainSchemaV1[]
56
+ }
57
+ }
58
+ readonly schema: {
59
+ resolve(input: any): ResolvedSchemaDomain
60
+ compareDependencyMeaning(
61
+ source: CanonicalDomainSchemaV1,
62
+ target: CanonicalDomainSchemaV1,
63
+ ): { readonly footprint: readonly unknown[] }
64
+ }
65
+ readonly ClassKey: {
66
+ is(input: unknown): input is IrClassKey
67
+ ref(input: IrClassKey): IrClassRef
68
+ }
32
69
  }
33
70
 
34
- export interface CanonicalSchemaProjection {
71
+ /** Smaller facade used when re-admitting an installed Bundle for drift checks. */
72
+ export interface SchemaAdmissionSdk {
73
+ readonly bundle: {
74
+ accept(input: any): { readonly root: CanonicalDomainSchemaV1 }
75
+ }
76
+ readonly schema: {
77
+ resolve(input: any): { readonly source: unknown; readonly revision: unknown }
78
+ }
79
+ }
80
+
81
+ export interface CanonicalSchemaExtraction {
82
+ readonly status: 'admitted' | 'preview'
83
+ readonly root: CanonicalDomainSchemaV1
84
+ readonly revision: SchemaRevision | null
35
85
  readonly ir: SchemaIR
36
86
  }
37
87
 
38
- export type CanonicalSchemaAdmission =
39
- | {
40
- readonly status: 'admitted'
41
- readonly root: CanonicalDomainSchemaV1
42
- readonly closure: CanonicalDomainSchemaV1[]
43
- readonly revision: `sha256:${string}`
44
- }
45
- | {
46
- readonly status: 'preview'
47
- readonly root: CanonicalDomainSchemaV1
48
- readonly closure: CanonicalDomainSchemaV1[]
49
- readonly revision: null
50
- }
88
+ interface ImportProjection {
89
+ importsByKey: Record<IrClassKey, IrImportDescriptor>
90
+ importedClassesByKey: Record<IrClassKey, IrClass>
91
+ }
51
92
 
52
93
  export function isCanonicalDomainSchemaV1(value: unknown): value is CanonicalDomainSchemaV1 {
53
94
  const candidate = asRecord(value)
@@ -74,134 +115,32 @@ export function findCanonicalDomainSchemaExport(
74
115
  )
75
116
  }
76
117
 
77
- /** Read the exact retained dependency closure through the authored SDK. */
78
- export function closureFromSdk(
79
- sdkModule: Record<string, unknown>,
80
- root: CanonicalDomainSchemaV1,
81
- ): CanonicalDomainSchemaV1[] {
82
- const create = asRecord(sdkModule.bundle)?.create
83
- if (typeof create !== 'function') return []
84
- try {
85
- const candidate = asRecord(Reflect.apply(create, undefined, [root]))
86
- if (candidate?.root !== root) return []
87
- return admitClosure(candidate.closure, root) ?? []
88
- } catch {
89
- return []
90
- }
91
- }
92
-
93
- /** Re-admit one portable root and prove its revision through the authored SDK. */
94
- export function admitCanonicalSchemaFromSdk(
95
- sdkModule: Record<string, unknown>,
118
+ /**
119
+ * Re-admit the authored Schema as one exact DSL Bundle and resolve its Domain.
120
+ * A V1-shaped value rejected by the installed DSL remains a best-effort local
121
+ * preview while the author is in the middle of an edit.
122
+ */
123
+ export function extractCanonicalSchemaFromSdk(
124
+ sdk: SchemaSdk,
96
125
  candidate: CanonicalDomainSchemaV1,
97
- ): CanonicalSchemaAdmission {
98
- const retainedClosure = closureFromSdk(sdkModule, candidate)
99
- const schemaApi = asRecord(sdkModule.schema)
100
- const accept = schemaApi?.accept
101
- const revision = schemaApi?.revision
102
- const resolve = schemaApi?.resolve
103
- if (
104
- typeof accept !== 'function' ||
105
- typeof revision !== 'function' ||
106
- typeof resolve !== 'function'
107
- ) {
108
- return { status: 'preview', root: candidate, closure: retainedClosure, revision: null }
109
- }
110
-
126
+ ): CanonicalSchemaExtraction {
111
127
  try {
112
- const revisions = new Map<string, string>()
113
- for (const dependency of retainedClosure) {
114
- const value = Reflect.apply(revision, schemaApi, [dependency])
115
- if (!isSchemaRevision(value)) throw new TypeError('SDK returned an invalid revision.')
116
- revisions.set(dependency.origin, value)
128
+ const admitted = sdk.bundle.accept(sdk.bundle.create(candidate))
129
+ if (admitted.root.origin !== candidate.origin) {
130
+ throw new TypeError('DSL Bundle admission returned another Schema root.')
117
131
  }
118
- const lookup = {
119
- get(origin: string, expected: string): CanonicalDomainSchemaV1 | undefined {
120
- const dependency = retainedClosure.find((entry) => entry.origin === origin)
121
- return dependency && revisions.get(origin) === expected ? dependency : undefined
122
- },
132
+ const domain = sdk.schema.resolve(admitted.root)
133
+ if (domain.source !== admitted.root || !isSchemaRevision(domain.revision)) {
134
+ throw new TypeError('DSL resolution did not retain the admitted Schema root.')
123
135
  }
124
- const accepted = Reflect.apply(accept, schemaApi, [candidate, lookup])
125
- if (!isCanonicalDomainSchemaV1(accepted) || accepted.origin !== candidate.origin) {
126
- throw new TypeError('SDK admission returned another root.')
127
- }
128
- const acceptedRevision = Reflect.apply(revision, schemaApi, [accepted])
129
- if (!isSchemaRevision(acceptedRevision))
130
- throw new TypeError('SDK returned an invalid revision.')
131
- const domain = asRecord(Reflect.apply(resolve, schemaApi, [accepted]))
132
- if (domain?.source !== accepted || domain.origin !== accepted.origin) {
133
- throw new TypeError('SDK resolution did not retain the admitted root.')
136
+ return {
137
+ status: 'admitted',
138
+ root: admitted.root,
139
+ revision: domain.revision,
140
+ ir: projectSchema(admitted.root, projectImports(sdk, domain, admitted.closure)),
134
141
  }
135
- const closure = closureFromSdk(sdkModule, accepted)
136
- return { status: 'admitted', root: accepted, closure, revision: acceptedRevision }
137
142
  } catch {
138
- return { status: 'preview', root: candidate, closure: retainedClosure, revision: null }
139
- }
140
- }
141
-
142
- export function projectCanonicalSchema(
143
- root: CanonicalDomainSchemaV1,
144
- closure: readonly CanonicalDomainSchemaV1[] = [],
145
- ): CanonicalSchemaProjection {
146
- const origin = root.origin
147
- const classes = Object.fromEntries(
148
- entriesOf(root.classes).map(([name, declaration]) => [
149
- name,
150
- projectClass(origin, name, declaration),
151
- ]),
152
- )
153
- const functions = Object.fromEntries(
154
- entriesOf(root.functions).map(([name, declaration]) => [
155
- name,
156
- projectFunction(name, declaration),
157
- ]),
158
- )
159
- const views = Object.fromEntries(
160
- entriesOf(root.views).map(([name, declaration]) => [name, projectView(name, declaration)]),
161
- )
162
- const schemas = new Map(
163
- closure.filter(isCanonicalDomainSchemaV1).map((value) => [value.origin, value]),
164
- )
165
- const importsByKey = {} as Record<IrClassKey, IrImportDescriptor>
166
- const importedClassesByKey = {} as Record<IrClassKey, IrClass>
167
- const pending = collectClassRefs(root)
168
- const visited = new Set<string>()
169
-
170
- for (let index = 0; index < pending.length; index += 1) {
171
- const ref = pending[index]
172
- if (ref.origin === origin) continue
173
- const key = classRefKey(ref)
174
- if (visited.has(key)) continue
175
- visited.add(key)
176
- importsByKey[key] = { origin: ref.origin, ref, key }
177
- const owner = schemas.get(ref.origin)
178
- const declaration = asRecord(asRecord(owner?.classes)?.[ref.name])
179
- if (owner === undefined || declaration === null) continue
180
- importedClassesByKey[key] = projectClass(owner.origin, ref.name, declaration)
181
- pending.push(...collectClassRefsFromClass(declaration))
182
- }
183
-
184
- const dependencies = arrayOf(root.dependencies).flatMap((value) => {
185
- const dependency = asRecord(value)
186
- return typeof dependency?.origin === 'string' && typeof dependency.revision === 'string'
187
- ? [{ origin: dependency.origin, revision: dependency.revision }]
188
- : []
189
- })
190
-
191
- return {
192
- ir: {
193
- format: 'astrale.dsl',
194
- version: 'v1',
195
- domain: origin,
196
- classes,
197
- importsByKey,
198
- importedClassesByKey,
199
- functions,
200
- views,
201
- policies: recordCopy(root.policies),
202
- dependencies,
203
- core: jsonCopy(root.core),
204
- },
143
+ return { status: 'preview', root: candidate, revision: null, ir: projectSchema(candidate) }
205
144
  }
206
145
  }
207
146
 
@@ -212,10 +151,9 @@ export function projectCanonicalCore(
212
151
  const core = asRecord(root.core) ?? {}
213
152
  const nodes = entriesOf(core.nodes).map(([slug, raw]) => {
214
153
  const node = asRecord(raw) ?? {}
215
- const selectedClass = classRef(node.class)
216
154
  return {
217
155
  path: corePath({ origin: root.origin, name: slug }),
218
- className: selectedClass?.name ?? '?',
156
+ className: classRef(node.class)?.name ?? '?',
219
157
  data: recordCopy(node.properties),
220
158
  }
221
159
  })
@@ -231,20 +169,64 @@ export function projectCanonicalCore(
231
169
  return { domain: root.origin, nodes, edges }
232
170
  }
233
171
 
172
+ function projectSchema(root: CanonicalDomainSchemaV1, imports?: ImportProjection): SchemaIR {
173
+ return {
174
+ format: 'astrale.dsl',
175
+ version: 'v1',
176
+ domain: root.origin,
177
+ classes: Object.fromEntries(
178
+ entriesOf(root.classes).map(([name, value]) => [
179
+ name,
180
+ projectClass(root.origin, name, value),
181
+ ]),
182
+ ),
183
+ importsByKey: imports?.importsByKey ?? {},
184
+ importedClassesByKey: imports?.importedClassesByKey ?? {},
185
+ functions: Object.fromEntries(
186
+ entriesOf(root.functions).map(([name, value]) => [name, projectFunction(name, value)]),
187
+ ),
188
+ views: Object.fromEntries(
189
+ entriesOf(root.views).map(([name, value]) => [name, projectView(name, value)]),
190
+ ),
191
+ policies: recordCopy(root.policies),
192
+ dependencies: Object.values(root.dependencies ?? {}).map(({ origin, revision }) => ({
193
+ origin,
194
+ revision,
195
+ })),
196
+ core: jsonCopy(root.core),
197
+ }
198
+ }
199
+
200
+ function projectImports(
201
+ sdk: SchemaSdk,
202
+ domain: ReturnType<SchemaSdk['schema']['resolve']>,
203
+ closure: readonly CanonicalDomainSchemaV1[],
204
+ ): ImportProjection {
205
+ const importsByKey = {} as Record<IrClassKey, IrImportDescriptor>
206
+ const importedClassesByKey = {} as Record<IrClassKey, IrClass>
207
+
208
+ // The DSL computes the exact reachable footprint. Studio no longer recurses
209
+ // through JSON Schemas, policies, Views, and Core declarations to rediscover it.
210
+ for (const dependency of closure) {
211
+ const footprint = sdk.schema.compareDependencyMeaning(domain.source, dependency).footprint
212
+ for (const candidate of footprint) {
213
+ if (!sdk.ClassKey.is(candidate)) continue
214
+ const ref = sdk.ClassKey.ref(candidate)
215
+ const definition = domain.definition(ref)
216
+ if (!definition) continue
217
+ const key = candidate as IrClassKey
218
+ importsByKey[key] = { origin: ref.origin, ref, key }
219
+ importedClassesByKey[key] = projectClass(ref.origin, ref.name, definition.definition)
220
+ }
221
+ }
222
+ return { importsByKey, importedClassesByKey }
223
+ }
224
+
234
225
  function projectClass(origin: string, name: string, value: unknown): IrClass {
235
226
  const declaration = asRecord(value) ?? {}
236
227
  const kind = declaration.kind === 'edge' ? 'edge' : 'node'
237
228
  const extendsRefs = refsOf(declaration.extends)
238
229
  const propertyEntries = entriesOf(declaration.properties)
239
- const properties = Object.fromEntries(
240
- propertyEntries.map(([propertyName, raw]) => [
241
- propertyName,
242
- studioSchema(asRecord(raw)?.schema),
243
- ]),
244
- )
245
- const required = propertyEntries.flatMap(([propertyName, raw]) =>
246
- asRecord(raw)?.required === true ? [propertyName] : [],
247
- )
248
230
  const propertyMetadata = Object.fromEntries(
249
231
  propertyEntries.map(([propertyName, raw]) => {
250
232
  const member = { ...(asRecord(raw) ?? {}) }
@@ -265,52 +247,54 @@ function projectClass(origin: string, name: string, value: unknown): IrClass {
265
247
  ref: { origin, kind: 'class', name },
266
248
  extends: extendsRefs.map((ref) => ref.name),
267
249
  extendsRefs,
268
- properties,
269
- required,
270
- methods: projectMethods(declaration.methods),
250
+ properties: Object.fromEntries(
251
+ propertyEntries.map(([propertyName, raw]) => [
252
+ propertyName,
253
+ studioSchema(asRecord(raw)?.schema),
254
+ ]),
255
+ ),
256
+ required: propertyEntries.flatMap(([propertyName, raw]) =>
257
+ asRecord(raw)?.required === true ? [propertyName] : [],
258
+ ),
259
+ methods: Object.fromEntries(
260
+ entriesOf(declaration.methods).map(([methodName, raw]) => [
261
+ methodName,
262
+ projectMethod(methodName, raw),
263
+ ]),
264
+ ),
271
265
  ...(kind === 'edge' ? edgeFields(declaration) : {}),
266
+ ...(typeof declaration.icon === 'string' ? { icon: declaration.icon } : {}),
272
267
  ...(typeof declaration.description === 'string'
273
268
  ? { description: declaration.description }
274
269
  : {}),
270
+ // the SDK lets a Class carry its own glyph; without this the schema declared one
271
+ // and every surface still drew the generic box
272
+ ...(typeof declaration.icon === 'string' ? { icon: declaration.icon } : {}),
275
273
  ...(Object.keys(propertyMetadata).length === 0 ? {} : { propertyMetadata }),
276
274
  ...(dataDeclaration(declaration.data) ? { data: dataDeclaration(declaration.data) } : {}),
277
275
  ...(Object.keys(policies).length === 0 ? {} : { policies }),
278
276
  }
279
277
  }
280
278
 
281
- function projectMethods(value: unknown): Record<string, IrMethod> {
282
- return Object.fromEntries(
283
- entriesOf(value).map(([name, raw]) => {
284
- const declaration = asRecord(raw) ?? {}
285
- const inheritance =
286
- declaration.inheritance === 'abstract' || declaration.inheritance === 'sealed'
287
- ? declaration.inheritance
288
- : 'default'
289
- return [
290
- name,
291
- {
292
- ...projectCallable(name, declaration),
293
- static: declaration.static === true,
294
- inheritance,
295
- } satisfies IrMethod,
296
- ]
297
- }),
298
- )
279
+ function projectMethod(name: string, value: unknown): IrMethod {
280
+ const declaration = asRecord(value) ?? {}
281
+ return {
282
+ ...projectFunction(name, declaration),
283
+ static: declaration.static === true,
284
+ inheritance:
285
+ declaration.inheritance === 'abstract' || declaration.inheritance === 'sealed'
286
+ ? declaration.inheritance
287
+ : 'default',
288
+ }
299
289
  }
300
290
 
301
291
  function projectFunction(name: string, value: unknown): IrFunction {
302
- return projectCallable(name, value)
303
- }
304
-
305
- function projectCallable(name: string, value: unknown): IrFunction {
306
292
  const declaration = asRecord(value) ?? {}
307
- const input = studioSchema(declaration.input)
308
- const output = callableOutput(declaration.output)
309
293
  const auth = callableAuth(declaration.auth)
310
294
  return {
311
295
  name,
312
- input,
313
- output,
296
+ input: studioSchema(declaration.input),
297
+ output: callableOutput(declaration.output),
314
298
  ...(typeof declaration.description === 'string'
315
299
  ? { description: declaration.description }
316
300
  : {}),
@@ -363,99 +347,35 @@ function projectEndpoint(
363
347
  ): IrEndpoint {
364
348
  const endpoint = asRecord(value) ?? {}
365
349
  const refs = refsOf(endpoint.accepts)
366
- const count = edgeCount(endpoint[cardinality])
350
+ const count = endpoint[cardinality]
367
351
  return {
368
352
  name: typeof endpoint.role === 'string' ? endpoint.role : '',
369
353
  types: refs.map((ref) => ref.name),
370
354
  refs,
371
- ...(count === undefined ? {} : { cardinality: count }),
355
+ ...(count === '0..*' || count === '1..*' || count === '0..1' || count === '1'
356
+ ? { cardinality: edgeCount(count) }
357
+ : {}),
372
358
  }
373
359
  }
374
360
 
375
- function edgeCount(value: unknown): IrEndpoint['cardinality'] | undefined {
361
+ function edgeCount(value: '0..*' | '1..*' | '0..1' | '1'): IrEndpoint['cardinality'] {
376
362
  if (value === '0..*') return { min: 0, max: null }
377
363
  if (value === '1..*') return { min: 1, max: null }
378
364
  if (value === '0..1') return { min: 0, max: 1 }
379
- if (value === '1') return { min: 1, max: 1 }
380
- return undefined
365
+ return { min: 1, max: 1 }
381
366
  }
382
367
 
383
- function collectClassRefs(root: CanonicalDomainSchemaV1): IrClassRef[] {
384
- const refs: IrClassRef[] = []
385
- for (const [, declaration] of entriesOf(root.classes)) {
386
- refs.push(...collectClassRefsFromClass(declaration))
387
- }
388
- for (const [, callable] of entriesOf(root.functions)) collectCallableRefs(callable, refs)
389
- for (const [, view] of entriesOf(root.views)) {
390
- refs.push(...refsOf(asRecord(asRecord(view)?.target)?.definitions))
391
- }
392
- const core = asRecord(root.core)
393
- for (const [, node] of entriesOf(core?.nodes)) addClassRef(asRecord(node)?.class, refs)
394
- for (const edge of arrayOf(core?.edges)) {
395
- const declaration = asRecord(edge)
396
- addClassRef(declaration?.class, refs)
397
- }
398
- for (const [, policy] of entriesOf(root.policies)) collectPolicyRefs(policy, refs)
399
- return uniqueClassRefs(refs)
400
- }
401
-
402
- function collectClassRefsFromClass(value: unknown): IrClassRef[] {
403
- const declaration = asRecord(value) ?? {}
404
- const refs = [...refsOf(declaration.extends)]
405
- const endpoints = asRecord(declaration.endpoints)
406
- for (const endpoint of [
407
- endpoints?.source,
408
- endpoints?.target,
409
- ...arrayOf(declaration.endpoints),
410
- ]) {
411
- refs.push(...refsOf(asRecord(endpoint)?.accepts))
412
- }
413
- for (const [, property] of entriesOf(declaration.properties)) {
414
- collectPathSchemaRefs(asRecord(property)?.schema, refs)
415
- }
416
- for (const [, callable] of entriesOf(declaration.methods)) collectCallableRefs(callable, refs)
417
- for (const [, policy] of entriesOf(declaration.policies)) collectPolicyRefs(policy, refs)
418
- return uniqueClassRefs(refs)
419
- }
420
-
421
- function collectCallableRefs(value: unknown, refs: IrClassRef[]): void {
422
- const callable = asRecord(value)
423
- if (callable === null) return
424
- collectPathSchemaRefs(callable.input, refs)
425
- const output = asRecord(callable.output)
426
- collectPathSchemaRefs(output?.schema ?? output?.item, refs)
427
- collectPolicyRefs(callable.policy, refs)
428
- }
429
-
430
- function collectPathSchemaRefs(value: unknown, refs: IrClassRef[]): void {
431
- if (Array.isArray(value)) {
432
- for (const item of value) collectPathSchemaRefs(item, refs)
433
- return
434
- }
435
- const object = asRecord(value)
436
- if (object === null) return
437
- const annotation = asRecord(object['x-astrale-path'])
438
- refs.push(...refsOf(annotation?.accepts))
439
- for (const [key, child] of Object.entries(object)) {
440
- if (key === 'const' || key === 'enum' || key === 'default' || key === 'examples') continue
441
- collectPathSchemaRefs(child, refs)
442
- }
443
- }
444
-
445
- function collectPolicyRefs(value: unknown, refs: IrClassRef[]): void {
446
- if (Array.isArray(value)) {
447
- for (const item of value) collectPolicyRefs(item, refs)
448
- return
449
- }
450
- const object = asRecord(value)
451
- if (object === null) return
452
- addClassRef(object.class, refs)
453
- for (const child of Object.values(object)) collectPolicyRefs(child, refs)
368
+ function callableOutput(value: unknown): IrCallableOutput {
369
+ const output = asRecord(value)
370
+ if (output?.mode === 'stream') return { mode: 'stream', item: studioSchema(output.item) }
371
+ if (output?.mode === 'binary') return { mode: 'binary' }
372
+ return { mode: 'value', schema: studioSchema(output?.schema) }
454
373
  }
455
374
 
456
- function addClassRef(value: unknown, refs: IrClassRef[]): void {
457
- const ref = classRef(value)
458
- if (ref !== null) refs.push(ref)
375
+ function callableAuth(value: unknown): IrCallableAuth | undefined {
376
+ return value === 'anonymous' || value === 'authenticated' || value === 'authorized'
377
+ ? value
378
+ : undefined
459
379
  }
460
380
 
461
381
  function refsOf(value: unknown): IrClassRef[] {
@@ -466,7 +386,7 @@ function refsOf(value: unknown): IrClassRef[] {
466
386
 
467
387
  function classRef(value: unknown): IrClassRef | null {
468
388
  const ref = definitionRefOf(value)
469
- return ref !== null && isIrClassRef(ref) ? ref : null
389
+ return ref?.kind === 'class' ? { origin: ref.origin, kind: 'class', name: ref.name } : null
470
390
  }
471
391
 
472
392
  function definitionRefOf(value: unknown): IrSchemaRef | null {
@@ -484,29 +404,6 @@ function definitionRefOf(value: unknown): IrSchemaRef | null {
484
404
  return { origin: ref.origin, kind: ref.kind, name: ref.name }
485
405
  }
486
406
 
487
- function uniqueClassRefs(refs: readonly IrClassRef[]): IrClassRef[] {
488
- const seen = new Set<string>()
489
- return refs.filter((ref) => {
490
- const key = classRefKey(ref)
491
- if (seen.has(key)) return false
492
- seen.add(key)
493
- return true
494
- })
495
- }
496
-
497
- function callableOutput(value: unknown): IrCallableOutput {
498
- const output = asRecord(value)
499
- if (output?.mode === 'stream') return { mode: 'stream', item: studioSchema(output.item) }
500
- if (output?.mode === 'binary') return { mode: 'binary' }
501
- return { mode: 'value', schema: studioSchema(output?.schema) }
502
- }
503
-
504
- function callableAuth(value: unknown): IrCallableAuth | undefined {
505
- return value === 'anonymous' || value === 'authenticated' || value === 'authorized'
506
- ? value
507
- : undefined
508
- }
509
-
510
407
  function dataDeclaration(
511
408
  value: unknown,
512
409
  ): { mediaType: string; [key: string]: unknown } | undefined {
@@ -518,8 +415,8 @@ function dataDeclaration(
518
415
 
519
416
  function coreEndpoint(value: unknown, rootOrigin: string): string {
520
417
  const endpoint = asRecord(value)
521
- if (endpoint?.kind === 'domain' && typeof endpoint.origin === 'string') {
522
- return `/:${endpoint.origin}`
418
+ if (endpoint?.kind === 'domain') {
419
+ return `/:${typeof endpoint.origin === 'string' ? endpoint.origin : rootOrigin}`
523
420
  }
524
421
  const ref = definitionRefOf(value)
525
422
  return ref?.kind === 'core' ? corePath(ref) : `/:${rootOrigin}`
@@ -529,22 +426,6 @@ function corePath(ref: Pick<IrSchemaRef, 'origin' | 'name'>): string {
529
426
  return `/:${ref.origin}:core.${ref.name}`
530
427
  }
531
428
 
532
- function admitClosure(
533
- value: unknown,
534
- root: CanonicalDomainSchemaV1,
535
- ): CanonicalDomainSchemaV1[] | null {
536
- if (!Array.isArray(value)) return null
537
- const closure: CanonicalDomainSchemaV1[] = []
538
- const origins = new Set<string>()
539
- for (const candidate of value) {
540
- if (!isCanonicalDomainSchemaV1(candidate) || candidate.origin === root.origin) return null
541
- if (origins.has(candidate.origin)) return null
542
- origins.add(candidate.origin)
543
- closure.push(candidate)
544
- }
545
- return closure
546
- }
547
-
548
429
  function studioSchema(value: unknown): JsonSchema {
549
430
  if (value === true) return {}
550
431
  if (value === false) return { not: {} }
@@ -1,16 +1,13 @@
1
1
  /**
2
- * core.ts — assembles a domain's StudioCore: the genesis node/edge graph
3
- * extracted from canonical `schema.core`, gated
4
- * on installed deps. Never throws; a failed import or a domain with no core both
5
- * become a well-formed (empty) render state.
2
+ * core.ts — derives a domain's genesis graph from the canonical Schema already
3
+ * extracted for its Studio bundle. Schema code is imported exactly once.
6
4
  */
7
- import type { StudioCore } from '../../shared/types'
5
+ import type { StudioCore, StudioSchemaBundle } from '../../shared/types'
6
+ import type { DomainHandle } from '../domain'
8
7
 
9
- import { type DomainHandle, depsInstalled } from '../domain'
10
- import { readSettings } from '../state/settings'
11
- import { coreExtract } from './runtime'
8
+ import { isCanonicalDomainSchemaV1, projectCanonicalCore } from './canonical-schema'
12
9
 
13
- export async function buildCore(handle: DomainHandle): Promise<StudioCore> {
10
+ export function buildCore(handle: DomainHandle, bundle: StudioSchemaBundle | null): StudioCore {
14
11
  const at = new Date().toISOString()
15
12
  const empty = (error: StudioCore['error']): StudioCore => ({
16
13
  domain: handle.origin ?? handle.id,
@@ -20,25 +17,21 @@ export async function buildCore(handle: DomainHandle): Promise<StudioCore> {
20
17
  extractedAt: at,
21
18
  })
22
19
 
23
- if (!depsInstalled(handle.root)) {
20
+ if (!bundle?.depsInstalled) {
24
21
  return empty({
25
22
  message: 'dependencies not installed — run `pnpm install` in the domain for core extraction',
26
23
  })
27
24
  }
25
+ if (!isCanonicalDomainSchemaV1(bundle.schemaRoot)) {
26
+ return empty(bundle.error ?? { message: 'canonical schema unavailable for core extraction' })
27
+ }
28
28
 
29
- const r = await coreExtract(
30
- handle.schemaIndex,
31
- handle.root,
32
- readSettings(handle.root).introspectTimeoutMs,
33
- )
34
- // r.ok && r.core===null → the domain simply defines no core (not an error).
35
- if (!r.ok) return empty(r.error ?? { message: 'core extraction failed' })
36
- if (!r.core) return empty(null)
29
+ const core = projectCanonicalCore(bundle.schemaRoot)
37
30
 
38
31
  return {
39
- domain: r.core.domain,
40
- nodes: r.core.nodes,
41
- edges: r.core.edges,
32
+ domain: core.domain,
33
+ nodes: core.nodes,
34
+ edges: core.edges,
42
35
  error: null,
43
36
  extractedAt: at,
44
37
  }