@astrale-os/cli 1.0.0-beta.0 → 1.0.0-beta.2

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 (180) hide show
  1. package/README.md +7 -11
  2. package/dist/astrale.js +3769 -3549
  3. package/dist/public/connect-core.js +396 -392
  4. package/dist/public/keys/index.js +371 -378
  5. package/dist/public/paths/index.js +371 -378
  6. package/dist/types/lib/idp.d.ts +6 -0
  7. package/dist/types/lib/meta.d.ts +2 -2
  8. package/package.json +6 -4
  9. package/src/admin/binding.ts +168 -0
  10. package/src/admin/catalog/__tests__/client.test.ts +2 -2
  11. package/src/admin/catalog/client.ts +4 -9
  12. package/src/admin/instance/__tests__/client.test.ts +5 -3
  13. package/src/admin/instance/client.ts +9 -27
  14. package/src/commands/__tests__/auth-login.test.ts +54 -1
  15. package/src/commands/__tests__/call.test.ts +5 -1
  16. package/src/commands/__tests__/domain-install-operation.test.ts +23 -7
  17. package/src/commands/__tests__/domain-install-owned.test.ts +5 -1
  18. package/src/commands/__tests__/domain-list.test.ts +83 -4
  19. package/src/commands/__tests__/install-identity-override.test.ts +30 -19
  20. package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
  21. package/src/commands/__tests__/update.test.ts +73 -0
  22. package/src/commands/domain/install.ts +11 -15
  23. package/src/commands/domain/list.ts +17 -12
  24. package/src/commands/studio.ts +20 -0
  25. package/src/commands/update.ts +39 -13
  26. package/src/connection/__tests__/errors.test.ts +16 -12
  27. package/src/connection/__tests__/exchange.test.ts +6 -5
  28. package/src/connection/exchange.ts +5 -4
  29. package/src/lib/__tests__/domain-publication.test.ts +144 -0
  30. package/src/lib/__tests__/idp-session.test.ts +22 -0
  31. package/src/lib/__tests__/idp.test.ts +2 -0
  32. package/src/lib/__tests__/instance.test.ts +6 -0
  33. package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
  34. package/src/lib/__tests__/update.test.ts +12 -0
  35. package/src/lib/domain-publication.ts +102 -0
  36. package/src/lib/idp.ts +5 -1
  37. package/src/lib/instance.ts +6 -1
  38. package/src/lib/login-flow.ts +13 -3
  39. package/src/lib/meta.ts +2 -2
  40. package/src/lib/update.ts +4 -4
  41. package/src/program/__tests__/program.test.ts +1 -1
  42. package/studio/client/dist/assets/{elk-api-D2xgMJvi.js → elk-api-lwhFo7vB.js} +1 -1
  43. package/studio/client/dist/assets/index-B-c-smo9.js +8 -0
  44. package/studio/client/dist/assets/index-BckHuAWk.js +81 -0
  45. package/studio/client/dist/assets/index-C4aNQ6dz.css +1 -0
  46. package/studio/client/dist/index.html +2 -2
  47. package/studio/package.json +3 -1
  48. package/studio/server/agent/bridge/stdio.ts +8 -1
  49. package/studio/server/agent/conversation.test.ts +10 -5
  50. package/studio/server/agent/conversation.ts +56 -8
  51. package/studio/server/agent/harness/codex/loadout.ts +18 -21
  52. package/studio/server/agent/harness/gateway/config.ts +20 -18
  53. package/studio/server/agent/harness/selection.ts +9 -1
  54. package/studio/server/agent/prompts/system.ts +3 -2
  55. package/studio/server/agent/prompts/turn.ts +13 -4
  56. package/studio/server/agent/run/completion.ts +2 -2
  57. package/studio/server/agent/run/preparation.ts +6 -5
  58. package/studio/server/agent/run/transcript.test.ts +35 -1
  59. package/studio/server/agent/run/transcript.ts +169 -3
  60. package/studio/server/agent/run/usage.test.ts +24 -0
  61. package/studio/server/agent/run/usage.ts +26 -1
  62. package/studio/server/api/canvas.ts +44 -0
  63. package/studio/server/api/comments.ts +85 -0
  64. package/studio/server/api/context.ts +61 -0
  65. package/studio/server/api/deployment.ts +39 -0
  66. package/studio/server/api/documents.ts +55 -0
  67. package/studio/server/api/domain.ts +59 -0
  68. package/studio/server/api/http.ts +45 -0
  69. package/studio/server/api/project.ts +57 -0
  70. package/studio/server/api/schema.ts +12 -0
  71. package/studio/server/api/views.ts +57 -0
  72. package/studio/server/api/workspace.ts +60 -0
  73. package/studio/server/api.test.ts +147 -0
  74. package/studio/server/api.ts +20 -422
  75. package/studio/server/cache.test.ts +111 -0
  76. package/studio/server/cache.ts +163 -9
  77. package/studio/server/cli-consumers.test.ts +494 -0
  78. package/studio/server/cli.test.ts +88 -0
  79. package/studio/server/cli.ts +240 -0
  80. package/studio/server/client-package.ts +25 -1
  81. package/studio/server/domain.test.ts +30 -2
  82. package/studio/server/domain.ts +14 -2
  83. package/studio/server/environment/dotenv-preview.test.ts +39 -0
  84. package/studio/server/environment/dotenv-preview.ts +27 -0
  85. package/studio/server/environment/files.test.ts +53 -0
  86. package/studio/server/{state/env.ts → environment/files.ts} +7 -48
  87. package/studio/server/{state → handoff}/copy.ts +21 -6
  88. package/studio/server/handoff/service.test.ts +22 -0
  89. package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
  90. package/studio/server/instances/active.ts +72 -0
  91. package/studio/server/instances/deploy-record.test.ts +31 -0
  92. package/studio/server/instances/deploy-record.ts +37 -0
  93. package/studio/server/instances/deploy.ts +56 -0
  94. package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
  95. package/studio/server/instances/probe.ts +67 -0
  96. package/studio/server/instances/status.test.ts +12 -0
  97. package/studio/server/instances/status.ts +49 -0
  98. package/studio/server/introspect/anatomy/client-tree.ts +57 -0
  99. package/studio/server/introspect/anatomy/env-fields.ts +66 -0
  100. package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
  101. package/studio/server/introspect/anatomy/source.ts +181 -0
  102. package/studio/server/introspect/anatomy/views/legacy.ts +232 -0
  103. package/studio/server/introspect/anatomy/views/routes.ts +105 -0
  104. package/studio/server/introspect/anatomy/views.ts +58 -0
  105. package/studio/server/introspect/anatomy-extras.test.ts +91 -4
  106. package/studio/server/introspect/anatomy-extras.ts +7 -726
  107. package/studio/server/introspect/anatomy.ts +46 -19
  108. package/studio/server/introspect/bundle.ts +15 -6
  109. package/studio/server/introspect/canonical-schema.test.ts +79 -0
  110. package/studio/server/introspect/canonical-schema.ts +80 -18
  111. package/studio/server/introspect/config-preview.test.ts +32 -0
  112. package/studio/server/introspect/config-preview.ts +119 -0
  113. package/studio/server/introspect/diff.test.ts +7 -11
  114. package/studio/server/introspect/diff.ts +28 -37
  115. package/studio/server/introspect/extractor.ts +8 -6
  116. package/studio/server/introspect/hash.ts +5 -2
  117. package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
  118. package/studio/server/introspect/revision.test.ts +54 -0
  119. package/studio/server/introspect/revision.ts +49 -0
  120. package/studio/server/introspect/runtime.test.ts +63 -5
  121. package/studio/server/introspect/runtime.ts +49 -3
  122. package/studio/server/introspect/schema-ir-json.ts +181 -0
  123. package/studio/server/introspect/source-overlay/annotations.ts +14 -0
  124. package/studio/server/introspect/source-overlay/handlers.ts +681 -0
  125. package/studio/server/introspect/source-overlay/kernel-calls.ts +49 -0
  126. package/studio/server/introspect/source-overlay/project.ts +248 -0
  127. package/studio/server/introspect/source-overlay/spans.ts +360 -0
  128. package/studio/server/json.ts +46 -0
  129. package/studio/server/lifecycle.ts +5 -1
  130. package/studio/server/sse.test.ts +26 -0
  131. package/studio/server/sse.ts +4 -1
  132. package/studio/server/state/baseline.test.ts +212 -1
  133. package/studio/server/state/baseline.ts +145 -29
  134. package/studio/server/state/comments.test.ts +66 -1
  135. package/studio/server/state/comments.ts +173 -10
  136. package/studio/server/state/context.ts +34 -2
  137. package/studio/server/state/documents.ts +42 -1
  138. package/studio/server/state/integrations.ts +27 -1
  139. package/studio/server/state/layout.test.ts +34 -0
  140. package/studio/server/state/layout.ts +44 -17
  141. package/studio/server/state/settings.ts +29 -22
  142. package/studio/server/state/store.test.ts +122 -0
  143. package/studio/server/state/store.ts +66 -17
  144. package/studio/server/state/visibility.ts +22 -7
  145. package/studio/server/views/model.test.ts +57 -0
  146. package/studio/server/views/model.ts +66 -0
  147. package/studio/server/views/runtime.ts +40 -0
  148. package/studio/server/views/selection-repository.test.ts +31 -0
  149. package/studio/server/views/selection-repository.ts +60 -0
  150. package/studio/server/views/session.test.ts +73 -0
  151. package/studio/server/views/session.ts +166 -0
  152. package/studio/server/{state/views.test.ts → views/target.test.ts} +8 -159
  153. package/studio/server/views/target.ts +248 -0
  154. package/studio/server/watch.ts +1 -1
  155. package/studio/server/workspace/catalog.test.ts +17 -0
  156. package/studio/server/workspace/catalog.ts +43 -0
  157. package/studio/server/{state → workspace}/create.test.ts +1 -1
  158. package/studio/server/{state → workspace}/create.ts +1 -1
  159. package/studio/server/{state → workspace}/git.ts +1 -1
  160. package/studio/server/workspace/updates.ts +83 -0
  161. package/studio/server/workspace-state.ts +1 -1
  162. package/studio/shared/contracts/README.md +16 -0
  163. package/studio/shared/contracts/agent.ts +270 -0
  164. package/studio/shared/contracts/runtime.ts +50 -0
  165. package/studio/shared/contracts/schema.ts +462 -0
  166. package/studio/shared/contracts/surface.test.ts +319 -0
  167. package/studio/shared/contracts/workspace.ts +274 -0
  168. package/studio/shared/layout.test.ts +116 -0
  169. package/studio/shared/schema/identity.test.ts +58 -0
  170. package/studio/shared/schema/identity.ts +82 -0
  171. package/studio/shared/types.ts +11 -1099
  172. package/studio/tsconfig.json +1 -1
  173. package/viewer/dist/main.js +35 -35
  174. package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
  175. package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
  176. package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
  177. package/studio/server/state/catalog.ts +0 -117
  178. package/studio/server/state/instance.ts +0 -280
  179. package/studio/server/state/updates.ts +0 -63
  180. package/studio/server/state/views.ts +0 -535
@@ -0,0 +1,54 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+
3
+ import { admittedBundleRevisionFromSdk } from './revision'
4
+
5
+ const REVISION = `sha256:${'a'.repeat(64)}` as const
6
+
7
+ describe('installed Bundle revision', () => {
8
+ test('uses bundle admission before asking the cohort schema owner for its revision', () => {
9
+ const calls: string[] = []
10
+ const root = { format: 'astrale.dsl', version: 'v1', origin: 'notes.example.dev' }
11
+ const input = { wire: true }
12
+ const sdk = {
13
+ bundle: {
14
+ accept(value: unknown) {
15
+ calls.push('bundle.accept')
16
+ expect(value).toBe(input)
17
+ return { root, closure: [] }
18
+ },
19
+ },
20
+ schema: {
21
+ revision(value: unknown) {
22
+ calls.push('schema.revision')
23
+ expect(value).toBe(root)
24
+ return REVISION
25
+ },
26
+ },
27
+ }
28
+
29
+ expect(admittedBundleRevisionFromSdk(sdk, input)).toBe(REVISION)
30
+ expect(calls).toEqual(['bundle.accept', 'schema.revision'])
31
+ })
32
+
33
+ test('fails closed when bundle admission or the returned revision is invalid', () => {
34
+ expect(
35
+ admittedBundleRevisionFromSdk(
36
+ { bundle: { accept: () => ({ root: {} }) }, schema: { revision: () => 'sha-short' } },
37
+ {},
38
+ ),
39
+ ).toBeNull()
40
+ expect(
41
+ admittedBundleRevisionFromSdk(
42
+ {
43
+ bundle: {
44
+ accept: () => {
45
+ throw new Error('invalid bundle')
46
+ },
47
+ },
48
+ schema: { revision: () => REVISION },
49
+ },
50
+ {},
51
+ ),
52
+ ).toBeNull()
53
+ })
54
+ })
@@ -0,0 +1,49 @@
1
+ import type { SchemaRevision } from '../../shared/types'
2
+
3
+ import { isSchemaRevision } from '../../shared/types'
4
+
5
+ type AnyRecord = Record<string, unknown>
6
+
7
+ /**
8
+ * Cross the installed-bundle JSON boundary through one exact SDK cohort, then
9
+ * ask the DSL owner for the root revision. Invalid/unavailable bundles return
10
+ * null so Studio reports unknown drift instead of inventing an identity.
11
+ */
12
+ export function admittedBundleRevisionFromSdk(
13
+ sdkModule: Record<string, unknown>,
14
+ input: unknown,
15
+ ): SchemaRevision | null {
16
+ const bundleApi = asRecord(sdkModule.bundle)
17
+ const schemaApi = asRecord(sdkModule.schema)
18
+ if (typeof bundleApi?.accept !== 'function' || typeof schemaApi?.revision !== 'function') {
19
+ return null
20
+ }
21
+ try {
22
+ const accepted = asRecord(Reflect.apply(bundleApi.accept, bundleApi, [input]))
23
+ if (!accepted || !Object.prototype.hasOwnProperty.call(accepted, 'root')) return null
24
+ const revision = Reflect.apply(schemaApi.revision, schemaApi, [accepted.root])
25
+ return isSchemaRevision(revision) ? revision : null
26
+ } catch {
27
+ return null
28
+ }
29
+ }
30
+
31
+ export async function admittedBundleRevision(
32
+ domainRoot: string,
33
+ input: unknown,
34
+ ): Promise<SchemaRevision | null> {
35
+ try {
36
+ const sdkModule: Record<string, unknown> = await import(
37
+ Bun.resolveSync('@astrale-os/sdk/schema', domainRoot)
38
+ )
39
+ return admittedBundleRevisionFromSdk(sdkModule, input)
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ function asRecord(value: unknown): AnyRecord | null {
46
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
47
+ ? (value as AnyRecord)
48
+ : null
49
+ }
@@ -6,7 +6,8 @@ import { join } from 'node:path'
6
6
  import type { DomainHandle } from '../domain'
7
7
 
8
8
  import { buildBundle } from './bundle'
9
- import { schemaHashOf } from './hash'
9
+ import { renderFingerprintOf } from './hash'
10
+ import { admittedBundleRevision } from './revision'
10
11
  import { coreExtract, runtimeExtract } from './runtime'
11
12
 
12
13
  const roots: string[] = []
@@ -40,7 +41,7 @@ function currentFixture(): DomainHandle {
40
41
  schemaIndex,
41
42
  `
42
43
  import {
43
- defineSchema, domain, edge, edgeClass, fn, node, nodeClass,
44
+ bundle, defineSchema, domain, edge, edgeClass, fn, node, nodeClass,
44
45
  nodeInterface, output, property, view,
45
46
  } from '@astrale-os/sdk/schema'
46
47
 
@@ -84,6 +85,7 @@ export const schema = defineSchema('documents.runtime.test', {
84
85
  edges: [edge(welcome, owned_by, domain(), {})],
85
86
  },
86
87
  })
88
+ export const installedBundle = bundle.create(schema)
87
89
  `,
88
90
  )
89
91
  // Canonical core extraction must not import the effectful composition entry.
@@ -105,6 +107,8 @@ describe('current SDK schema extractor', () => {
105
107
  const result = await runtimeExtract(handle.schemaIndex, handle.root)
106
108
 
107
109
  expect(result.ok).toBe(true)
110
+ expect(result.schemaMode).toBe('canonical-admitted')
111
+ expect(result.revision).toMatch(/^sha256:[0-9a-f]{64}$/)
108
112
  expect(result.root).toMatchObject({
109
113
  format: 'astrale.dsl',
110
114
  version: 'v1',
@@ -133,15 +137,48 @@ describe('current SDK schema extractor', () => {
133
137
  })
134
138
  })
135
139
 
136
- test('hashes the raw canonical root instead of the render projection', async () => {
140
+ test('transports the DSL revision and fingerprints the root only for rendering', async () => {
137
141
  const handle = currentFixture()
138
142
  const extracted = await runtimeExtract(handle.schemaIndex, handle.root)
139
143
  expect(extracted.ok).toBe(true)
144
+ if (extracted.revision === null) throw new Error('expected an admitted schema revision')
140
145
 
141
146
  const bundle = await buildBundle(handle)
142
147
  expect(bundle.schemaRoot).toEqual(extracted.root)
143
- expect(bundle.schemaHash).toBe(schemaHashOf(extracted.root))
144
- expect(bundle.schemaHash).not.toBe(schemaHashOf(extracted.ir))
148
+ expect(bundle.schemaMode).toBe('canonical-admitted')
149
+ expect(bundle.schemaRevision).toBe(extracted.revision)
150
+ expect(bundle.renderFingerprint).toBe(renderFingerprintOf(extracted.root))
151
+ expect(bundle.renderFingerprint).not.toBe(renderFingerprintOf(extracted.ir))
152
+ })
153
+
154
+ test('re-admits installed Bundle JSON before comparing its DSL revision', async () => {
155
+ const handle = currentFixture()
156
+ const extracted = await runtimeExtract(handle.schemaIndex, handle.root)
157
+ if (extracted.revision === null) throw new Error('expected an admitted local schema revision')
158
+ const fixture = (await import(handle.schemaIndex)) as { installedBundle: unknown }
159
+ const installedWire = JSON.parse(JSON.stringify(fixture.installedBundle))
160
+
161
+ expect(await admittedBundleRevision(handle.root, installedWire)).toBe(extracted.revision)
162
+ })
163
+
164
+ test('renders a V1-shaped but unadmitted root only as a structural preview', async () => {
165
+ const root = fixtureRoot('canonical-preview')
166
+ linkCurrentSdk(root)
167
+ const schemaIndex = join(root, 'schema', 'index.ts')
168
+ writeFileSync(
169
+ schemaIndex,
170
+ `export const schema = {
171
+ format: 'astrale.dsl', version: 'v1', origin: 'not an origin',
172
+ dependencies: [], types: {}, interfaces: {}, classes: {}, functions: {},
173
+ policies: {}, views: {}, core: { nodes: {}, edges: [] },
174
+ }\n`,
175
+ )
176
+
177
+ const result = await runtimeExtract(schemaIndex, root)
178
+ expect(result.ok).toBe(true)
179
+ expect(result.schemaMode).toBe('canonical-preview')
180
+ expect(result.revision).toBeNull()
181
+ expect(result.root).toMatchObject({ format: 'astrale.dsl', version: 'v1' })
145
182
  })
146
183
 
147
184
  test('extracts canonical core without importing implementation.ts', async () => {
@@ -172,6 +209,27 @@ describe('current SDK schema extractor', () => {
172
209
  })
173
210
  })
174
211
 
212
+ test('runtime extractor preserves the legacy compiled-IR envelope', async () => {
213
+ const root = fixtureRoot('legacy-runtime')
214
+ const schemaIndex = join(root, 'schema', 'index.ts')
215
+ writeFileSync(
216
+ schemaIndex,
217
+ `export const D = { $: { ir: {
218
+ version: 'legacy', domain: 'legacy.runtime.test', types: {}, interfaces: {},
219
+ classes: {}, imports: {}, functions: {},
220
+ } } }\n`,
221
+ )
222
+
223
+ const result = await runtimeExtract(schemaIndex, root)
224
+ expect(result).toMatchObject({
225
+ ok: true,
226
+ schemaMode: 'legacy',
227
+ revision: null,
228
+ root: null,
229
+ ir: { domain: 'legacy.runtime.test', functions: {} },
230
+ })
231
+ })
232
+
175
233
  test('core extractor preserves the legacy defineCore fallback', async () => {
176
234
  const root = fixtureRoot('legacy-core')
177
235
  const schemaIndex = join(root, 'schema', 'index.ts')
@@ -5,7 +5,15 @@
5
5
  * render IR plus the raw canonical root (when present), or an error render-state
6
6
  * — never throws.
7
7
  */
8
- import type { IrInterface, SchemaIR, StudioCore } from '../../shared/types'
8
+ import type {
9
+ IrInterface,
10
+ SchemaIR,
11
+ SchemaRevision,
12
+ StudioCore,
13
+ StudioSchemaBundle,
14
+ } from '../../shared/types'
15
+
16
+ import { isSchemaRevision } from '../../shared/types'
9
17
 
10
18
  const EXTRACTOR = new URL('./extractor.ts', import.meta.url).pathname
11
19
  const CORE_EXTRACTOR = new URL('./core-extractor.ts', import.meta.url).pathname
@@ -13,8 +21,11 @@ const CORE_EXTRACTOR = new URL('./core-extractor.ts', import.meta.url).pathname
13
21
  export interface RuntimeExtractResult {
14
22
  ok: boolean
15
23
  ir: SchemaIR | null
16
- /** Exact canonical V1 root. Null for legacy `D.$.ir` domains and failures. */
24
+ /** Portable canonical V1 document. Null for legacy domains and failures. */
17
25
  root: unknown | null
26
+ schemaMode: StudioSchemaBundle['schemaMode']
27
+ /** Present only when the Domain cohort SDK admitted `root`. */
28
+ revision: SchemaRevision | null
18
29
  /** member bodies of imported (kernel + cross-domain) interfaces, by name */
19
30
  importedInterfaces?: Record<string, IrInterface>
20
31
  error?: { message: string }
@@ -42,6 +53,8 @@ export async function runtimeExtract(
42
53
  ok: false,
43
54
  ir: null,
44
55
  root: null,
56
+ schemaMode: 'unavailable',
57
+ revision: null,
45
58
  error: { message: err.trim() || 'extractor produced no output' },
46
59
  }
47
60
  }
@@ -51,16 +64,49 @@ export async function runtimeExtract(
51
64
  ok: false,
52
65
  ir: null,
53
66
  root: null,
67
+ schemaMode: 'unavailable',
68
+ revision: null,
54
69
  error: parsed.error ?? { message: 'extraction failed' },
55
70
  }
71
+ const schemaMode = parsed.schemaMode
72
+ const revision = parsed.revision
73
+ const validMode =
74
+ schemaMode === 'canonical-admitted' ||
75
+ schemaMode === 'canonical-preview' ||
76
+ schemaMode === 'legacy'
77
+ const validRevision =
78
+ schemaMode === 'canonical-admitted' ? isSchemaRevision(revision) : revision == null
79
+ const validRoot =
80
+ schemaMode === 'legacy'
81
+ ? parsed.root == null
82
+ : parsed.root !== null && parsed.root !== undefined
83
+ if (!validMode || !validRevision || !validRoot) {
84
+ return {
85
+ ok: false,
86
+ ir: null,
87
+ root: null,
88
+ schemaMode: 'unavailable',
89
+ revision: null,
90
+ error: { message: 'extractor produced an invalid schema admission envelope' },
91
+ }
92
+ }
56
93
  return {
57
94
  ok: true,
58
95
  ir: parsed.ir as SchemaIR,
59
96
  root: parsed.root ?? null,
97
+ schemaMode,
98
+ revision: revision ?? null,
60
99
  importedInterfaces: (parsed.importedInterfaces ?? {}) as Record<string, IrInterface>,
61
100
  }
62
101
  } catch (e: any) {
63
- return { ok: false, ir: null, root: null, error: { message: String(e?.message ?? e) } }
102
+ return {
103
+ ok: false,
104
+ ir: null,
105
+ root: null,
106
+ schemaMode: 'unavailable',
107
+ revision: null,
108
+ error: { message: String(e?.message ?? e) },
109
+ }
64
110
  }
65
111
  }
66
112
 
@@ -0,0 +1,181 @@
1
+ /** Structural admission for persisted Studio render IR. SDK admission is separate. */
2
+
3
+ import type {
4
+ IrFunction,
5
+ IrInterface,
6
+ IrMethod,
7
+ IrSchemaRef,
8
+ JsonSchema,
9
+ SchemaIR,
10
+ } from '../../shared/types'
11
+
12
+ import { asJsonRecord, asStringArray } from '../json'
13
+
14
+ function optional<T>(value: unknown, guard: (candidate: unknown) => candidate is T): boolean {
15
+ return value === undefined || guard(value)
16
+ }
17
+
18
+ function recordOf(value: unknown, guard: (candidate: unknown) => boolean): boolean {
19
+ const record = asJsonRecord(value)
20
+ return !!record && Object.values(record).every(guard)
21
+ }
22
+
23
+ function isJsonSchema(value: unknown): value is JsonSchema {
24
+ return !!asJsonRecord(value)
25
+ }
26
+
27
+ function isSchemaRef(value: unknown): value is IrSchemaRef {
28
+ const record = asJsonRecord(value)
29
+ return (
30
+ typeof record?.origin === 'string' &&
31
+ typeof record.name === 'string' &&
32
+ ['type', 'interface', 'class', 'function', 'policy', 'view', 'core'].includes(
33
+ String(record.kind),
34
+ )
35
+ )
36
+ }
37
+
38
+ function isSchemaRefArray(value: unknown): value is IrSchemaRef[] {
39
+ return Array.isArray(value) && value.every(isSchemaRef)
40
+ }
41
+
42
+ function isOutput(value: unknown): boolean {
43
+ const record = asJsonRecord(value)
44
+ if (!record) return false
45
+ if (record.mode === 'binary') return true
46
+ if (record.mode === 'value') return isJsonSchema(record.schema)
47
+ if (record.mode === 'stream') return isJsonSchema(record.item)
48
+ return false
49
+ }
50
+
51
+ function isMethod(value: unknown): value is IrMethod {
52
+ const record = asJsonRecord(value)
53
+ return (
54
+ typeof record?.name === 'string' &&
55
+ recordOf(record.params, isJsonSchema) &&
56
+ isJsonSchema(record.returns) &&
57
+ typeof record.static === 'boolean' &&
58
+ ['default', 'abstract', 'sealed'].includes(String(record.inheritance)) &&
59
+ optional(record.input, isJsonSchema) &&
60
+ (record.requiredParams === undefined || asStringArray(record.requiredParams) !== undefined) &&
61
+ (record.output === undefined || isOutput(record.output))
62
+ )
63
+ }
64
+
65
+ function isEndpoint(value: unknown): boolean {
66
+ const record = asJsonRecord(value)
67
+ if (!record || typeof record.name !== 'string' || !asStringArray(record.types)) return false
68
+ if (record.refs !== undefined && !isSchemaRefArray(record.refs)) return false
69
+ if (record.cardinality !== undefined) {
70
+ const cardinality = asJsonRecord(record.cardinality)
71
+ if (
72
+ !cardinality ||
73
+ typeof cardinality.min !== 'number' ||
74
+ !Number.isFinite(cardinality.min) ||
75
+ (cardinality.max !== null &&
76
+ (typeof cardinality.max !== 'number' || !Number.isFinite(cardinality.max)))
77
+ ) {
78
+ return false
79
+ }
80
+ }
81
+ return true
82
+ }
83
+
84
+ function isInterface(value: unknown): value is IrInterface {
85
+ const record = asJsonRecord(value)
86
+ return (
87
+ record?.type === 'interface' &&
88
+ typeof record.name === 'string' &&
89
+ recordOf(record.properties, isJsonSchema) &&
90
+ recordOf(record.methods, isMethod) &&
91
+ (record.required === undefined || asStringArray(record.required) !== undefined) &&
92
+ (record.extends === undefined || asStringArray(record.extends) !== undefined) &&
93
+ (record.extendsRefs === undefined || isSchemaRefArray(record.extendsRefs)) &&
94
+ (record.endpoints === undefined ||
95
+ (Array.isArray(record.endpoints) && record.endpoints.every(isEndpoint)))
96
+ )
97
+ }
98
+
99
+ function isClass(value: unknown): boolean {
100
+ const record = asJsonRecord(value)
101
+ return (
102
+ (record?.type === 'node' || record?.type === 'edge') &&
103
+ typeof record.name === 'string' &&
104
+ recordOf(record.properties, isJsonSchema) &&
105
+ recordOf(record.methods, isMethod) &&
106
+ (record.required === undefined || asStringArray(record.required) !== undefined) &&
107
+ (record.implements === undefined || asStringArray(record.implements) !== undefined) &&
108
+ (record.implementsRefs === undefined || isSchemaRefArray(record.implementsRefs)) &&
109
+ (record.endpoints === undefined ||
110
+ (Array.isArray(record.endpoints) && record.endpoints.every(isEndpoint)))
111
+ )
112
+ }
113
+
114
+ function isFunction(value: unknown): value is IrFunction {
115
+ const record = asJsonRecord(value)
116
+ return (
117
+ typeof record?.name === 'string' &&
118
+ isJsonSchema(record.input) &&
119
+ recordOf(record.params, isJsonSchema) &&
120
+ (record.requiredParams === undefined || asStringArray(record.requiredParams) !== undefined) &&
121
+ isOutput(record.output) &&
122
+ isJsonSchema(record.returns) &&
123
+ record.static === true &&
124
+ record.inheritance === 'default'
125
+ )
126
+ }
127
+
128
+ function isImport(value: unknown): boolean {
129
+ const record = asJsonRecord(value)
130
+ return (
131
+ typeof record?.origin === 'string' &&
132
+ (record.definition === 'class' || record.definition === 'interface') &&
133
+ optional(record.ref, isSchemaRef)
134
+ )
135
+ }
136
+
137
+ function isView(value: unknown): boolean {
138
+ const record = asJsonRecord(value)
139
+ const target = asJsonRecord(record?.target)
140
+ return (
141
+ typeof record?.name === 'string' &&
142
+ ['required', 'optional', 'public'].includes(String(record.auth)) &&
143
+ !!target &&
144
+ (target.kind === 'domain' ||
145
+ (target.kind === 'definition' && isSchemaRefArray(target.definitions)))
146
+ )
147
+ }
148
+
149
+ export function decodeSchemaIR(value: unknown): SchemaIR | undefined {
150
+ const record = asJsonRecord(value)
151
+ if (
152
+ !record ||
153
+ typeof record.version !== 'string' ||
154
+ typeof record.domain !== 'string' ||
155
+ !recordOf(record.types, isJsonSchema) ||
156
+ !recordOf(record.interfaces, isInterface) ||
157
+ !recordOf(record.classes, isClass) ||
158
+ !recordOf(record.imports, isImport) ||
159
+ !recordOf(record.functions, isFunction) ||
160
+ (record.importsByKey !== undefined && !recordOf(record.importsByKey, isImport)) ||
161
+ (record.importedInterfacesByKey !== undefined &&
162
+ !recordOf(record.importedInterfacesByKey, isInterface)) ||
163
+ (record.views !== undefined && !recordOf(record.views, isView)) ||
164
+ (record.policies !== undefined && !asJsonRecord(record.policies)) ||
165
+ (record.dependencies !== undefined &&
166
+ (!Array.isArray(record.dependencies) ||
167
+ !record.dependencies.every((dependency) => {
168
+ const item = asJsonRecord(dependency)
169
+ return typeof item?.origin === 'string' && typeof item.revision === 'string'
170
+ })))
171
+ ) {
172
+ return undefined
173
+ }
174
+ return record as unknown as SchemaIR
175
+ }
176
+
177
+ export function decodeIrInterfaceRecord(value: unknown): Record<string, IrInterface> | undefined {
178
+ const record = asJsonRecord(value)
179
+ if (!record || !Object.values(record).every(isInterface)) return undefined
180
+ return record as unknown as Record<string, IrInterface>
181
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Compatibility owner for Studio schema annotations.
3
+ *
4
+ * The current Kernel admits enum updates against their exact Value Schema, so
5
+ * there are no source-derived annotations today. The serialized overlay field
6
+ * remains consumed by the client and shared contract.
7
+ */
8
+ import type { SchemaAnnotation, SchemaIR } from '../../../shared/types'
9
+
10
+ export function buildSchemaAnnotations(_args: { ir: SchemaIR | null }): SchemaAnnotation[] {
11
+ // The current Kernel admits updated values against their exact Value Schema;
12
+ // enum properties therefore need no Studio-specific compatibility warning.
13
+ return []
14
+ }