@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.
- package/README.md +15 -3
- package/dist/astrale.js +86 -35
- package/package.json +5 -5
- package/src/commands/__tests__/domain-install-operation.test.ts +121 -0
- package/src/commands/__tests__/domain-install-owned.test.ts +66 -0
- package/src/commands/__tests__/install-direct.test.ts +3 -2
- package/src/commands/domain/install.ts +78 -15
- package/src/connection/__tests__/errors.test.ts +82 -6
- package/src/connection/command.ts +9 -1
- package/src/connection/errors.ts +19 -8
- package/src/connection/index.ts +1 -1
- package/src/connection/reasons.ts +26 -12
- package/src/program/__tests__/program.test.ts +1 -1
- package/studio/client/dist/assets/{elk-api-D0cBetPW.js → elk-api-D2xgMJvi.js} +1 -1
- package/studio/client/dist/assets/{index-BQnJ5sgd.css → index-BMdnsIJA.css} +1 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +8 -0
- package/studio/client/dist/assets/index-LGSWRrk8.js +81 -0
- package/studio/client/dist/index.html +2 -2
- package/studio/package.json +1 -1
- package/studio/server/agent/prompts/anchors.test.ts +74 -0
- package/studio/server/agent/prompts/anchors.ts +85 -15
- package/studio/server/agent/prompts/system.test.ts +12 -0
- package/studio/server/agent/prompts/system.ts +6 -6
- package/studio/server/api.ts +1 -5
- package/studio/server/cache.ts +5 -2
- package/studio/server/domain.test.ts +67 -0
- package/studio/server/domain.ts +29 -6
- package/studio/server/index.ts +1 -3
- package/studio/server/introspect/anatomy-extras.test.ts +104 -1
- package/studio/server/introspect/anatomy-extras.ts +340 -8
- package/studio/server/introspect/anatomy.test.ts +33 -0
- package/studio/server/introspect/anatomy.ts +22 -7
- package/studio/server/introspect/bundle.ts +7 -1
- package/studio/server/introspect/canonical-schema.test.ts +395 -0
- package/studio/server/introspect/canonical-schema.ts +751 -0
- package/studio/server/introspect/core-extractor.ts +30 -10
- package/studio/server/introspect/core.ts +4 -2
- package/studio/server/introspect/diff.test.ts +124 -0
- package/studio/server/introspect/diff.ts +252 -23
- package/studio/server/introspect/extractor.ts +46 -14
- package/studio/server/introspect/overlay-tsmorph.test.ts +164 -1
- package/studio/server/introspect/overlay-tsmorph.ts +381 -106
- package/studio/server/introspect/overlay.test.ts +72 -0
- package/studio/server/introspect/overlay.ts +16 -6
- package/studio/server/introspect/runtime.test.ts +217 -0
- package/studio/server/introspect/runtime.ts +18 -6
- package/studio/server/introspect/schema-refs.test.ts +100 -0
- package/studio/server/introspect/schema-refs.ts +23 -2
- package/studio/server/state/baseline.test.ts +51 -0
- package/studio/server/state/baseline.ts +31 -2
- package/studio/server/state/create.test.ts +37 -0
- package/studio/server/state/create.ts +21 -15
- package/studio/server/state/instance.test.ts +63 -0
- package/studio/server/state/instance.ts +65 -29
- package/studio/server/state/views.test.ts +209 -9
- package/studio/server/state/views.ts +176 -69
- package/studio/server/watch.test.ts +36 -0
- package/studio/server/watch.ts +24 -16
- package/studio/server/workspace-watch.ts +8 -3
- package/studio/shared/types.ts +164 -33
- package/studio/client/dist/assets/index-Dspir4w7.js +0 -81
- package/studio/client/dist/assets/index-bVD2KJgz.js +0 -8
- package/studio/server/view-dev-server.test.ts +0 -111
- 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
|
+
}
|