@astrale-os/cli 0.8.1-alpha.7 → 1.0.0-beta.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 (64) hide show
  1. package/README.md +15 -3
  2. package/dist/astrale.js +86 -35
  3. package/package.json +5 -5
  4. package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
  5. package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
  6. package/src/commands/__tests__/install-direct.test.ts +3 -2
  7. package/src/commands/domain/install.ts +78 -15
  8. package/src/connection/__tests__/errors.test.ts +82 -6
  9. package/src/connection/command.ts +9 -1
  10. package/src/connection/errors.ts +19 -8
  11. package/src/connection/index.ts +1 -1
  12. package/src/connection/reasons.ts +26 -12
  13. package/src/program/__tests__/program.test.ts +1 -1
  14. package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
  15. package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
  16. package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
  17. package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
  18. package/studio/client/dist/index.html +2 -2
  19. package/studio/package.json +1 -1
  20. package/studio/server/agent/prompts/anchors.test.ts +74 -0
  21. package/studio/server/agent/prompts/anchors.ts +85 -15
  22. package/studio/server/agent/prompts/system.test.ts +12 -0
  23. package/studio/server/agent/prompts/system.ts +6 -6
  24. package/studio/server/api.ts +1 -5
  25. package/studio/server/cache.ts +5 -2
  26. package/studio/server/domain.test.ts +67 -0
  27. package/studio/server/domain.ts +29 -6
  28. package/studio/server/index.ts +1 -3
  29. package/studio/server/introspect/anatomy-extras.test.ts +104 -1
  30. package/studio/server/introspect/anatomy-extras.ts +340 -8
  31. package/studio/server/introspect/anatomy.test.ts +33 -0
  32. package/studio/server/introspect/anatomy.ts +22 -7
  33. package/studio/server/introspect/bundle.ts +7 -1
  34. package/studio/server/introspect/canonical-schema.test.ts +395 -0
  35. package/studio/server/introspect/canonical-schema.ts +751 -0
  36. package/studio/server/introspect/core-extractor.ts +30 -10
  37. package/studio/server/introspect/core.ts +4 -2
  38. package/studio/server/introspect/diff.test.ts +124 -0
  39. package/studio/server/introspect/diff.ts +252 -23
  40. package/studio/server/introspect/extractor.ts +46 -14
  41. package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
  42. package/studio/server/introspect/overlay-tsmorph.ts +381 -106
  43. package/studio/server/introspect/overlay.test.ts +72 -0
  44. package/studio/server/introspect/overlay.ts +16 -6
  45. package/studio/server/introspect/runtime.test.ts +217 -0
  46. package/studio/server/introspect/runtime.ts +18 -6
  47. package/studio/server/introspect/schema-refs.test.ts +100 -0
  48. package/studio/server/introspect/schema-refs.ts +23 -2
  49. package/studio/server/state/baseline.test.ts +51 -0
  50. package/studio/server/state/baseline.ts +31 -2
  51. package/studio/server/state/create.test.ts +37 -0
  52. package/studio/server/state/create.ts +21 -15
  53. package/studio/server/state/instance.test.ts +63 -0
  54. package/studio/server/state/instance.ts +65 -29
  55. package/studio/server/state/views.test.ts +209 -9
  56. package/studio/server/state/views.ts +176 -69
  57. package/studio/server/watch.test.ts +36 -0
  58. package/studio/server/watch.ts +24 -16
  59. package/studio/server/workspace-watch.ts +8 -3
  60. package/studio/shared/types.ts +164 -33
  61. package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
  62. package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
  63. package/studio/server/view-dev-server.test.ts +0 -111
  64. package/studio/server/view-dev-server.ts +0 -372
@@ -0,0 +1,751 @@
1
+ /**
2
+ * Pure projection of the canonical, portable DomainSchema V1 document into the
3
+ * Studio's backward-compatible render IR. This module deliberately imports no
4
+ * Kernel or SDK package: extractor islands must interpret the schema emitted by
5
+ * the domain's own SDK cohort, not a second copy installed beside Studio.
6
+ */
7
+ import type {
8
+ IrCallableAuth,
9
+ IrCallableOutput,
10
+ IrClass,
11
+ IrDefinitionKey,
12
+ IrDefinitionRef,
13
+ IrEndpoint,
14
+ IrFunction,
15
+ IrImportDescriptor,
16
+ IrInterface,
17
+ IrMethod,
18
+ IrSchemaKey,
19
+ IrSchemaRef,
20
+ IrView,
21
+ JsonSchema,
22
+ SchemaIR,
23
+ StudioCore,
24
+ } from '../../shared/types'
25
+
26
+ type AnyRecord = Record<string, unknown>
27
+
28
+ export type CanonicalDomainSchemaV1 = AnyRecord & {
29
+ readonly format: 'astrale.dsl'
30
+ readonly version: 'v1'
31
+ readonly origin: string
32
+ }
33
+
34
+ export interface CanonicalSchemaProjection {
35
+ ir: SchemaIR
36
+ importedInterfaces: Record<string, IrInterface>
37
+ }
38
+
39
+ export function isCanonicalDomainSchemaV1(value: unknown): value is CanonicalDomainSchemaV1 {
40
+ const candidate = asRecord(value)
41
+ return (
42
+ candidate?.format === 'astrale.dsl' &&
43
+ candidate.version === 'v1' &&
44
+ typeof candidate.origin === 'string' &&
45
+ candidate.origin.length > 0
46
+ )
47
+ }
48
+
49
+ /** Find a directly exported V1 root, preferring the conventional `schema`. */
50
+ export function findCanonicalDomainSchemaExport(
51
+ moduleExports: Record<string, unknown>,
52
+ ): CanonicalDomainSchemaV1 | null {
53
+ if (isCanonicalDomainSchemaV1(moduleExports.schema)) return moduleExports.schema
54
+ if (isCanonicalDomainSchemaV1(moduleExports.default)) return moduleExports.default
55
+
56
+ const candidates = Object.entries(moduleExports)
57
+ .filter((entry): entry is [string, CanonicalDomainSchemaV1] =>
58
+ isCanonicalDomainSchemaV1(entry[1]),
59
+ )
60
+ .sort(([left], [right]) => left.localeCompare(right))
61
+ return candidates[0]?.[1] ?? null
62
+ }
63
+
64
+ /**
65
+ * Read the exact retained dependency closure through the domain's own SDK.
66
+ * Calls are optional and fail closed: an unrecognised/substituted result is
67
+ * discarded rather than trusted as imported-schema evidence.
68
+ */
69
+ export function closureFromSdk(
70
+ sdkModule: Record<string, unknown>,
71
+ root: CanonicalDomainSchemaV1,
72
+ ): CanonicalDomainSchemaV1[] {
73
+ const bundleApi = asRecord(sdkModule.bundle)
74
+ const create = bundleApi?.create
75
+ if (typeof create === 'function') {
76
+ try {
77
+ const candidate = Reflect.apply(create, bundleApi, [root])
78
+ const bundle = asRecord(candidate)
79
+ if (bundle?.root === root) {
80
+ const closure = admitClosure(bundle.closure, root)
81
+ if (closure) return closure
82
+ }
83
+ } catch {
84
+ // A structurally V1-looking export may lack retained SDK admission context.
85
+ }
86
+ }
87
+
88
+ const schemaApi = asRecord(sdkModule.schema)
89
+ const resolve = schemaApi?.resolve
90
+ if (typeof resolve === 'function') {
91
+ try {
92
+ const candidate = asRecord(Reflect.apply(resolve, schemaApi, [root]))
93
+ const resolution = asRecord(candidate?.$)
94
+ if (resolution?.schema === root) {
95
+ const closure = admitClosure(resolution.closure, root)
96
+ if (closure) return closure
97
+ }
98
+ } catch {
99
+ // Missing retained context is a valid degradation: local schema still renders.
100
+ }
101
+ }
102
+
103
+ return []
104
+ }
105
+
106
+ /** Add fields required by the current shared contract to a legacy serializer IR. */
107
+ export function normalizeLegacySchemaIR(value: unknown): SchemaIR {
108
+ const ir = asRecord(value)
109
+ if (!ir) throw new TypeError('legacy schema IR must be an object')
110
+
111
+ const functions: Record<string, IrFunction> = {}
112
+ for (const [name, raw] of entriesOf(ir.functions)) {
113
+ const declaration = asRecord(raw) ?? {}
114
+ const params = schemaRecord(declaration.params)
115
+ const returns = studioSchema(declaration.returns)
116
+ const requiredParams =
117
+ declaration.input === undefined
118
+ ? legacyRequiredParams(params)
119
+ : requiredNames(declaration.input)
120
+ functions[name] = {
121
+ name,
122
+ input: studioSchema(declaration.input ?? objectInputFromParams(params, requiredParams)),
123
+ params,
124
+ requiredParams,
125
+ output: callableOutput(declaration.output, returns),
126
+ returns,
127
+ static: true,
128
+ inheritance: 'default',
129
+ ...(callableAuth(declaration.auth) ? { auth: callableAuth(declaration.auth) } : {}),
130
+ ...(typeof declaration.description === 'string'
131
+ ? { description: declaration.description }
132
+ : {}),
133
+ ...(declaration.policy === undefined ? {} : { policy: jsonCopy(declaration.policy) }),
134
+ }
135
+ }
136
+
137
+ return { ...(ir as unknown as SchemaIR), functions }
138
+ }
139
+
140
+ export function projectCanonicalSchema(
141
+ root: CanonicalDomainSchemaV1,
142
+ closure: readonly CanonicalDomainSchemaV1[] = [],
143
+ ): CanonicalSchemaProjection {
144
+ const origin = root.origin
145
+ const interfaces = Object.fromEntries(
146
+ entriesOf(root.interfaces).map(([name, declaration]) => [
147
+ name,
148
+ projectInterface(origin, name, declaration),
149
+ ]),
150
+ )
151
+ const classes = Object.fromEntries(
152
+ entriesOf(root.classes).map(([name, declaration]) => [
153
+ name,
154
+ projectClass(origin, name, declaration),
155
+ ]),
156
+ )
157
+ const functions = Object.fromEntries(
158
+ entriesOf(root.functions).map(([name, declaration]) => [
159
+ name,
160
+ projectFunction(name, declaration),
161
+ ]),
162
+ )
163
+ const views = Object.fromEntries(
164
+ entriesOf(root.views).map(([name, declaration]) => [name, projectView(name, declaration)]),
165
+ )
166
+
167
+ const importsByKey: Record<IrDefinitionKey, IrImportDescriptor> = {}
168
+ const importedInterfacesByKey: Record<IrDefinitionKey, IrInterface> = {}
169
+ const schemas = new Map(
170
+ closure.filter(isCanonicalDomainSchemaV1).map((schema) => [schema.origin, schema]),
171
+ )
172
+ const pending = collectDefinitionRefs(root)
173
+ const visited = new Set<string>()
174
+
175
+ for (let index = 0; index < pending.length; index++) {
176
+ const ref = pending[index]
177
+ if (!isDefinitionRef(ref) || ref.origin === origin) continue
178
+ const key = definitionKey(ref)
179
+ if (visited.has(key)) continue
180
+ visited.add(key)
181
+
182
+ importsByKey[key] = {
183
+ origin: ref.origin,
184
+ definition: ref.kind,
185
+ ref,
186
+ key,
187
+ }
188
+ if (ref.kind !== 'interface' || key in importedInterfacesByKey) continue
189
+
190
+ const owner = schemas.get(ref.origin)
191
+ const declaration = asRecord(asRecord(owner?.interfaces)?.[ref.name])
192
+ if (!owner || !declaration) continue
193
+ const projected = projectInterface(owner.origin, ref.name, declaration)
194
+ importedInterfacesByKey[key] = projected
195
+ pending.push(...collectDefinitionRefsFromDeclaration(declaration))
196
+ }
197
+
198
+ // Bare member names are retained solely as an unambiguous compatibility
199
+ // lookup. The canonical Key maps above remain complete when origins or kinds
200
+ // reuse a name, and prevent a traversal-order-dependent "first one wins".
201
+ const nameCounts = new Map<string, number>()
202
+ for (const descriptor of Object.values(importsByKey)) {
203
+ const name = descriptor.ref?.name
204
+ if (name) nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1)
205
+ }
206
+ const localNames = new Set([...Object.keys(interfaces), ...Object.keys(classes)])
207
+ const isUnambiguous = (name: string): boolean =>
208
+ nameCounts.get(name) === 1 && !localNames.has(name)
209
+ const imports = Object.fromEntries(
210
+ Object.values(importsByKey).flatMap((descriptor) => {
211
+ const name = descriptor.ref?.name
212
+ return name && isUnambiguous(name) ? [[name, descriptor]] : []
213
+ }),
214
+ )
215
+ const importedInterfaces = Object.fromEntries(
216
+ Object.values(importedInterfacesByKey).flatMap((declaration) =>
217
+ isUnambiguous(declaration.name) ? [[declaration.name, declaration]] : [],
218
+ ),
219
+ )
220
+
221
+ const dependencies = arrayOf(root.dependencies)
222
+ .map(asRecord)
223
+ .filter((dependency): dependency is AnyRecord => dependency !== null)
224
+ .flatMap((dependency) =>
225
+ typeof dependency.origin === 'string' && typeof dependency.revision === 'string'
226
+ ? [{ origin: dependency.origin, revision: dependency.revision }]
227
+ : [],
228
+ )
229
+
230
+ return {
231
+ ir: {
232
+ format: 'astrale.dsl',
233
+ version: 'v1',
234
+ domain: origin,
235
+ types: schemaRecord(root.types),
236
+ interfaces,
237
+ classes,
238
+ imports,
239
+ importsByKey,
240
+ importedInterfacesByKey,
241
+ functions,
242
+ views,
243
+ policies: recordCopy(root.policies),
244
+ dependencies,
245
+ core: jsonCopy(root.core),
246
+ },
247
+ importedInterfaces,
248
+ }
249
+ }
250
+
251
+ /** Project canonical genesis data without importing the effectful Domain entry. */
252
+ export function projectCanonicalCore(
253
+ root: CanonicalDomainSchemaV1,
254
+ ): Pick<StudioCore, 'domain' | 'nodes' | 'edges'> {
255
+ const core = asRecord(root.core) ?? {}
256
+ const nodes = entriesOf(core.nodes).map(([slug, raw]) => {
257
+ const node = asRecord(raw) ?? {}
258
+ const classRef = schemaRef(node.class)
259
+ return {
260
+ path: corePath({ origin: root.origin, kind: 'core', name: slug }),
261
+ className: classRef?.name ?? '?',
262
+ data: recordCopy(node.properties),
263
+ }
264
+ })
265
+ const edges = arrayOf(core.edges).map((raw) => {
266
+ const edge = asRecord(raw) ?? {}
267
+ const classRef = schemaRef(edge.class)
268
+ return {
269
+ from: coreEndpoint(edge.source, root.origin),
270
+ to: coreEndpoint(edge.target, root.origin),
271
+ edgeName: classRef?.name ?? '?',
272
+ ...(edge.properties === undefined ? {} : { data: recordCopy(edge.properties) }),
273
+ }
274
+ })
275
+ return { domain: root.origin, nodes, edges }
276
+ }
277
+
278
+ function projectInterface(origin: string, name: string, value: unknown): IrInterface {
279
+ const declaration = asRecord(value) ?? {}
280
+ const family = definitionFamily(declaration.family, 'both')
281
+ const extendsRefs = refsOf(declaration.extends, 'interface')
282
+ return {
283
+ type: 'interface',
284
+ name,
285
+ origin,
286
+ family,
287
+ ref: { origin, kind: 'interface', name },
288
+ extends: extendsRefs.map((ref) => ref.name),
289
+ extendsRefs,
290
+ properties: propertySchemas(declaration),
291
+ required: requiredNames(declaration.properties),
292
+ methods: projectMethods(declaration.methods),
293
+ ...(family === 'edge' ? edgeFields(declaration) : {}),
294
+ ...(typeof declaration.description === 'string'
295
+ ? { description: declaration.description }
296
+ : {}),
297
+ ...(asRecord(declaration.propertyMetadata)
298
+ ? { propertyMetadata: recordCopy(declaration.propertyMetadata) }
299
+ : {}),
300
+ ...(dataDeclaration(declaration.data) ? { data: dataDeclaration(declaration.data) } : {}),
301
+ }
302
+ }
303
+
304
+ function projectClass(origin: string, name: string, value: unknown): IrClass {
305
+ const declaration = asRecord(value) ?? {}
306
+ const family = definitionFamily(declaration.family, 'node') === 'edge' ? 'edge' : 'node'
307
+ const implementsRefs = refsOf(declaration.implements, 'interface')
308
+ const policies = Object.fromEntries(
309
+ entriesOf(declaration.policies).flatMap(([policyName, raw]) => {
310
+ const ref = schemaRef(raw)
311
+ return ref ? [[policyName, ref]] : []
312
+ }),
313
+ )
314
+ return {
315
+ type: family,
316
+ name,
317
+ origin,
318
+ ref: { origin, kind: 'class', name },
319
+ implements: implementsRefs.map((ref) => ref.name),
320
+ implementsRefs,
321
+ properties: propertySchemas(declaration),
322
+ required: requiredNames(declaration.properties),
323
+ methods: projectMethods(declaration.methods),
324
+ ...(family === 'edge' ? edgeFields(declaration) : {}),
325
+ ...(typeof declaration.icon === 'string' ? { icon: declaration.icon } : {}),
326
+ ...(typeof declaration.description === 'string'
327
+ ? { description: declaration.description }
328
+ : {}),
329
+ ...(asRecord(declaration.propertyMetadata)
330
+ ? { propertyMetadata: recordCopy(declaration.propertyMetadata) }
331
+ : {}),
332
+ ...(dataDeclaration(declaration.data) ? { data: dataDeclaration(declaration.data) } : {}),
333
+ ...(Object.keys(policies).length > 0 ? { policies } : {}),
334
+ }
335
+ }
336
+
337
+ function projectMethods(value: unknown): Record<string, IrMethod> {
338
+ return Object.fromEntries(
339
+ entriesOf(value).map(([name, declaration]) => {
340
+ const callable = projectCallable(name, declaration)
341
+ const raw = asRecord(declaration) ?? {}
342
+ const inheritance =
343
+ raw.inheritance === 'abstract' ||
344
+ raw.inheritance === 'sealed' ||
345
+ raw.inheritance === 'default'
346
+ ? raw.inheritance
347
+ : 'default'
348
+ return [
349
+ name,
350
+ {
351
+ ...callable,
352
+ static: raw.static === true,
353
+ inheritance,
354
+ } satisfies IrMethod,
355
+ ]
356
+ }),
357
+ )
358
+ }
359
+
360
+ function projectFunction(name: string, declaration: unknown): IrFunction {
361
+ return {
362
+ ...projectCallable(name, declaration),
363
+ static: true,
364
+ inheritance: 'default',
365
+ }
366
+ }
367
+
368
+ function projectCallable(name: string, value: unknown): Omit<IrFunction, 'static' | 'inheritance'> {
369
+ const declaration = asRecord(value) ?? {}
370
+ const input = studioSchema(declaration.input)
371
+ const output = callableOutput(declaration.output, {})
372
+ const auth = callableAuth(declaration.auth)
373
+ return {
374
+ name,
375
+ input,
376
+ params: callableParams(declaration.input),
377
+ requiredParams: requiredNames(declaration.input),
378
+ output,
379
+ returns: outputValue(output),
380
+ ...(typeof declaration.description === 'string'
381
+ ? { description: declaration.description }
382
+ : {}),
383
+ ...(auth ? { auth } : {}),
384
+ ...(declaration.policy === undefined ? {} : { policy: jsonCopy(declaration.policy) }),
385
+ }
386
+ }
387
+
388
+ function projectView(name: string, value: unknown): IrView {
389
+ const declaration = asRecord(value) ?? {}
390
+ const rawTarget = asRecord(declaration.target)
391
+ const target =
392
+ rawTarget?.kind === 'definition'
393
+ ? ({
394
+ kind: 'definition',
395
+ definitions: refsOf(rawTarget.definitions, ['class', 'interface']),
396
+ } as const)
397
+ : ({ kind: 'domain' } as const)
398
+ const auth =
399
+ declaration.auth === 'optional' || declaration.auth === 'public' ? declaration.auth : 'required'
400
+ return {
401
+ name,
402
+ target,
403
+ auth,
404
+ ...(typeof declaration.description === 'string'
405
+ ? { description: declaration.description }
406
+ : {}),
407
+ }
408
+ }
409
+
410
+ function edgeFields(
411
+ declaration: AnyRecord,
412
+ ): Pick<IrClass, 'endpoints' | 'orientation' | 'constraints'> {
413
+ const orientation = declaration.orientation === 'undirected' ? 'undirected' : 'directed'
414
+ const endpoints = asRecord(declaration.endpoints)
415
+ const projected =
416
+ orientation === 'undirected'
417
+ ? arrayOf(declaration.endpoints).map((endpoint) => projectEndpoint(endpoint, 'incident'))
418
+ : [
419
+ projectEndpoint(endpoints?.source, 'outgoing'),
420
+ projectEndpoint(endpoints?.target, 'incoming'),
421
+ ]
422
+ const rawConstraints = asRecord(declaration.constraints)
423
+ const constraints = {
424
+ ...(rawConstraints?.noSelf === true ? { noSelf: true as const } : {}),
425
+ ...(rawConstraints?.acyclic === true ? { acyclic: true as const } : {}),
426
+ }
427
+ return { endpoints: projected, orientation, constraints }
428
+ }
429
+
430
+ function projectEndpoint(
431
+ value: unknown,
432
+ countKey: 'outgoing' | 'incoming' | 'incident',
433
+ ): IrEndpoint {
434
+ const endpoint = asRecord(value) ?? {}
435
+ const refs = refsOf(endpoint.accepts, ['class', 'interface'])
436
+ const cardinality = edgeCount(endpoint[countKey])
437
+ return {
438
+ name: typeof endpoint.role === 'string' ? endpoint.role : '',
439
+ types: refs.map((ref) => ref.name),
440
+ refs,
441
+ ...(cardinality ? { cardinality } : {}),
442
+ }
443
+ }
444
+
445
+ function edgeCount(value: unknown): IrEndpoint['cardinality'] | undefined {
446
+ switch (value) {
447
+ case '0..*':
448
+ return { min: 0, max: null }
449
+ case '1..*':
450
+ return { min: 1, max: null }
451
+ case '0..1':
452
+ return { min: 0, max: 1 }
453
+ case '1':
454
+ return { min: 1, max: 1 }
455
+ default:
456
+ return undefined
457
+ }
458
+ }
459
+
460
+ function propertySchemas(declaration: AnyRecord): Record<string, JsonSchema> {
461
+ const object = asRecord(declaration.properties) ?? {}
462
+ return Object.fromEntries(
463
+ entriesOf(object.properties).map(([name, schema]) => [name, studioSchema(schema)]),
464
+ )
465
+ }
466
+
467
+ function callableParams(input: unknown): Record<string, JsonSchema> {
468
+ const object = asRecord(input)
469
+ if (!object) return {}
470
+ return Object.fromEntries(
471
+ entriesOf(object.properties).map(([name, schema]) => [name, studioSchema(schema)]),
472
+ )
473
+ }
474
+
475
+ function callableOutput(value: unknown, fallback: JsonSchema): IrCallableOutput {
476
+ const output = asRecord(value)
477
+ if (output?.mode === 'stream') return { mode: 'stream', item: studioSchema(output.item) }
478
+ if (output?.mode === 'binary') return { mode: 'binary' }
479
+ if (output?.mode === 'value') return { mode: 'value', schema: studioSchema(output.schema) }
480
+ return { mode: 'value', schema: fallback }
481
+ }
482
+
483
+ function outputValue(output: IrCallableOutput): JsonSchema {
484
+ if (output.mode === 'value') return output.schema
485
+ if (output.mode === 'stream') return output.item
486
+ return {}
487
+ }
488
+
489
+ function callableAuth(value: unknown): IrCallableAuth | undefined {
490
+ return value === 'anonymous' || value === 'authenticated' || value === 'authorized'
491
+ ? value
492
+ : undefined
493
+ }
494
+
495
+ function definitionFamily(value: unknown, fallback: 'node' | 'both'): 'node' | 'edge' | 'both' {
496
+ return value === 'node' || value === 'edge' || value === 'both' ? value : fallback
497
+ }
498
+
499
+ function dataDeclaration(
500
+ value: unknown,
501
+ ): { mediaType: string; [key: string]: unknown } | undefined {
502
+ const data = asRecord(value)
503
+ return typeof data?.mediaType === 'string'
504
+ ? (jsonCopy(data) as { mediaType: string; [key: string]: unknown })
505
+ : undefined
506
+ }
507
+
508
+ function refsOf(
509
+ value: unknown,
510
+ kinds: IrSchemaRef['kind'] | readonly IrSchemaRef['kind'][],
511
+ ): IrSchemaRef[] {
512
+ const accepted = new Set(Array.isArray(kinds) ? kinds : [kinds])
513
+ return arrayOf(value)
514
+ .map(schemaRef)
515
+ .filter((ref): ref is IrSchemaRef => ref !== null && accepted.has(ref.kind))
516
+ }
517
+
518
+ function schemaRef(value: unknown): IrSchemaRef | null {
519
+ const ref = asRecord(value)
520
+ if (!ref || typeof ref.origin !== 'string' || typeof ref.name !== 'string') return null
521
+ switch (ref.kind) {
522
+ case 'type':
523
+ case 'interface':
524
+ case 'class':
525
+ case 'function':
526
+ case 'policy':
527
+ case 'view':
528
+ case 'core':
529
+ return { origin: ref.origin, kind: ref.kind, name: ref.name }
530
+ default:
531
+ return null
532
+ }
533
+ }
534
+
535
+ function collectDefinitionRefs(root: CanonicalDomainSchemaV1): IrSchemaRef[] {
536
+ const refs: IrSchemaRef[] = []
537
+ for (const [, declaration] of entriesOf(root.types)) {
538
+ collectPathSchemaRefs(declaration, refs)
539
+ }
540
+ for (const [, declaration] of entriesOf(root.interfaces)) {
541
+ refs.push(...collectDefinitionRefsFromDeclaration(declaration))
542
+ }
543
+ for (const [, declaration] of entriesOf(root.classes)) {
544
+ refs.push(...collectDefinitionRefsFromDeclaration(declaration))
545
+ }
546
+ for (const [, callable] of entriesOf(root.functions)) collectCallableRefs(callable, refs)
547
+ for (const [, view] of entriesOf(root.views)) {
548
+ const target = asRecord(asRecord(view)?.target)
549
+ refs.push(...refsOf(target?.definitions, ['class', 'interface']))
550
+ }
551
+ const core = asRecord(root.core)
552
+ for (const [, node] of entriesOf(core?.nodes)) addRef(asRecord(node)?.class, refs)
553
+ for (const edge of arrayOf(core?.edges)) {
554
+ const declaration = asRecord(edge)
555
+ addRef(declaration?.class, refs)
556
+ addRef(declaration?.source, refs)
557
+ addRef(declaration?.target, refs)
558
+ }
559
+ collectPolicyRefs(root.policies, refs)
560
+ return uniqueRefs(refs)
561
+ }
562
+
563
+ function collectDefinitionRefsFromDeclaration(value: unknown): IrSchemaRef[] {
564
+ const declaration = asRecord(value) ?? {}
565
+ const refs = [
566
+ ...refsOf(declaration.extends, 'interface'),
567
+ ...refsOf(declaration.implements, 'interface'),
568
+ ]
569
+ collectPathSchemaRefs(declaration.properties, refs)
570
+ const endpoints = asRecord(declaration.endpoints)
571
+ for (const endpoint of [
572
+ endpoints?.source,
573
+ endpoints?.target,
574
+ ...arrayOf(declaration.endpoints),
575
+ ]) {
576
+ refs.push(...refsOf(asRecord(endpoint)?.accepts, ['class', 'interface']))
577
+ }
578
+ for (const [, callable] of entriesOf(declaration.methods)) collectCallableRefs(callable, refs)
579
+ for (const [, policy] of entriesOf(declaration.policies)) addRef(policy, refs)
580
+ return uniqueRefs(refs)
581
+ }
582
+
583
+ function collectCallableRefs(value: unknown, refs: IrSchemaRef[]): void {
584
+ const callable = asRecord(value)
585
+ if (!callable) return
586
+ collectPathSchemaRefs(callable.input, refs)
587
+ const output = asRecord(callable.output)
588
+ collectPathSchemaRefs(output?.schema ?? output?.item, refs)
589
+ collectPolicyRefs(callable.policy, refs)
590
+ }
591
+
592
+ /** Embedded value data is opaque; only the declared x-astrale-path vocabulary carries refs. */
593
+ function collectPathSchemaRefs(value: unknown, refs: IrSchemaRef[]): void {
594
+ if (Array.isArray(value)) {
595
+ for (const item of value) collectPathSchemaRefs(item, refs)
596
+ return
597
+ }
598
+ const object = asRecord(value)
599
+ if (!object) return
600
+ const annotation = asRecord(object['x-astrale-path'])
601
+ if (annotation) refs.push(...refsOf(annotation.accepts, ['class', 'interface']))
602
+ for (const [key, child] of Object.entries(object)) {
603
+ if (key === 'const' || key === 'enum' || key === 'default' || key === 'examples') continue
604
+ collectPathSchemaRefs(child, refs)
605
+ }
606
+ }
607
+
608
+ function collectPolicyRefs(value: unknown, refs: IrSchemaRef[]): void {
609
+ if (Array.isArray(value)) {
610
+ for (const item of value) collectPolicyRefs(item, refs)
611
+ return
612
+ }
613
+ const object = asRecord(value)
614
+ if (!object) return
615
+ for (const key of ['check', 'class', 'ref']) addRef(object[key], refs)
616
+ for (const key of [
617
+ 'allOf',
618
+ 'anyOf',
619
+ 'exists',
620
+ 'where',
621
+ 'match',
622
+ 'nodes',
623
+ 'source',
624
+ 'target',
625
+ 'object',
626
+ ]) {
627
+ collectPolicyRefs(object[key], refs)
628
+ }
629
+ }
630
+
631
+ function addRef(value: unknown, refs: IrSchemaRef[]): void {
632
+ const ref = schemaRef(value)
633
+ if (ref) refs.push(ref)
634
+ }
635
+
636
+ function uniqueRefs(refs: readonly IrSchemaRef[]): IrSchemaRef[] {
637
+ const seen = new Set<string>()
638
+ return refs.filter((ref) => {
639
+ const key = refKey(ref)
640
+ if (seen.has(key)) return false
641
+ seen.add(key)
642
+ return true
643
+ })
644
+ }
645
+
646
+ function refKey(ref: IrSchemaRef): IrSchemaKey {
647
+ return `${ref.origin}:${ref.kind}.${ref.name}`
648
+ }
649
+
650
+ function isDefinitionRef(ref: IrSchemaRef): ref is IrDefinitionRef {
651
+ return ref.kind === 'class' || ref.kind === 'interface'
652
+ }
653
+
654
+ function definitionKey(ref: IrDefinitionRef): IrDefinitionKey {
655
+ return `${ref.origin}:${ref.kind}.${ref.name}`
656
+ }
657
+
658
+ function coreEndpoint(value: unknown, rootOrigin: string): string {
659
+ const endpoint = asRecord(value)
660
+ if (endpoint?.kind === 'domain') return `/:${rootOrigin}`
661
+ const ref = schemaRef(value)
662
+ return ref?.kind === 'core' ? corePath(ref) : `/:${rootOrigin}`
663
+ }
664
+
665
+ function corePath(ref: Pick<IrSchemaRef, 'origin' | 'kind' | 'name'>): string {
666
+ return `/:${ref.origin}:core.${ref.name}`
667
+ }
668
+
669
+ function admitClosure(
670
+ value: unknown,
671
+ root: CanonicalDomainSchemaV1,
672
+ ): CanonicalDomainSchemaV1[] | null {
673
+ if (!Array.isArray(value)) return null
674
+ const closure: CanonicalDomainSchemaV1[] = []
675
+ const origins = new Set<string>()
676
+ for (const schema of value) {
677
+ if (!isCanonicalDomainSchemaV1(schema) || schema.origin === root.origin) return null
678
+ if (origins.has(schema.origin)) return null
679
+ origins.add(schema.origin)
680
+ closure.push(schema)
681
+ }
682
+ return closure
683
+ }
684
+
685
+ function studioSchema(value: unknown): JsonSchema {
686
+ if (value === true) return {}
687
+ if (value === false) return { not: {} }
688
+ return (jsonCopy(asRecord(value) ?? {}) ?? {}) as JsonSchema
689
+ }
690
+
691
+ function schemaRecord(value: unknown): Record<string, JsonSchema> {
692
+ return Object.fromEntries(entriesOf(value).map(([name, schema]) => [name, studioSchema(schema)]))
693
+ }
694
+
695
+ function requiredNames(value: unknown): string[] {
696
+ const object = asRecord(value)
697
+ return arrayOf(object?.required).filter((name): name is string => typeof name === 'string')
698
+ }
699
+
700
+ function legacyRequiredParams(params: Record<string, JsonSchema>): string[] {
701
+ return Object.entries(params).flatMap(([name, schema]) =>
702
+ schemaAllowsNull(schema) ? [] : [name],
703
+ )
704
+ }
705
+
706
+ function schemaAllowsNull(schema: JsonSchema): boolean {
707
+ if (schema.type === 'null') return true
708
+ if (Array.isArray(schema.type) && schema.type.includes('null')) return true
709
+ return [...arrayOf(schema.anyOf), ...arrayOf(schema.oneOf)].some(
710
+ (candidate) => asRecord(candidate)?.type === 'null',
711
+ )
712
+ }
713
+
714
+ function objectInputFromParams(
715
+ params: Record<string, JsonSchema>,
716
+ required: readonly string[],
717
+ ): JsonSchema {
718
+ return {
719
+ type: 'object',
720
+ properties: params,
721
+ required: [...required],
722
+ additionalProperties: false,
723
+ }
724
+ }
725
+
726
+ function recordCopy(value: unknown): Record<string, any> {
727
+ const record = asRecord(value)
728
+ return record ? (jsonCopy(record) as Record<string, any>) : {}
729
+ }
730
+
731
+ function jsonCopy(value: unknown): any {
732
+ if (Array.isArray(value)) return value.map(jsonCopy)
733
+ const record = asRecord(value)
734
+ if (record)
735
+ return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, jsonCopy(item)]))
736
+ return value
737
+ }
738
+
739
+ function entriesOf(value: unknown): [string, unknown][] {
740
+ return Object.entries(asRecord(value) ?? {})
741
+ }
742
+
743
+ function arrayOf(value: unknown): unknown[] {
744
+ return Array.isArray(value) ? value : []
745
+ }
746
+
747
+ function asRecord(value: unknown): AnyRecord | null {
748
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
749
+ ? (value as AnyRecord)
750
+ : null
751
+ }