@astrale-os/cli 1.0.0-beta.27 → 1.0.0-beta.29

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 (108) hide show
  1. package/README.md +59 -3
  2. package/dist/astrale.js +1765 -1057
  3. package/dist/public/connect-core.js +86 -17
  4. package/dist/public/keys/index.js +69 -2
  5. package/dist/public/paths/index.js +67 -0
  6. package/dist/types/connection/session.d.ts +2 -2
  7. package/dist/types/lib/config.d.ts +6 -0
  8. package/dist/types/state/exchange-credentials.d.ts +6 -2
  9. package/dist/types/state/files.d.ts +2 -0
  10. package/dist/types/state/index.d.ts +3 -2
  11. package/dist/types/state/paths.d.ts +2 -0
  12. package/dist/types/state/session-routes.d.ts +10 -0
  13. package/package.json +2 -2
  14. package/src/commands/__tests__/domain-install-operation.test.ts +23 -0
  15. package/src/commands/__tests__/install-identity-override.test.ts +3 -3
  16. package/src/commands/auth/logout.ts +2 -1
  17. package/src/commands/browser.ts +29 -0
  18. package/src/commands/domain/install.ts +19 -11
  19. package/src/commands/get.ts +1 -1
  20. package/src/commands/identity/register.ts +3 -4
  21. package/src/commands/logs.ts +2 -5
  22. package/src/commands/session/analyze.ts +26 -4
  23. package/src/connection/.spec/architecture.md +9 -1
  24. package/src/connection/__tests__/auth.test.ts +1 -0
  25. package/src/connection/__tests__/credential.test.ts +1 -0
  26. package/src/connection/__tests__/exchange.test.ts +34 -10
  27. package/src/connection/__tests__/session.test.ts +5 -1
  28. package/src/connection/__tests__/target.test.ts +1 -0
  29. package/src/connection/exchange.ts +63 -38
  30. package/src/connection/session.ts +8 -1
  31. package/src/identity/__tests__/fixtures/registry-journey.ts +13 -1
  32. package/src/identity/__tests__/registry.test.ts +4 -0
  33. package/src/identity/registry.ts +2 -0
  34. package/src/lib/__tests__/browser-retention.test.ts +212 -0
  35. package/src/lib/__tests__/config.test.ts +27 -0
  36. package/src/lib/__tests__/skills.test.ts +8 -2
  37. package/src/lib/browser-retention.ts +210 -0
  38. package/src/lib/config.ts +12 -1
  39. package/src/program/__tests__/program.test.ts +1 -1
  40. package/src/state/.spec/api.d.ts +21 -2
  41. package/src/state/.spec/architecture.md +18 -4
  42. package/src/state/.spec/layout.ts +1 -0
  43. package/src/state/__tests__/exchange-credentials.test.ts +88 -42
  44. package/src/state/__tests__/files.test.ts +24 -1
  45. package/src/state/__tests__/fixtures/session-route-process.ts +93 -0
  46. package/src/state/__tests__/paths.test.ts +1 -0
  47. package/src/state/__tests__/session-routes.test.ts +138 -0
  48. package/src/state/exchange-credentials.ts +22 -9
  49. package/src/state/files.ts +41 -0
  50. package/src/state/index.ts +3 -1
  51. package/src/state/paths.ts +3 -0
  52. package/src/state/session-routes.ts +34 -0
  53. package/src/telemetry/__tests__/analyze-log.test.ts +38 -0
  54. package/src/telemetry/__tests__/retention.test.ts +267 -0
  55. package/src/telemetry/__tests__/settings.test.ts +102 -0
  56. package/src/telemetry/__tests__/store-scan.test.ts +128 -0
  57. package/src/telemetry/__tests__/trigger.test.ts +33 -4
  58. package/src/telemetry/analyze.ts +24 -2
  59. package/src/telemetry/recorder.ts +6 -3
  60. package/src/telemetry/retention.ts +176 -0
  61. package/src/telemetry/session.ts +18 -12
  62. package/src/telemetry/settings.ts +64 -11
  63. package/src/telemetry/store.ts +92 -9
  64. package/src/telemetry/trigger.ts +16 -28
  65. package/studio/client/dist/assets/index-BFVFs_8x.js +81 -0
  66. package/studio/client/dist/assets/index-DuB9iUdu.css +2 -0
  67. package/studio/client/dist/assets/schema-studio--a0kX3QU.css +1 -0
  68. package/studio/client/dist/assets/schema-studio-DH6oEWME.js +8 -0
  69. package/studio/client/dist/index.html +3 -3
  70. package/studio/package.json +1 -1
  71. package/studio/server/agent/prompts/anchors.test.ts +17 -4
  72. package/studio/server/agent/prompts/anchors.ts +3 -2
  73. package/studio/server/agent/prompts/system.test.ts +2 -3
  74. package/studio/server/agent/prompts/system.ts +3 -3
  75. package/studio/server/agent/run/preparation.ts +1 -1
  76. package/studio/server/api/context.ts +1 -1
  77. package/studio/server/api/deployment.ts +1 -1
  78. package/studio/server/api/views.ts +2 -2
  79. package/studio/server/api/workspace.ts +1 -1
  80. package/studio/server/cache.test.ts +3 -8
  81. package/studio/server/cache.ts +4 -45
  82. package/studio/server/handoff/copy.ts +1 -1
  83. package/studio/server/introspect/canonical-schema.test.ts +154 -128
  84. package/studio/server/introspect/canonical-schema.ts +183 -302
  85. package/studio/server/introspect/core.ts +14 -21
  86. package/studio/server/introspect/extractor.ts +11 -15
  87. package/studio/server/introspect/overlay-tsmorph.test.ts +1 -5
  88. package/studio/server/introspect/overlay-tsmorph.ts +0 -1
  89. package/studio/server/introspect/overlay.ts +3 -24
  90. package/studio/server/introspect/revision.test.ts +24 -28
  91. package/studio/server/introspect/revision.ts +10 -21
  92. package/studio/server/introspect/runtime.test.ts +14 -14
  93. package/studio/server/introspect/runtime.ts +1 -46
  94. package/studio/server/lifecycle.ts +4 -1
  95. package/studio/server/state/documents.test.ts +69 -0
  96. package/studio/server/state/documents.ts +57 -10
  97. package/studio/shared/contracts/schema.ts +10 -26
  98. package/studio/shared/contracts/surface.test.ts +2 -2
  99. package/studio/shared/contracts/workspace.ts +3 -0
  100. package/studio/shared/schema/identity.ts +0 -1
  101. package/viewer/dist/main.js +28 -28
  102. package/studio/client/dist/assets/index-C0FAnwNw.css +0 -2
  103. package/studio/client/dist/assets/index-CpBed0V1.js +0 -81
  104. package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +0 -1
  105. package/studio/client/dist/assets/schema-studio-DJm9guI8.js +0 -8
  106. package/studio/server/introspect/core-extractor.ts +0 -30
  107. package/studio/server/introspect/overlay.test.ts +0 -44
  108. package/studio/server/introspect/source-overlay/annotations.ts +0 -14
@@ -1,12 +1,12 @@
1
1
  <!doctype html>
2
- <html lang="en" class="dark">
2
+ <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Domain Studio</title>
7
- <script type="module" crossorigin src="/assets/index-CpBed0V1.js"></script>
7
+ <script type="module" crossorigin src="/assets/index-BFVFs_8x.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-Dd_uD5pT.js">
9
- <link rel="stylesheet" crossorigin href="/assets/index-C0FAnwNw.css">
9
+ <link rel="stylesheet" crossorigin href="/assets/index-DuB9iUdu.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -21,7 +21,7 @@
21
21
  "typecheck": "tsgo --noEmit"
22
22
  },
23
23
  "dependencies": {
24
- "@astrale-os/sdk": "0.5.0-beta.51",
24
+ "@astrale-os/sdk": "0.5.0-beta.54",
25
25
  "@astrale-os/shell": "0.4.2-beta.6",
26
26
  "@dagrejs/dagre": "^3.0.0",
27
27
  "@radix-ui/react-collapsible": "^1.1.0",
@@ -41,7 +41,20 @@ const ir: SchemaIR = {
41
41
  functions: {
42
42
  inspect: {
43
43
  name: 'inspect',
44
- input: { type: 'object', properties: { cursor: { type: 'string' } }, required: [] },
44
+ input: {
45
+ type: 'object',
46
+ properties: {
47
+ cursor: {
48
+ $ref: 'https://schemas.astrale.ai/graph/1/node-path',
49
+ 'x-astrale-path': {
50
+ target: 'node',
51
+ cardinality: 'one',
52
+ accepts: [{ origin: 'example.dev', kind: 'class', name: 'Item' }],
53
+ },
54
+ },
55
+ },
56
+ required: [],
57
+ },
45
58
  output: { mode: 'stream', item: { type: 'integer' } },
46
59
  auth: 'authenticated',
47
60
  },
@@ -63,8 +76,8 @@ describe('anchor descriptions', () => {
63
76
  })
64
77
 
65
78
  test('describes standalone Functions with output mode and authentication', () => {
66
- expect(describeAnchor('function.inspect', ir, undefined)).toContain(
67
- 'stream<int> [authenticated]',
68
- )
79
+ const description = describeAnchor('function.inspect', ir, undefined)
80
+ expect(description).toContain('inspect(cursor:→node?)')
81
+ expect(description).toContain('stream<int> [authenticated]')
69
82
  })
70
83
  })
@@ -14,6 +14,8 @@ import type {
14
14
  SchemaOverlay,
15
15
  } from '../../../shared/types'
16
16
 
17
+ import { isNodePathSchema } from '../../../shared/types'
18
+
17
19
  /** Terse JSON-Schema type label (mirrors the client's format.tsx describe/typeLabel). */
18
20
  function propType(s: JsonSchema | undefined, optionalOverride?: boolean): string {
19
21
  if (!s) return 'any'
@@ -22,8 +24,7 @@ function propType(s: JsonSchema | undefined, optionalOverride?: boolean): string
22
24
  const base = Array.isArray(t) ? t.find((x) => x !== 'null') : t
23
25
  let label: string
24
26
  if (s.enum) label = `enum(${s.enum.map(String).join('|')})`
25
- else if (s.$nodeRef) label = '→node'
26
- else if (s.$dataRef) label = '→data'
27
+ else if (isNodePathSchema(s)) label = '→node'
27
28
  else if (base === 'array') label = `${propType(s.items)}[]`
28
29
  else if (base === 'object') label = `{${Object.keys(s.properties ?? {}).join(',')}}`
29
30
  else if (base === 'integer') label = 'int'
@@ -2,12 +2,11 @@ import { expect, test } from 'bun:test'
2
2
 
3
3
  import { buildSystemPrompt } from './system'
4
4
 
5
- test('embedded agents receive the current SDK layout contract', () => {
5
+ test('embedded agents receive the current SDK and frontend skill contracts', () => {
6
6
  const prompt = buildSystemPrompt({ bridge: false })
7
7
 
8
8
  expect(prompt).toContain('modular Actions and Workflows')
9
- expect(prompt).toContain('pre-Kernel-V2 APIs or layouts')
9
+ expect(prompt).toContain('astrale-frontend-design')
10
10
  expect(prompt).toContain('Runtime/Application entries')
11
- expect(prompt).toContain('do not restore defineDomain or Interface-era layouts')
12
11
  expect(prompt).not.toContain('schema/ runtime/ views/')
13
12
  })
@@ -25,12 +25,12 @@ export function buildSystemPrompt(options: { bridge: boolean }): string {
25
25
  'Operating rules (when a thread genuinely calls for a code change):',
26
26
  '- Read the skills under `.agents/skills/` FIRST and follow them: **astrale-domain**',
27
27
  ' (schema modeling, handlers, views, deploy/install) before editing schema/handlers,',
28
- ' and **astrale-cli** when you run the `astrale` CLI. Treat those current skills and',
29
- ' the project itself as authoritative; do not restore pre-Kernel-V2 APIs or layouts.',
28
+ ' **astrale-frontend-design** before editing a Domain UI, and **astrale-cli** when you',
29
+ ' run the `astrale` CLI. Treat those current skills and the project itself as authoritative.',
30
30
  '- Prefer the project’s existing semantic modules over inventing new structure. Current',
31
31
  ' SDK projects author Schema definitions, modular Actions and Workflows, a Runtime,',
32
32
  ' and an Application. Wire modules through their semantic SDK facades and explicit',
33
- ' Runtime/Application entries; do not restore defineDomain or Interface-era layouts.',
33
+ ' Runtime/Application entries.',
34
34
  '- The studio re-renders automatically as you save files — never start, build, or refresh',
35
35
  ' anything for the UI to update.',
36
36
  '- Sanity-check schema/handler edits with `pnpm typecheck` (or `tsgo --noEmit`).',
@@ -94,7 +94,7 @@ export async function prepareRun(
94
94
  bareResume && !firstTurn
95
95
  ? buildResumePrompt()
96
96
  : buildTurnPrompt({
97
- origin: bundle?.overlay.origin ?? domainId,
97
+ origin: bundle?.ir?.domain ?? handle.origin ?? domainId,
98
98
  root,
99
99
  renderFingerprint,
100
100
  schemaRevision: bundle?.schemaRevision,
@@ -43,7 +43,7 @@ export async function handleContextRoute(context: DomainRouteContext): Promise<R
43
43
  const storedContext = readContext(root)
44
44
  const openComments = readComments(root).comments.filter((comment) => comment.status === 'open')
45
45
  const markdown = buildCopyMarkdown({
46
- origin: bundle?.overlay.origin ?? id,
46
+ origin: bundle?.ir?.domain ?? handle.origin ?? id,
47
47
  root,
48
48
  renderFingerprint: bundle?.renderFingerprint ?? '',
49
49
  schemaRevision: bundle?.schemaRevision,
@@ -24,7 +24,7 @@ export async function handleDeploymentRoute(context: DomainRouteContext): Promis
24
24
  await instanceStatus(
25
25
  handle,
26
26
  target,
27
- bundle?.overlay.origin ?? null,
27
+ bundle?.ir?.domain ?? handle.origin ?? null,
28
28
  bundle?.schemaRevision ?? null,
29
29
  ),
30
30
  )
@@ -24,7 +24,7 @@ export async function handleViewRoute(context: DomainRouteContext): Promise<Resp
24
24
  const view = anatomy?.views.find((candidate) => candidate.slug === slug)
25
25
  if (!view) return notFound()
26
26
  const bundle = await getBundle(id)
27
- const origin = bundle?.overlay.origin || anatomy?.overview.origin
27
+ const origin = bundle?.ir?.domain || anatomy?.overview.origin
28
28
  if (!origin) return badRequest('domain origin is unavailable')
29
29
  return json(
30
30
  await getViewRuntime(root, origin, view, bundle, readSettings(root).viewProbeTimeoutMs),
@@ -39,7 +39,7 @@ export async function handleViewRoute(context: DomainRouteContext): Promise<Resp
39
39
  const view = anatomy?.views.find((candidate) => candidate.slug === slug)
40
40
  if (!view) return notFound()
41
41
  const bundle = await getBundle(id)
42
- const origin = bundle?.overlay.origin || anatomy?.overview.origin
42
+ const origin = bundle?.ir?.domain || anatomy?.overview.origin
43
43
  if (!origin) return badRequest('domain origin is unavailable')
44
44
  return json(
45
45
  await launchViewSession(
@@ -18,7 +18,7 @@ export async function handleWorkspaceRoute(
18
18
  const bundle = await getBundle(handle.id)
19
19
  return {
20
20
  id: handle.id,
21
- origin: bundle?.overlay.origin || handle.origin || handle.id,
21
+ origin: bundle?.ir?.domain || handle.origin || handle.id,
22
22
  path: handle.root,
23
23
  schemaDir: handle.schemaDirName,
24
24
  depsInstalled: depsInstalled(handle.root),
@@ -4,9 +4,9 @@ import { decodeBundleCacheEntry } from './cache'
4
4
 
5
5
  function entry(): Record<string, unknown> {
6
6
  return {
7
- version: 5,
7
+ version: 6,
8
8
  key: 'cache-key',
9
- futureEntryField: { version: 5 },
9
+ futureEntryField: { version: 6 },
10
10
  bundle: {
11
11
  domainId: 'notes',
12
12
  renderFingerprint: 'render-hash',
@@ -27,13 +27,8 @@ function entry(): Record<string, unknown> {
27
27
  core: {},
28
28
  },
29
29
  overlay: {
30
- origin: 'notes.example.dev',
31
- requires: [],
32
- crossDomainImports: [],
33
- mixins: [],
34
30
  handlerLinks: [],
35
31
  sourceSpans: {},
36
- annotations: [],
37
32
  },
38
33
  schemaRoot: {
39
34
  format: 'astrale.dsl',
@@ -51,7 +46,7 @@ test('bundle cache admits known structure while ignoring future fields', () => {
51
46
  const decoded = decodeBundleCacheEntry(entry())
52
47
 
53
48
  expect(decoded).toMatchObject({
54
- version: 5,
49
+ version: 6,
55
50
  key: 'cache-key',
56
51
  bundle: { domainId: 'notes', schemaMode: 'canonical-preview' },
57
52
  })
@@ -17,16 +17,15 @@ import { buildBundle } from './introspect/bundle'
17
17
  import { isCanonicalDomainSchemaV1 } from './introspect/canonical-schema'
18
18
  import { buildCore } from './introspect/core'
19
19
  import { decodeSchemaIR } from './introspect/schema-ir-json'
20
- import { asBoolean, asFiniteNumber, asJsonRecord, asString, asStringArray } from './json'
20
+ import { asBoolean, asFiniteNumber, asJsonRecord, asString } from './json'
21
21
  import { hashAnatomyFiles } from './state/baseline'
22
22
  import { readJson, writeJson } from './state/store'
23
23
 
24
24
  const bundles = new Map<string, StudioSchemaBundle>()
25
25
  const anatomies = new Map<string, Promise<DomainAnatomy>>()
26
- const cores = new Map<string, StudioCore>()
27
26
 
28
27
  const BUNDLE_CACHE_FILE = '.cache/schema-bundle.json'
29
- const BUNDLE_CACHE_VERSION = 5
28
+ const BUNDLE_CACHE_VERSION = 6
30
29
  const LOCKFILES = ['bun.lock', 'pnpm-lock.yaml', 'package-lock.json', 'yarn.lock']
31
30
  const TOOL_INPUTS = [
32
31
  'cache.ts',
@@ -37,7 +36,6 @@ const TOOL_INPUTS = [
37
36
  'introspect/canonical-schema.ts',
38
37
  'introspect/overlay.ts',
39
38
  'introspect/overlay-tsmorph.ts',
40
- 'introspect/source-overlay/annotations.ts',
41
39
  'introspect/source-overlay/handlers.ts',
42
40
  'introspect/source-overlay/kernel-calls.ts',
43
41
  'introspect/source-overlay/project.ts',
@@ -53,18 +51,6 @@ interface BundleCacheEntry {
53
51
  bundle: StudioSchemaBundle
54
52
  }
55
53
 
56
- function isCrossDomainImport(value: unknown): boolean {
57
- const record = asJsonRecord(value)
58
- const ref = asJsonRecord(record?.ref)
59
- return (
60
- typeof record?.name === 'string' &&
61
- typeof record.origin === 'string' &&
62
- ref?.kind === 'class' &&
63
- typeof ref.origin === 'string' &&
64
- typeof ref.name === 'string'
65
- )
66
- }
67
-
68
54
  function isHandlerLink(value: unknown): boolean {
69
55
  const record = asJsonRecord(value)
70
56
  return (
@@ -87,37 +73,15 @@ function isSourceSpan(value: unknown): boolean {
87
73
  )
88
74
  }
89
75
 
90
- function isAnnotation(value: unknown): boolean {
91
- const record = asJsonRecord(value)
92
- return (
93
- typeof record?.target === 'string' &&
94
- (record.severity === 'warn' || record.severity === 'info') &&
95
- (record.code === 'COMPILE_ERROR' || record.code === 'EDGE_PROP_TYPE_MISSING') &&
96
- typeof record.message === 'string'
97
- )
98
- }
99
-
100
76
  function decodeOverlay(value: unknown): SchemaOverlay | undefined {
101
77
  const record = asJsonRecord(value)
102
- const requires = asStringArray(record?.requires)
103
- const crossDomainImports = record?.crossDomainImports
104
- const mixins = record?.mixins
105
78
  const handlerLinks = record?.handlerLinks
106
79
  const sourceSpans = asJsonRecord(record?.sourceSpans)
107
- const annotations = record?.annotations
108
80
  if (
109
- typeof record?.origin !== 'string' ||
110
- !requires ||
111
- !Array.isArray(crossDomainImports) ||
112
- !crossDomainImports.every(isCrossDomainImport) ||
113
- !Array.isArray(mixins) ||
114
- !mixins.every(isCrossDomainImport) ||
115
81
  !Array.isArray(handlerLinks) ||
116
82
  !handlerLinks.every(isHandlerLink) ||
117
83
  !sourceSpans ||
118
- !Object.values(sourceSpans).every(isSourceSpan) ||
119
- !Array.isArray(annotations) ||
120
- !annotations.every(isAnnotation)
84
+ !Object.values(sourceSpans).every(isSourceSpan)
121
85
  ) {
122
86
  return undefined
123
87
  }
@@ -285,10 +249,7 @@ export async function getAnatomy(id: string, rebuild = false): Promise<DomainAna
285
249
  export async function getCore(id: string, rebuild = false): Promise<StudioCore | null> {
286
250
  const h = getDomain(id)
287
251
  if (!h) return null
288
- if (!rebuild && cores.has(id)) return cores.get(id)!
289
- const c = await buildCore(h)
290
- cores.set(id, c)
291
- return c
252
+ return buildCore(h, await getBundle(id, rebuild))
292
253
  }
293
254
 
294
255
  export function invalidate(id: string, what: 'schema' | 'anatomy' | 'all'): void {
@@ -298,6 +259,4 @@ export function invalidate(id: string, what: 'schema' | 'anatomy' | 'all'): void
298
259
  const domain = getDomain(id)
299
260
  if (domain) invalidateClientPackage(domain.root)
300
261
  }
301
- // Core is derived from the canonical Schema and belongs to the anatomy set.
302
- if (what !== 'schema') cores.delete(id)
303
262
  }
@@ -84,7 +84,7 @@ export function buildCopyMarkdown(parts: CopyParts): string {
84
84
  lines.push(`- **Render fingerprint:** \`${renderFingerprint}\``)
85
85
  lines.push('')
86
86
  lines.push(
87
- 'Load the **astrale-domain** skill, use the context/threads below, ' +
87
+ 'Load the **astrale-domain** skill and, for UI work, **astrale-frontend-design**. Use the context/threads below, ' +
88
88
  'read any listed context documents if relevant, edit code on disk, and reply by appending one `{role:"author"}` entry per open thread ' +
89
89
  '(merge by `id`). Resolve a thread by adding a closing note plus `"status":"closed"`; ' +
90
90
  'a thread whose last entry is not yours will be resent.',
@@ -1,108 +1,103 @@
1
+ import * as sdk from '@astrale-os/sdk/schema'
2
+ import {
3
+ core,
4
+ defineSchema,
5
+ edgeClass,
6
+ func,
7
+ method,
8
+ nodeClass,
9
+ output,
10
+ property,
11
+ valueSchema,
12
+ view,
13
+ } from '@astrale-os/sdk/schema'
1
14
  import { describe, expect, test } from 'bun:test'
2
15
 
3
16
  import {
4
- admitCanonicalSchemaFromSdk,
5
- closureFromSdk,
17
+ extractCanonicalSchemaFromSdk,
6
18
  findCanonicalDomainSchemaExport,
7
19
  isCanonicalDomainSchemaV1,
8
20
  projectCanonicalCore,
9
- projectCanonicalSchema,
10
21
  type CanonicalDomainSchemaV1,
11
22
  } from './canonical-schema'
12
23
 
13
- const localRef = { origin: 'docs.example.dev', kind: 'class', name: 'Document' } as const
14
- const baseRef = { origin: 'shared.example.dev', kind: 'class', name: 'Named' } as const
15
- const edgeRef = { origin: 'docs.example.dev', kind: 'class', name: 'links' } as const
16
-
17
- const dependency = {
18
- format: 'astrale.dsl',
19
- version: 'v1',
20
- origin: 'shared.example.dev',
21
- classes: {
22
- Named: {
23
- kind: 'node',
24
- properties: { title: { schema: { type: 'string' }, required: true } },
25
- methods: {},
26
- },
27
- },
28
- } satisfies CanonicalDomainSchemaV1
24
+ const string = valueSchema<string>()({ type: 'string' })
25
+ const boolean = valueSchema<boolean>()({ type: 'boolean' })
26
+ const rename = method({
27
+ input: valueSchema<{ title: string }>()({
28
+ type: 'object',
29
+ properties: { title: { type: 'string' } },
30
+ required: ['title'],
31
+ additionalProperties: false,
32
+ }),
33
+ output: boolean,
34
+ auth: 'authorized',
35
+ })
29
36
 
30
- const root = {
31
- format: 'astrale.dsl',
32
- version: 'v1',
33
- origin: 'docs.example.dev',
34
- dependencies: [{ origin: dependency.origin, revision: `sha256:${'1'.repeat(64)}` }],
35
- classes: {
36
- Document: {
37
- kind: 'node',
38
- extends: [baseRef],
39
- properties: {
40
- slug: { schema: { type: 'string', minLength: 1 }, required: true },
41
- },
42
- methods: {
43
- rename: {
44
- input: {
45
- type: 'object',
46
- properties: { title: { type: 'string' } },
47
- required: ['title'],
48
- },
49
- output: { mode: 'value', schema: { type: 'boolean' } },
50
- auth: 'authorized',
51
- static: false,
52
- inheritance: 'default',
53
- },
54
- },
55
- },
56
- links: {
57
- kind: 'edge',
58
- orientation: 'directed',
59
- endpoints: {
60
- source: { role: 'source', accepts: [localRef], outgoing: '1' },
61
- target: { role: 'target', accepts: [baseRef], incoming: '0..*' },
62
- },
63
- properties: {},
64
- methods: {},
65
- },
66
- },
37
+ const Named = nodeClass({ properties: { title: string } })
38
+ const dependency = defineSchema('shared.example.dev', { classes: { Named } })
39
+ const Document = nodeClass({
40
+ description: 'A document.',
41
+ icon: '<svg />',
42
+ extends: [Named],
43
+ properties: { slug: property(string, { description: 'Stable slug.' }) },
44
+ methods: { rename },
45
+ })
46
+ const links = edgeClass.directed({
47
+ source: { as: 'source', accepts: [Document], outgoing: '1' },
48
+ target: { as: 'target', accepts: [Named], incoming: '0..*' },
49
+ })
50
+ const primary = core.node(Document, { slug: 'primary', title: 'Primary' })
51
+ const primaryLink = core.edge(primary, links, primary, {})
52
+ const schema = defineSchema('docs.example.dev', {
53
+ dependencies: { shared: dependency },
54
+ classes: { Document, links },
67
55
  functions: {
68
- search: {
69
- input: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
70
- output: { mode: 'stream', item: { type: 'string' } },
71
- auth: 'authenticated',
72
- },
73
- },
74
- views: {
75
- documents: {
76
- target: { kind: 'definition', definitions: [localRef] },
77
- },
56
+ search: func({ input: string, output: output.stream(string), auth: 'authenticated' }),
78
57
  },
79
- core: {
80
- nodes: {
81
- primary: { class: localRef, properties: { slug: 'primary' } },
82
- },
83
- edges: [
84
- {
85
- class: edgeRef,
86
- source: { origin: 'docs.example.dev', kind: 'core', name: 'primary' },
87
- target: { kind: 'domain', origin: 'docs.example.dev' },
88
- },
89
- ],
90
- },
91
- } satisfies CanonicalDomainSchemaV1
58
+ views: { documents: view({ target: Document }) },
59
+ core: { nodes: { primary }, edges: [primaryLink] },
60
+ })
61
+ const namedRef = { origin: dependency.origin, kind: 'class', name: 'Named' } as const
62
+ const documentRef = { origin: schema.origin, kind: 'class', name: 'Document' } as const
63
+
64
+ const sdkModule = sdk
92
65
 
93
66
  describe('canonical Schema projection', () => {
94
- test('recognizes and selects only portable V1 schema roots', () => {
95
- expect(isCanonicalDomainSchemaV1(root)).toBe(true)
96
- expect(isCanonicalDomainSchemaV1({ ...root, version: 'v0' })).toBe(false)
97
- expect(findCanonicalDomainSchemaExport({ other: dependency, schema: root })).toBe(root)
67
+ test('recognizes exported V1 roots and preserves an invalid root as a preview', () => {
68
+ expect(isCanonicalDomainSchemaV1(schema)).toBe(true)
69
+ expect(findCanonicalDomainSchemaExport({ other: dependency, schema })).toBe(schema)
70
+
71
+ const invalid = {
72
+ format: 'astrale.dsl',
73
+ version: 'v1',
74
+ origin: 'not an origin',
75
+ dependencies: {},
76
+ classes: {},
77
+ functions: {},
78
+ policies: {},
79
+ views: {},
80
+ core: { nodes: {}, edges: [] },
81
+ } as CanonicalDomainSchemaV1
82
+ expect(extractCanonicalSchemaFromSdk(sdkModule, invalid)).toMatchObject({
83
+ status: 'preview',
84
+ revision: null,
85
+ ir: { domain: 'not an origin' },
86
+ })
98
87
  })
99
88
 
100
- test('projects Classes, exact inheritance/imports, callables, Views, and edges', () => {
101
- const { ir } = projectCanonicalSchema(root, [dependency])
102
- expect(ir.classes.Document).toMatchObject({
103
- extendsRefs: [baseRef],
104
- properties: { slug: { type: 'string', minLength: 1 } },
89
+ test('uses the resolved Domain for Classes, dependency footprint, callables, and Views', () => {
90
+ const extraction = extractCanonicalSchemaFromSdk(sdkModule, schema)
91
+ expect(extraction.status).toBe('admitted')
92
+ expect(extraction.revision).toMatch(/^sha256:[0-9a-f]{64}$/)
93
+ expect(extraction.ir.classes.Document).toMatchObject({
94
+ icon: '<svg />',
95
+ extendsRefs: [namedRef],
96
+ properties: { slug: { type: 'string' } },
105
97
  required: ['slug'],
98
+ propertyMetadata: {
99
+ slug: { required: true, visibility: 'public', description: 'Stable slug.' },
100
+ },
106
101
  methods: {
107
102
  rename: {
108
103
  input: { required: ['title'] },
@@ -111,69 +106,100 @@ describe('canonical Schema projection', () => {
111
106
  },
112
107
  },
113
108
  })
114
- expect(ir.importsByKey['shared.example.dev:class.Named']).toEqual({
115
- origin: baseRef.origin,
116
- ref: baseRef,
109
+ expect(extraction.ir.importsByKey['shared.example.dev:class.Named']).toEqual({
110
+ origin: namedRef.origin,
111
+ ref: namedRef,
117
112
  key: 'shared.example.dev:class.Named',
118
113
  })
119
- expect(ir.importedClassesByKey['shared.example.dev:class.Named']?.properties.title).toEqual({
120
- type: 'string',
114
+ expect(
115
+ extraction.ir.importedClassesByKey['shared.example.dev:class.Named']?.properties.title,
116
+ ).toEqual({ type: 'string' })
117
+ expect(extraction.ir.classes.links.endpoints).toEqual([
118
+ {
119
+ name: 'source',
120
+ types: ['Document'],
121
+ refs: [documentRef],
122
+ cardinality: { min: 1, max: 1 },
123
+ },
124
+ {
125
+ name: 'target',
126
+ types: ['Named'],
127
+ refs: [namedRef],
128
+ cardinality: { min: 0, max: null },
129
+ },
130
+ ])
131
+ expect(extraction.ir.functions.search).toMatchObject({
132
+ auth: 'authenticated',
133
+ output: { mode: 'stream' },
134
+ })
135
+ expect(extraction.ir.views.documents.target).toEqual({
136
+ kind: 'definition',
137
+ definitions: [documentRef],
121
138
  })
122
- expect(ir.classes.links.endpoints).toEqual([
123
- { name: 'source', types: ['Document'], refs: [localRef], cardinality: { min: 1, max: 1 } },
124
- { name: 'target', types: ['Named'], refs: [baseRef], cardinality: { min: 0, max: null } },
139
+ expect(extraction.ir.dependencies).toEqual([
140
+ { origin: dependency.origin, revision: sdk.schema.revision(dependency) },
125
141
  ])
126
- expect(ir.functions.search).toMatchObject({ auth: 'authenticated', output: { mode: 'stream' } })
127
- expect(ir.views?.documents.target).toEqual({ kind: 'definition', definitions: [localRef] })
128
- expect(ir.views?.documents).not.toHaveProperty('auth')
129
142
  })
130
143
 
131
- test('projects canonical Core coordinates without importing Application or Runtime', () => {
132
- expect(projectCanonicalCore(root)).toEqual({
133
- domain: root.origin,
144
+ test('projects Core from the same admitted root without a second Schema import', () => {
145
+ const extraction = extractCanonicalSchemaFromSdk(sdkModule, schema)
146
+ expect(projectCanonicalCore(extraction.root)).toEqual({
147
+ domain: schema.origin,
134
148
  nodes: [
135
149
  {
136
150
  path: '/:docs.example.dev:core.primary',
137
151
  className: 'Document',
138
- data: { slug: 'primary' },
152
+ data: {
153
+ 'docs.example.dev:class.Document.property.slug': 'primary',
154
+ 'shared.example.dev:class.Named.property.title': 'Primary',
155
+ },
139
156
  },
140
157
  ],
141
158
  edges: [
142
159
  {
143
160
  from: '/:docs.example.dev:core.primary',
144
- to: '/:docs.example.dev',
161
+ to: '/:docs.example.dev:core.primary',
145
162
  edgeName: 'links',
163
+ data: {},
146
164
  },
147
165
  ],
148
166
  })
149
167
  })
150
168
 
151
- test('uses the authored SDK for closure, admission, revision, and resolved-source proof', () => {
152
- const accepted = { ...root }
153
- const revision = `sha256:${'a'.repeat(64)}` as const
154
- const sdk = {
155
- bundle: { create: () => ({ root: accepted, closure: [dependency] }) },
169
+ test('delegates admission, resolution, and dependency reachability to the DSL', () => {
170
+ const calls: string[] = []
171
+ const wrapped = {
172
+ ...sdk,
173
+ bundle: {
174
+ ...sdk.bundle,
175
+ create(value: typeof schema) {
176
+ calls.push('bundle.create')
177
+ return sdk.bundle.create(value)
178
+ },
179
+ accept(value: unknown) {
180
+ calls.push('bundle.accept')
181
+ return sdk.bundle.accept(value)
182
+ },
183
+ },
156
184
  schema: {
157
- accept: (candidate: unknown) => candidate,
158
- revision: () => revision,
159
- resolve: (candidate: unknown) => ({ source: candidate, origin: root.origin }),
185
+ ...sdk.schema,
186
+ resolve(value: typeof schema) {
187
+ calls.push('schema.resolve')
188
+ return sdk.schema.resolve(value)
189
+ },
190
+ compareDependencyMeaning(source: typeof schema, target: typeof dependency) {
191
+ calls.push('schema.compareDependencyMeaning')
192
+ return sdk.schema.compareDependencyMeaning(source, target)
193
+ },
160
194
  },
161
- }
162
- expect(closureFromSdk(sdk, accepted)).toEqual([dependency])
163
- expect(admitCanonicalSchemaFromSdk(sdk, accepted)).toEqual({
164
- status: 'admitted',
165
- root: accepted,
166
- closure: [dependency],
167
- revision,
168
- })
169
- })
195
+ } as unknown as typeof sdk
170
196
 
171
- test('fails to preview when the SDK cannot prove admission', () => {
172
- expect(admitCanonicalSchemaFromSdk({}, root)).toEqual({
173
- status: 'preview',
174
- root,
175
- closure: [],
176
- revision: null,
177
- })
197
+ expect(extractCanonicalSchemaFromSdk(wrapped, schema).status).toBe('admitted')
198
+ expect(calls).toEqual([
199
+ 'bundle.create',
200
+ 'bundle.accept',
201
+ 'schema.resolve',
202
+ 'schema.compareDependencyMeaning',
203
+ ])
178
204
  })
179
205
  })