@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
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* core-extractor.ts — the Bun-executed island for a domain's CORE (genesis) data.
|
|
3
3
|
* Spawned as a short-lived subprocess by runtime.ts (cwd = domain dir, so the
|
|
4
|
-
* domain's own node_modules resolve @astrale-os/*). It imports the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* domain's own node_modules resolve @astrale-os/*). It imports the pure schema
|
|
5
|
+
* entry and reads canonical V1 `schema.core` directly. Importing the composition
|
|
6
|
+
* entry and discovering legacy `defineCore(...)` output is retained only as a
|
|
7
|
+
* fallback.
|
|
7
8
|
*
|
|
8
|
-
* bun core-extractor.ts <domainFile> <domainDir>
|
|
9
|
+
* bun core-extractor.ts <schemaIndexPath> <domainFile> <domainDir>
|
|
9
10
|
*
|
|
10
11
|
* Contract mirrors extractor.ts exactly: NEVER crash. A thrown error prints
|
|
11
12
|
* { ok:false } and exits 0 — the driver treats it as a render state. A domain
|
|
@@ -24,11 +25,13 @@
|
|
|
24
25
|
export {} // module marker — see header note
|
|
25
26
|
import { isAbsolute, resolve } from 'node:path'
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
28
|
+
import { findCanonicalDomainSchemaExport, projectCanonicalCore } from './canonical-schema'
|
|
29
|
+
|
|
30
|
+
const domainDir = process.argv[4] ?? process.cwd()
|
|
31
|
+
const resolveInput = (file: string | undefined): string | undefined =>
|
|
32
|
+
file && !isAbsolute(file) ? resolve(domainDir, file) : file
|
|
33
|
+
const schemaFile = resolveInput(process.argv[2])
|
|
34
|
+
const domainFile = resolveInput(process.argv[3])
|
|
32
35
|
|
|
33
36
|
type AnyRec = Record<string, any>
|
|
34
37
|
|
|
@@ -72,7 +75,24 @@ function classNameMap(schema: AnyRec): Map<any, string> {
|
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
async function main() {
|
|
75
|
-
if (!
|
|
78
|
+
if (!schemaFile) throw new Error('core-extractor: missing <schemaIndexPath>')
|
|
79
|
+
|
|
80
|
+
// Current SDK roots embed genesis data in the canonical, portable schema.
|
|
81
|
+
// Do not import implementation.ts/domain.ts when that source of truth exists.
|
|
82
|
+
try {
|
|
83
|
+
const schemaModule: AnyRec = await import(schemaFile)
|
|
84
|
+
const canonical = findCanonicalDomainSchemaExport(schemaModule)
|
|
85
|
+
if (canonical) {
|
|
86
|
+
const core = projectCanonicalCore(canonical)
|
|
87
|
+
const empty = core.nodes.length === 0 && core.edges.length === 0
|
|
88
|
+
process.stdout.write(JSON.stringify({ ok: true, core: empty ? null : core }))
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
// A legacy composition entry may still expose a valid defineCore result.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!domainFile) throw new Error('core-extractor: missing <domainFile> legacy fallback')
|
|
76
96
|
const mod: AnyRec = await import(domainFile)
|
|
77
97
|
const core = findCore(mod)
|
|
78
98
|
if (!core) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* core.ts — assembles a domain's StudioCore: the genesis node/edge graph
|
|
3
|
-
* extracted from `
|
|
4
|
-
* import or a domain with no core both
|
|
3
|
+
* extracted from canonical `schema.core` (or legacy `defineCore` fallback), 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.
|
|
5
6
|
*/
|
|
6
7
|
import type { StudioCore } from '../../shared/types'
|
|
7
8
|
|
|
@@ -26,6 +27,7 @@ export async function buildCore(handle: DomainHandle): Promise<StudioCore> {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
const r = await coreExtract(
|
|
30
|
+
handle.schemaIndex,
|
|
29
31
|
handle.domainFile,
|
|
30
32
|
handle.root,
|
|
31
33
|
readSettings(handle.root).introspectTimeoutMs,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
|
|
3
|
+
import type { IrClass, IrFunction, SchemaIR } from '../../shared/types'
|
|
4
|
+
|
|
5
|
+
import { classify, diffSchemas } from './diff'
|
|
6
|
+
|
|
7
|
+
const callable = (returnsType = 'string'): IrFunction => ({
|
|
8
|
+
name: 'inspect',
|
|
9
|
+
input: { type: 'object', properties: {}, additionalProperties: false },
|
|
10
|
+
params: {},
|
|
11
|
+
output: { mode: 'value', schema: { type: returnsType } },
|
|
12
|
+
returns: { type: returnsType },
|
|
13
|
+
static: true,
|
|
14
|
+
inheritance: 'default',
|
|
15
|
+
auth: 'authenticated',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const schema = (functions: SchemaIR['functions']): SchemaIR => ({
|
|
19
|
+
version: 'v1',
|
|
20
|
+
domain: 'example.test',
|
|
21
|
+
types: {},
|
|
22
|
+
interfaces: {},
|
|
23
|
+
classes: {},
|
|
24
|
+
imports: {},
|
|
25
|
+
functions,
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const withClass = (member: IrClass): SchemaIR => ({
|
|
29
|
+
...schema({}),
|
|
30
|
+
classes: { [member.name]: member },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('canonical standalone Function diffs', () => {
|
|
34
|
+
test('classifies addition as additive and removal as breaking', () => {
|
|
35
|
+
const empty = schema({})
|
|
36
|
+
const populated = schema({ inspect: callable() })
|
|
37
|
+
expect(diffSchemas(empty, populated)).toEqual([
|
|
38
|
+
{ kind: 'function-added', target: 'inspect', breaking: false },
|
|
39
|
+
])
|
|
40
|
+
const removed = diffSchemas(populated, empty)
|
|
41
|
+
expect(removed).toEqual([{ kind: 'function-removed', target: 'inspect', breaking: true }])
|
|
42
|
+
expect(classify(removed)).toBe('breaking')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('detects a standalone Function signature change', () => {
|
|
46
|
+
expect(
|
|
47
|
+
diffSchemas(schema({ inspect: callable() }), schema({ inspect: callable('number') })),
|
|
48
|
+
).toEqual([{ kind: 'function-signature-changed', target: 'inspect', breaking: true }])
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('detects auth and output-mode changes in callable contracts', () => {
|
|
52
|
+
const before = callable()
|
|
53
|
+
const after: IrFunction = {
|
|
54
|
+
...before,
|
|
55
|
+
auth: 'authorized',
|
|
56
|
+
output: { mode: 'stream', item: { type: 'string' } },
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(diffSchemas(schema({ inspect: before }), schema({ inspect: after }))).toEqual([
|
|
60
|
+
{ kind: 'function-signature-changed', target: 'inspect', breaking: true },
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('uses canonical required membership and compares complete value schemas', () => {
|
|
65
|
+
const member = (required: string[], minLength: number): IrClass => ({
|
|
66
|
+
type: 'node',
|
|
67
|
+
name: 'Person',
|
|
68
|
+
properties: { name: { type: 'string', minLength } },
|
|
69
|
+
required,
|
|
70
|
+
methods: {},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
expect(diffSchemas(withClass(member([], 1)), withClass(member(['name'], 2)))).toEqual([
|
|
74
|
+
{
|
|
75
|
+
kind: 'prop-schema-changed',
|
|
76
|
+
target: 'Person.name',
|
|
77
|
+
detail: 'value constraints changed',
|
|
78
|
+
breaking: true,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
kind: 'prop-required-changed',
|
|
82
|
+
target: 'Person.name',
|
|
83
|
+
detail: 'optional → required',
|
|
84
|
+
breaking: true,
|
|
85
|
+
},
|
|
86
|
+
])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('tracks canonical views, policies, dependencies, topology, and Core', () => {
|
|
90
|
+
const before: SchemaIR = {
|
|
91
|
+
...withClass({
|
|
92
|
+
type: 'edge',
|
|
93
|
+
name: 'owns',
|
|
94
|
+
properties: {},
|
|
95
|
+
required: [],
|
|
96
|
+
methods: {},
|
|
97
|
+
orientation: 'directed',
|
|
98
|
+
endpoints: [],
|
|
99
|
+
}),
|
|
100
|
+
views: { home: { name: 'home', target: { kind: 'domain' }, auth: 'public' } },
|
|
101
|
+
policies: { canRead: { anyOf: [] } },
|
|
102
|
+
dependencies: [{ origin: 'kernel.astrale.ai', revision: 'sha256:one' }],
|
|
103
|
+
core: { nodes: {}, edges: [] },
|
|
104
|
+
}
|
|
105
|
+
const after: SchemaIR = {
|
|
106
|
+
...before,
|
|
107
|
+
classes: {
|
|
108
|
+
owns: { ...before.classes.owns, orientation: 'undirected' },
|
|
109
|
+
},
|
|
110
|
+
views: { home: { ...before.views!.home, auth: 'required' } },
|
|
111
|
+
policies: { canRead: { anyOf: [{ name: 'owner' }] } },
|
|
112
|
+
dependencies: [{ origin: 'kernel.astrale.ai', revision: 'sha256:two' }],
|
|
113
|
+
core: { nodes: { root: {} }, edges: [] },
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expect(diffSchemas(before, after).map((change) => change.kind)).toEqual([
|
|
117
|
+
'edge-contract-changed',
|
|
118
|
+
'view-changed',
|
|
119
|
+
'policy-changed',
|
|
120
|
+
'dependency-changed',
|
|
121
|
+
'core-changed',
|
|
122
|
+
])
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
* diff.ts — structural diff of two Schema IRs + breaking/additive classification.
|
|
3
3
|
* Shared by change tracking (baseline) and data versioning. Pure.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Canonical V1 keeps optionality in the owning `required` arrays and nullability
|
|
6
|
+
* in each value schema. Legacy nullable projections remain supported. Contract
|
|
7
|
+
* changes are classified conservatively: removals, tightened value schemas,
|
|
8
|
+
* callable/auth/policy changes, topology changes, dependency revisions and Core
|
|
9
|
+
* changes are breaking; pure additions and descriptions are additive.
|
|
8
10
|
*/
|
|
9
11
|
import type {
|
|
10
12
|
IrClass,
|
|
13
|
+
IrFunction,
|
|
11
14
|
IrInterface,
|
|
12
15
|
IrMethod,
|
|
13
16
|
JsonSchema,
|
|
@@ -31,23 +34,171 @@ export function baseType(s: JsonSchema | undefined): string {
|
|
|
31
34
|
return s.enum ? `enum(${s.enum.join(',')})` : 'unknown'
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
function
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
function requiredParams(m: IrMethod | IrFunction): string[] {
|
|
38
|
+
return (
|
|
39
|
+
m.requiredParams ??
|
|
40
|
+
Object.entries(m.params ?? {}).flatMap(([name, schema]) => (isOptional(schema) ? [] : [name]))
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function callableContract(m: IrMethod | IrFunction): unknown {
|
|
45
|
+
const required = requiredParams(m)
|
|
46
|
+
return {
|
|
47
|
+
input:
|
|
48
|
+
m.input ??
|
|
49
|
+
({
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: m.params ?? {},
|
|
52
|
+
required,
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
} satisfies JsonSchema),
|
|
55
|
+
required,
|
|
56
|
+
output: m.output ?? { mode: 'value', schema: m.returns },
|
|
57
|
+
static: m.static,
|
|
58
|
+
inheritance: m.inheritance,
|
|
59
|
+
auth: m.auth,
|
|
60
|
+
policy: m.policy,
|
|
61
|
+
}
|
|
40
62
|
}
|
|
41
63
|
|
|
42
64
|
export function diffSchemas(prev: SchemaIR | null, next: SchemaIR | null): SchemaChange[] {
|
|
43
65
|
const changes: SchemaChange[] = []
|
|
44
66
|
if (!prev || !next) return changes
|
|
45
67
|
|
|
68
|
+
if (
|
|
69
|
+
!same(
|
|
70
|
+
{ format: prev.format, version: prev.version, domain: prev.domain },
|
|
71
|
+
{
|
|
72
|
+
format: next.format,
|
|
73
|
+
version: next.version,
|
|
74
|
+
domain: next.domain,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
) {
|
|
78
|
+
changes.push({ kind: 'schema-metadata-changed', target: next.domain, breaking: true })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
diffValueRecord(prev.types ?? {}, next.types ?? {}, 'type', changes)
|
|
82
|
+
diffValueRecord(
|
|
83
|
+
prev.importsByKey ?? prev.imports ?? {},
|
|
84
|
+
next.importsByKey ?? next.imports ?? {},
|
|
85
|
+
'import',
|
|
86
|
+
changes,
|
|
87
|
+
)
|
|
46
88
|
diffMembers(prev.interfaces ?? {}, next.interfaces ?? {}, 'interface', changes)
|
|
47
89
|
diffMembers(prev.classes ?? {}, next.classes ?? {}, 'class', changes)
|
|
90
|
+
diffFunctions(prev.functions ?? {}, next.functions ?? {}, changes)
|
|
91
|
+
diffViews(prev.views ?? {}, next.views ?? {}, changes)
|
|
92
|
+
diffValueRecord(prev.policies ?? {}, next.policies ?? {}, 'policy', changes)
|
|
93
|
+
diffDependencies(prev.dependencies ?? [], next.dependencies ?? [], changes)
|
|
94
|
+
if (!same(prev.core, next.core) && (prev.core !== undefined || next.core !== undefined)) {
|
|
95
|
+
changes.push({ kind: 'core-changed', target: next.domain, breaking: prev.core !== undefined })
|
|
96
|
+
}
|
|
48
97
|
return changes
|
|
49
98
|
}
|
|
50
99
|
|
|
100
|
+
function diffValueRecord(
|
|
101
|
+
prev: Record<string, unknown>,
|
|
102
|
+
next: Record<string, unknown>,
|
|
103
|
+
scope: 'type' | 'import' | 'policy',
|
|
104
|
+
out: SchemaChange[],
|
|
105
|
+
): void {
|
|
106
|
+
for (const name of sortedKeys(next)) {
|
|
107
|
+
if (!(name in prev)) {
|
|
108
|
+
out.push({ kind: `${scope}-added`, target: name, breaking: false })
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
for (const name of sortedKeys(prev)) {
|
|
112
|
+
if (!(name in next)) {
|
|
113
|
+
out.push({ kind: `${scope}-removed`, target: name, breaking: true })
|
|
114
|
+
} else if (!same(prev[name], next[name])) {
|
|
115
|
+
out.push({ kind: `${scope}-changed`, target: name, breaking: true })
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function diffFunctions(
|
|
121
|
+
prev: Record<string, IrFunction>,
|
|
122
|
+
next: Record<string, IrFunction>,
|
|
123
|
+
out: SchemaChange[],
|
|
124
|
+
): void {
|
|
125
|
+
for (const name of sortedKeys(next)) {
|
|
126
|
+
if (!prev[name]) out.push({ kind: 'function-added', target: name, breaking: false })
|
|
127
|
+
}
|
|
128
|
+
for (const name of sortedKeys(prev)) {
|
|
129
|
+
if (!next[name]) {
|
|
130
|
+
out.push({ kind: 'function-removed', target: name, breaking: true })
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
if (!same(callableContract(prev[name]), callableContract(next[name]))) {
|
|
134
|
+
out.push({ kind: 'function-signature-changed', target: name, breaking: true })
|
|
135
|
+
}
|
|
136
|
+
if (prev[name].description !== next[name].description) {
|
|
137
|
+
out.push({ kind: 'function-metadata-changed', target: name, breaking: false })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function diffViews(
|
|
143
|
+
prev: NonNullable<SchemaIR['views']>,
|
|
144
|
+
next: NonNullable<SchemaIR['views']>,
|
|
145
|
+
out: SchemaChange[],
|
|
146
|
+
): void {
|
|
147
|
+
for (const name of sortedKeys(next)) {
|
|
148
|
+
if (!(name in prev)) out.push({ kind: 'view-added', target: name, breaking: false })
|
|
149
|
+
}
|
|
150
|
+
for (const name of sortedKeys(prev)) {
|
|
151
|
+
if (!(name in next)) {
|
|
152
|
+
out.push({ kind: 'view-removed', target: name, breaking: true })
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
!same(
|
|
157
|
+
{ target: prev[name].target, auth: prev[name].auth },
|
|
158
|
+
{
|
|
159
|
+
target: next[name].target,
|
|
160
|
+
auth: next[name].auth,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
) {
|
|
164
|
+
out.push({ kind: 'view-changed', target: name, breaking: true })
|
|
165
|
+
}
|
|
166
|
+
if (prev[name].description !== next[name].description) {
|
|
167
|
+
out.push({ kind: 'view-metadata-changed', target: name, breaking: false })
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function diffDependencies(
|
|
173
|
+
prev: NonNullable<SchemaIR['dependencies']>,
|
|
174
|
+
next: NonNullable<SchemaIR['dependencies']>,
|
|
175
|
+
out: SchemaChange[],
|
|
176
|
+
): void {
|
|
177
|
+
const before = Object.fromEntries(
|
|
178
|
+
prev.map((dependency) => [dependency.origin, dependency.revision]),
|
|
179
|
+
)
|
|
180
|
+
const after = Object.fromEntries(
|
|
181
|
+
next.map((dependency) => [dependency.origin, dependency.revision]),
|
|
182
|
+
)
|
|
183
|
+
for (const origin of sortedKeys(after)) {
|
|
184
|
+
if (!(origin in before)) {
|
|
185
|
+
out.push({ kind: 'dependency-added', target: origin, breaking: true })
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const origin of sortedKeys(before)) {
|
|
189
|
+
if (!(origin in after)) {
|
|
190
|
+
out.push({ kind: 'dependency-removed', target: origin, breaking: true })
|
|
191
|
+
} else if (before[origin] !== after[origin]) {
|
|
192
|
+
out.push({
|
|
193
|
+
kind: 'dependency-changed',
|
|
194
|
+
target: origin,
|
|
195
|
+
detail: `${before[origin]} → ${after[origin]}`,
|
|
196
|
+
breaking: true,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
51
202
|
function memberKind(m: IrClass | IrInterface): 'class' | 'edge' | 'interface' {
|
|
52
203
|
if ((m as IrInterface).type === 'interface') return 'interface'
|
|
53
204
|
return (m as IrClass).type === 'edge' ? 'edge' : 'class'
|
|
@@ -59,13 +210,13 @@ function diffMembers(
|
|
|
59
210
|
_scope: 'class' | 'interface',
|
|
60
211
|
out: SchemaChange[],
|
|
61
212
|
): void {
|
|
62
|
-
for (const name of
|
|
213
|
+
for (const name of sortedKeys(next)) {
|
|
63
214
|
if (!prev[name]) {
|
|
64
215
|
const k = memberKind(next[name])
|
|
65
216
|
out.push({ kind: `${k}-added` as SchemaChange['kind'], target: name, breaking: false })
|
|
66
217
|
}
|
|
67
218
|
}
|
|
68
|
-
for (const name of
|
|
219
|
+
for (const name of sortedKeys(prev)) {
|
|
69
220
|
if (!next[name]) {
|
|
70
221
|
const k = memberKind(prev[name])
|
|
71
222
|
out.push({ kind: `${k}-removed` as SchemaChange['kind'], target: name, breaking: true })
|
|
@@ -83,24 +234,35 @@ function diffMemberBody(
|
|
|
83
234
|
): void {
|
|
84
235
|
const pa = a.properties ?? {}
|
|
85
236
|
const pb = b.properties ?? {}
|
|
86
|
-
for (const p of
|
|
87
|
-
if (!pa[p])
|
|
237
|
+
for (const p of sortedKeys(pb)) {
|
|
238
|
+
if (!pa[p]) {
|
|
239
|
+
const required = propertyRequired(b, p)
|
|
240
|
+
out.push({
|
|
241
|
+
kind: 'prop-added',
|
|
242
|
+
target: `${name}.${p}`,
|
|
243
|
+
detail: required ? 'required' : 'optional',
|
|
244
|
+
breaking: required,
|
|
245
|
+
})
|
|
246
|
+
}
|
|
88
247
|
}
|
|
89
|
-
for (const p of
|
|
248
|
+
for (const p of sortedKeys(pa)) {
|
|
90
249
|
if (!pb[p]) {
|
|
91
250
|
out.push({ kind: 'prop-removed', target: `${name}.${p}`, breaking: true })
|
|
92
251
|
continue
|
|
93
252
|
}
|
|
94
|
-
if (
|
|
253
|
+
if (!same(pa[p], pb[p])) {
|
|
254
|
+
const baseChanged = baseType(pa[p]) !== baseType(pb[p])
|
|
95
255
|
out.push({
|
|
96
|
-
kind: 'prop-type-changed',
|
|
256
|
+
kind: baseChanged ? 'prop-type-changed' : 'prop-schema-changed',
|
|
97
257
|
target: `${name}.${p}`,
|
|
98
|
-
detail:
|
|
258
|
+
detail: baseChanged
|
|
259
|
+
? `${baseType(pa[p])} → ${baseType(pb[p])}`
|
|
260
|
+
: 'value constraints changed',
|
|
99
261
|
breaking: true,
|
|
100
262
|
})
|
|
101
263
|
}
|
|
102
|
-
const wasOpt =
|
|
103
|
-
const nowOpt =
|
|
264
|
+
const wasOpt = !propertyRequired(a, p)
|
|
265
|
+
const nowOpt = !propertyRequired(b, p)
|
|
104
266
|
if (wasOpt !== nowOpt) {
|
|
105
267
|
out.push({
|
|
106
268
|
kind: 'prop-required-changed',
|
|
@@ -113,20 +275,87 @@ function diffMemberBody(
|
|
|
113
275
|
|
|
114
276
|
const ma = a.methods ?? {}
|
|
115
277
|
const mb = b.methods ?? {}
|
|
116
|
-
for (const m of
|
|
117
|
-
if (!ma[m])
|
|
278
|
+
for (const m of sortedKeys(mb)) {
|
|
279
|
+
if (!ma[m]) {
|
|
280
|
+
out.push({
|
|
281
|
+
kind: 'method-added',
|
|
282
|
+
target: `${name}.${m}`,
|
|
283
|
+
breaking: b.type === 'interface' && mb[m].inheritance === 'abstract',
|
|
284
|
+
})
|
|
285
|
+
}
|
|
118
286
|
}
|
|
119
|
-
for (const m of
|
|
287
|
+
for (const m of sortedKeys(ma)) {
|
|
120
288
|
if (!mb[m]) {
|
|
121
289
|
out.push({ kind: 'method-removed', target: `${name}.${m}`, breaking: true })
|
|
122
290
|
continue
|
|
123
291
|
}
|
|
124
|
-
if (
|
|
292
|
+
if (!same(callableContract(ma[m]), callableContract(mb[m]))) {
|
|
125
293
|
out.push({ kind: 'method-signature-changed', target: `${name}.${m}`, breaking: true })
|
|
126
294
|
}
|
|
295
|
+
if (ma[m].description !== mb[m].description) {
|
|
296
|
+
out.push({ kind: 'method-metadata-changed', target: `${name}.${m}`, breaking: false })
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const kind = memberKind(b)
|
|
301
|
+
if (!same(memberContract(a), memberContract(b))) {
|
|
302
|
+
out.push({
|
|
303
|
+
kind: `${kind}-contract-changed`,
|
|
304
|
+
target: name,
|
|
305
|
+
breaking: true,
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
if (!same(memberMetadata(a), memberMetadata(b))) {
|
|
309
|
+
out.push({ kind: 'definition-metadata-changed', target: name, breaking: false })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function propertyRequired(member: IrClass | IrInterface, name: string): boolean {
|
|
314
|
+
return member.required ? member.required.includes(name) : !isOptional(member.properties?.[name])
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function memberContract(member: IrClass | IrInterface): unknown {
|
|
318
|
+
const cls = member as IrClass
|
|
319
|
+
const iface = member as IrInterface
|
|
320
|
+
return {
|
|
321
|
+
type: member.type,
|
|
322
|
+
origin: member.origin,
|
|
323
|
+
ref: member.ref,
|
|
324
|
+
family: iface.family,
|
|
325
|
+
extends: iface.extendsRefs ?? iface.extends,
|
|
326
|
+
implements: cls.implementsRefs ?? cls.implements,
|
|
327
|
+
endpoints: member.endpoints,
|
|
328
|
+
orientation: member.orientation,
|
|
329
|
+
constraints: member.constraints,
|
|
330
|
+
propertyMetadata: member.propertyMetadata,
|
|
331
|
+
data: member.data,
|
|
332
|
+
policies: cls.policies,
|
|
127
333
|
}
|
|
128
334
|
}
|
|
129
335
|
|
|
336
|
+
function memberMetadata(member: IrClass | IrInterface): unknown {
|
|
337
|
+
return { description: member.description, icon: (member as IrClass).icon }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function sortedKeys(value: Record<string, unknown>): string[] {
|
|
341
|
+
return Object.keys(value).sort((left, right) => left.localeCompare(right))
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function same(left: unknown, right: unknown): boolean {
|
|
345
|
+
return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right))
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function canonical(value: unknown): unknown {
|
|
349
|
+
if (Array.isArray(value)) return value.map(canonical)
|
|
350
|
+
if (value === null || typeof value !== 'object') return value
|
|
351
|
+
return Object.fromEntries(
|
|
352
|
+
Object.entries(value as Record<string, unknown>)
|
|
353
|
+
.filter(([, item]) => item !== undefined)
|
|
354
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
355
|
+
.map(([key, item]) => [key, canonical(item)]),
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
130
359
|
export function classify(changes: SchemaChange[]): 'none' | 'additive' | 'breaking' {
|
|
131
360
|
if (changes.length === 0) return 'none'
|
|
132
361
|
return changes.some((c) => c.breaking) ? 'breaking' : 'additive'
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* extractor.ts — the Bun-executed island. Spawned as a short-lived subprocess by
|
|
3
3
|
* runtime.ts. It imports ONLY the domain's pure `schema/index.ts` module graph
|
|
4
|
-
* (never astrale.config.ts or
|
|
5
|
-
*
|
|
4
|
+
* (never astrale.config.ts or the composition entry → integrations). Canonical
|
|
5
|
+
* DomainSchema V1 roots are projected directly; legacy `D.$.ir` / serialize
|
|
6
|
+
* domains retain their existing fallback.
|
|
6
7
|
*
|
|
7
8
|
* bun extractor.ts <schemaIndexPath> <domainDir>
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* driver treats it as a render state, never a crash.
|
|
10
|
+
* A thrown error prints { ok:false } and exits 0 — the driver treats it as a
|
|
11
|
+
* render state, never a crash.
|
|
12
12
|
*/
|
|
13
|
+
import {
|
|
14
|
+
closureFromSdk,
|
|
15
|
+
findCanonicalDomainSchemaExport,
|
|
16
|
+
normalizeLegacySchemaIR,
|
|
17
|
+
projectCanonicalSchema,
|
|
18
|
+
} from './canonical-schema'
|
|
19
|
+
|
|
13
20
|
const schemaPath = process.argv[2]
|
|
14
21
|
const domainDir = process.argv[3] ?? process.cwd()
|
|
15
22
|
|
|
@@ -17,18 +24,36 @@ async function main() {
|
|
|
17
24
|
if (!schemaPath) throw new Error('extractor: missing <schemaIndexPath>')
|
|
18
25
|
const mod: Record<string, any> = await import(schemaPath)
|
|
19
26
|
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
let
|
|
23
|
-
const
|
|
24
|
-
if (
|
|
27
|
+
// Resolve through the DOMAIN's dependency graph. This prevents Studio's own
|
|
28
|
+
// SDK version from accepting or resolving a root authored by another cohort.
|
|
29
|
+
let sdk: Record<string, any> | null = null
|
|
30
|
+
const loadSdkSchema = async (): Promise<Record<string, any>> => {
|
|
31
|
+
if (sdk) return sdk
|
|
25
32
|
const loaded: Record<string, any> = await import(
|
|
26
33
|
Bun.resolveSync('@astrale-os/sdk/schema', domainDir)
|
|
27
34
|
)
|
|
28
|
-
|
|
35
|
+
sdk = loaded
|
|
29
36
|
return loaded
|
|
30
37
|
}
|
|
31
38
|
|
|
39
|
+
const canonicalRoot = findCanonicalDomainSchemaExport(mod)
|
|
40
|
+
if (canonicalRoot) {
|
|
41
|
+
const domainSdk = await loadSdkSchema()
|
|
42
|
+
const projected = projectCanonicalSchema(
|
|
43
|
+
canonicalRoot,
|
|
44
|
+
closureFromSdk(domainSdk, canonicalRoot),
|
|
45
|
+
)
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
JSON.stringify({
|
|
48
|
+
ok: true,
|
|
49
|
+
ir: projected.ir,
|
|
50
|
+
root: canonicalRoot,
|
|
51
|
+
importedInterfaces: projected.importedInterfaces,
|
|
52
|
+
}),
|
|
53
|
+
)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
32
57
|
// Prefer the conventional `D` (compiled), but accept a compiled domain exported
|
|
33
58
|
// under ANY name — e.g. ai-gateway exports its `compileDomain(...)` result as
|
|
34
59
|
// `Gateway`, not `D`. A compiled domain is recognized by shape (`.$.ir`).
|
|
@@ -56,7 +81,7 @@ async function main() {
|
|
|
56
81
|
'schema entry exports no compiled domain (a `compileDomain(...)` value with `.$.ir`, e.g. `D`) nor a raw `schema`',
|
|
57
82
|
)
|
|
58
83
|
}
|
|
59
|
-
const d = await
|
|
84
|
+
const d = await loadSdkSchema()
|
|
60
85
|
if (typeof d.serialize !== 'function') throw new Error('kernel-dsl.serialize unavailable')
|
|
61
86
|
ir = d.serialize(schema)
|
|
62
87
|
}
|
|
@@ -70,7 +95,7 @@ async function main() {
|
|
|
70
95
|
const importSchemas: any[] = Array.isArray(mod?.schema?.imports) ? mod.schema.imports : []
|
|
71
96
|
if (importSchemas.length > 0) {
|
|
72
97
|
try {
|
|
73
|
-
const d = await
|
|
98
|
+
const d = await loadSdkSchema()
|
|
74
99
|
if (typeof d.serialize === 'function') {
|
|
75
100
|
for (const imp of importSchemas) {
|
|
76
101
|
try {
|
|
@@ -88,7 +113,14 @@ async function main() {
|
|
|
88
113
|
}
|
|
89
114
|
}
|
|
90
115
|
|
|
91
|
-
process.stdout.write(
|
|
116
|
+
process.stdout.write(
|
|
117
|
+
JSON.stringify({
|
|
118
|
+
ok: true,
|
|
119
|
+
ir: normalizeLegacySchemaIR(ir),
|
|
120
|
+
root: null,
|
|
121
|
+
importedInterfaces,
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
92
124
|
}
|
|
93
125
|
|
|
94
126
|
main().catch((err: any) => {
|