@astrale-os/sdk 0.1.9 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/auth/index.d.ts +1 -1
  2. package/dist/auth/index.d.ts.map +1 -1
  3. package/dist/auth/index.js +1 -1
  4. package/dist/auth/index.js.map +1 -1
  5. package/dist/cli/run.d.ts +0 -1
  6. package/dist/cli/run.d.ts.map +1 -1
  7. package/dist/cli/run.js +21 -10
  8. package/dist/cli/run.js.map +1 -1
  9. package/dist/config/adapter.d.ts +19 -11
  10. package/dist/config/adapter.d.ts.map +1 -1
  11. package/dist/config/adapter.js.map +1 -1
  12. package/dist/config/define-domain.d.ts +13 -29
  13. package/dist/config/define-domain.d.ts.map +1 -1
  14. package/dist/config/define-domain.js +22 -33
  15. package/dist/config/define-domain.js.map +1 -1
  16. package/dist/config/deploy.d.ts +1 -1
  17. package/dist/config/deploy.js +1 -1
  18. package/dist/config/index.d.ts +1 -1
  19. package/dist/config/index.d.ts.map +1 -1
  20. package/dist/define/remote-function.d.ts +9 -14
  21. package/dist/define/remote-function.d.ts.map +1 -1
  22. package/dist/define/remote-function.js +9 -8
  23. package/dist/define/remote-function.js.map +1 -1
  24. package/dist/define/view.d.ts +14 -6
  25. package/dist/define/view.d.ts.map +1 -1
  26. package/dist/define/view.js +8 -6
  27. package/dist/define/view.js.map +1 -1
  28. package/dist/dispatch/identity.d.ts +10 -9
  29. package/dist/dispatch/identity.d.ts.map +1 -1
  30. package/dist/dispatch/identity.js +9 -15
  31. package/dist/dispatch/identity.js.map +1 -1
  32. package/dist/domain/binding.d.ts +18 -0
  33. package/dist/domain/binding.d.ts.map +1 -0
  34. package/dist/domain/binding.js +29 -0
  35. package/dist/domain/binding.js.map +1 -0
  36. package/dist/domain/build-spec.d.ts.map +1 -1
  37. package/dist/domain/build-spec.js +10 -7
  38. package/dist/domain/build-spec.js.map +1 -1
  39. package/dist/domain/define.d.ts +27 -27
  40. package/dist/domain/define.d.ts.map +1 -1
  41. package/dist/domain/define.js +31 -49
  42. package/dist/domain/define.js.map +1 -1
  43. package/dist/domain/extend-core.d.ts.map +1 -1
  44. package/dist/domain/extend-core.js +9 -1
  45. package/dist/domain/extend-core.js.map +1 -1
  46. package/dist/domain/extend-functions.d.ts +44 -0
  47. package/dist/domain/extend-functions.d.ts.map +1 -0
  48. package/dist/domain/extend-functions.js +69 -0
  49. package/dist/domain/extend-functions.js.map +1 -0
  50. package/dist/domain/extend-views.d.ts +45 -0
  51. package/dist/domain/extend-views.d.ts.map +1 -0
  52. package/dist/domain/extend-views.js +116 -0
  53. package/dist/domain/extend-views.js.map +1 -0
  54. package/dist/index.d.ts +2 -2
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +1 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/server/auxiliary-routes.js +1 -1
  59. package/dist/server/auxiliary-routes.js.map +1 -1
  60. package/dist/server/worker-entry.d.ts +3 -3
  61. package/dist/server/worker-entry.js +3 -3
  62. package/package.json +2 -5
  63. package/src/auth/index.ts +1 -1
  64. package/src/cli/run.ts +21 -12
  65. package/src/config/adapter.ts +16 -11
  66. package/src/config/define-domain.ts +34 -56
  67. package/src/config/deploy.ts +1 -1
  68. package/src/config/index.ts +1 -1
  69. package/src/define/remote-function.ts +9 -14
  70. package/src/define/view.ts +14 -6
  71. package/src/dispatch/identity.ts +15 -21
  72. package/src/domain/binding.ts +37 -0
  73. package/src/domain/build-spec.ts +18 -7
  74. package/src/domain/define.ts +67 -62
  75. package/src/domain/extend-functions.ts +86 -0
  76. package/src/domain/extend-views.ts +151 -0
  77. package/src/index.ts +7 -2
  78. package/src/server/auxiliary-routes.ts +1 -1
  79. package/src/server/worker-entry.ts +3 -3
  80. package/src/domain/extend-core.ts +0 -287
@@ -32,17 +32,18 @@ export interface DomainInfo {
32
32
  /** Optional Astrale Path called by the kernel after install (as __SYSTEM__). */
33
33
  postInstall?: string
34
34
  /**
35
- * Whether the domain declares any Views / standalone Functions / a client SPA
36
- * — read from the `defineDomain` definition, NOT probed from the filesystem.
37
- * The adapter codegen imports the corresponding module / mounts the SPA hook
38
- * only when true. `hasClient` governs the WORKER's `/ui` hook specifically
39
- * (present whenever the domain has a client, even on managed deploys that ship
40
- * the built assets separately and pass no `clientDir`); the wrangler
41
- * `assets.directory` binding tracks `clientDir` instead.
35
+ * Whether the domain declares Views / standalone Functions read from the
36
+ * `defineDomain` definition, NOT probed from the filesystem. The adapter
37
+ * codegen imports the corresponding module only when true.
42
38
  */
43
39
  hasViews: boolean
44
40
  hasFunctions: boolean
45
- hasClient: boolean
41
+ /**
42
+ * Views whose binding is backed by a worker-relative SPA mount. This is domain
43
+ * contract metadata (`defineView({ mount })`), not a frontend source folder:
44
+ * adapters decide, per env, which client assets serve these mounts.
45
+ */
46
+ mountedViews: Array<{ slug: string; mount: string }>
46
47
  /**
47
48
  * Whether the domain declares a `deps` mapper (`defineDomain({ deps })`).
48
49
  * When true the adapter codegen imports it from the domain's fixed `deps`
@@ -58,8 +59,6 @@ export interface WatchCtx {
58
59
  projectDir: string
59
60
  /** Absolute path where the CLI writes the diagnostic `spec.json`. */
60
61
  specPath: string
61
- /** Absolute path to the client SPA dir, when the domain declares a `client`. */
62
- clientDir?: string
63
62
  /** Flat secret map loaded from the env's `secrets` file — injected locally in dev. */
64
63
  secrets: Record<string, string>
65
64
  /** Domain metadata for codegen. */
@@ -75,10 +74,16 @@ export interface DeployCtx {
75
74
  specPath: string
76
75
  /** Flat secret map loaded from the env's `secrets` file (gitignored). */
77
76
  secrets: Record<string, string>
78
- clientDir?: string
79
77
  domain: DomainInfo
80
78
  /** The env key being deployed (e.g. 'dev' | 'prod' | 'canary'). */
81
79
  env: string
80
+ /**
81
+ * Build the expected install-graph hash for a concrete serving URL from the
82
+ * current domain definition. Adapters that must verify live drift during
83
+ * deploy should use this instead of reading `.astrale/spec.json`, which is a
84
+ * post-deploy diagnostic artifact and may be stale.
85
+ */
86
+ schemaHashForUrl?(url: string): Promise<string | undefined>
82
87
  }
83
88
 
84
89
  /** A running local watch — exposes its URL and a stop handle. */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `defineDomain` — the WORKER-SAFE definition of a domain: what the domain *is*
3
- * (its `schema`, `methods`, `deps`, `views`, standalone `functions`, `client`
4
- * SPA) plus its addressing identity (`origin`, `requires`, `postInstall`). It
3
+ * (its `schema`, `methods`, `deps`, `views`, standalone `functions`) plus its
4
+ * addressing identity (`origin`, `requires`, `postInstall`). It
5
5
  * deliberately carries NO deployment adapter — the adapter (`cloudflare(...)`,
6
6
  * `astrale(...)`) is node-only code (filesystem, wrangler) that must never enter
7
7
  * the worker bundle. The author wires this in a `domain.ts` the generated worker
@@ -11,14 +11,14 @@
11
11
  * The modules are wired EXPLICITLY here — imported and passed in — not
12
12
  * discovered from magic folder names. A renamed or mistyped module is a compile
13
13
  * error at this call site, never a silently-missing worker route. The adapter
14
- * reads this one definition for everything it codegens; there is no second
15
- * filesystem probe to drift from it. `defineDomain` itself builds no server and
16
- * boots no kernel — it validates and packages the declaration.
14
+ * reads this one definition for domain-side codegen; frontend source folders
15
+ * live in adapter env config. `defineDomain` itself builds no server and boots
16
+ * no kernel — it validates and packages the declaration.
17
17
  */
18
18
 
19
19
  import type { Schema } from '@astrale-os/kernel-dsl'
20
20
 
21
- import { DomainOrigin, extractDomainSlug } from '@astrale-os/kernel-core/domain'
21
+ import { DomainOrigin } from '@astrale-os/kernel-core/domain'
22
22
 
23
23
  import type { RemoteFunctionDef, ViewDef } from '../define'
24
24
  import type { SchemaMethodsImpl } from '../method'
@@ -33,15 +33,6 @@ type AnyViewDef = ViewDef<any>
33
33
  type AnyFunctionDef = RemoteFunctionDef<any, any, any>
34
34
  // oxlint-enable no-explicit-any
35
35
 
36
- /**
37
- * The domain's client SPA binding. Its presence is the whole signal — there is
38
- * no `existsSync('client/')` probe. `dir` is the project-relative source folder
39
- * (e.g. `'client'`); the worker serves its built SPA under `/ui` via its Assets
40
- * binding. Always written out explicitly (`client: { dir: 'client' }`) — there
41
- * is no boolean shorthand, so the source folder is never implicit.
42
- */
43
- export type ClientBinding = { dir: string }
44
-
45
36
  export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
46
37
  /** The domain schema (from `schema/`). Its `.domain` seeds the default origin. */
47
38
  schema: S
@@ -76,12 +67,6 @@ export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
76
67
  * by slug. Omit when the domain has none.
77
68
  */
78
69
  functions?: Record<string, AnyFunctionDef>
79
- /**
80
- * The domain's client SPA, e.g. `{ dir: 'client' }`. Its presence enables it
81
- * (no folder probing); `dir` is the project-relative source folder, built and
82
- * served under `/ui`. Omit for a domain with no SPA.
83
- */
84
- client?: ClientBinding
85
70
  /**
86
71
  * The domain's **addressing name** (the graph slug it mounts under, e.g.
87
72
  * `'crm.acme.dev'`). Defaults to `schema.domain`. Must be a name, never a
@@ -94,11 +79,15 @@ export interface DefineDomainConfig<S extends Schema, TDeps, TEnv = unknown> {
94
79
  /** Cross-domain deps, by origin. Verified present on the instance at install. */
95
80
  requires?: readonly string[]
96
81
  /**
97
- * Astrale Path (usually an AbsolutePath to a `functions/` entry) the kernel
98
- * calls once after install, as __SYSTEM__ where the domain posts its own
99
- * grants / seed.
82
+ * The function the kernel runs once after install, as __SYSTEM__ — where the
83
+ * domain seeds itself / posts its own grants. Reference it from the `functions`
84
+ * map: `postInstall: functions.seed`. The SDK derives its path by identity, so a
85
+ * typo or a renamed key is a compile error here, never a stale string. It is
86
+ * always a standalone function (a domain bootstrap belongs to the domain, not to
87
+ * a class) under THIS domain — you never write the origin, and the kernel
88
+ * resolves it relative to wherever the domain is installed.
100
89
  */
101
- postInstall?: string
90
+ postInstall?: AnyFunctionDef
102
91
  }
103
92
 
104
93
  export interface DomainDefinition {
@@ -114,8 +103,6 @@ export interface DomainDefinition {
114
103
  deps?: (env: any, url: string) => any
115
104
  views?: Record<string, ViewDef>
116
105
  functions?: Record<string, AnyFunctionDef>
117
- /** Normalized client binding (resolved `dir`), or absent when the domain has no SPA. */
118
- client?: { dir: string }
119
106
  origin: string
120
107
  requires: readonly string[]
121
108
  postInstall?: string
@@ -170,46 +157,37 @@ export function defineDomain<S extends Schema, TDeps, TEnv = unknown>(
170
157
  ...(config.deps ? { deps: config.deps as DomainDefinition['deps'] } : {}),
171
158
  ...(config.views ? { views: config.views as Record<string, ViewDef> } : {}),
172
159
  ...(config.functions ? { functions: config.functions } : {}),
173
- ...(config.client ? { client: config.client } : {}),
174
160
  origin,
175
161
  requires,
176
- ...(config.postInstall
177
- ? { postInstall: normalizePostInstall(config.postInstall, origin) }
162
+ ...(config.postInstall !== undefined
163
+ ? { postInstall: normalizePostInstall(config.postInstall, origin, config.functions) }
178
164
  : {}),
179
165
  }
180
166
  }
181
167
 
182
168
  /**
183
- * Validate a `postInstall` hook path and align its leading origin segment with
184
- * the canonical (lowercased) origin. Only the typed colon forms are accepted
185
- * (`/:<origin>:class.X:seed`, `/:<origin>:interface.Ops:seed`) mirroring the
186
- * kernel's own origin guard, which refuses absolute tree paths because they
187
- * cannot prove their origin from the string alone. The hook is often authored
188
- * as `` `/:${schema.domain}:…` `` where `schema.domain` keeps its source
189
- * casing, but the kernel stores graph nodes (and runs its guard) under the
190
- * lowercased origin — so the origin segment is re-stamped canonical. Rejects a
191
- * tree path or one pointing at a different domain (the kernel calls the hook
192
- * as __SYSTEM__ and refuses a foreign target).
169
+ * Resolve a `postInstall` function reference to the colon-path the bundle carries.
170
+ * The slug is the `functions` map key the reference is registered under (found by
171
+ * identity), so a renamed key is a compile error at the reference site AND the
172
+ * derived path follows the rename. The origin is never the author's to supply
173
+ * (postInstall is always a standalone function of THIS domain) and the path is
174
+ * mount-agnostic the kernel resolves it wherever the domain is installed.
193
175
  */
194
- function normalizePostInstall(postInstall: string, origin: string): string {
195
- const slug = extractDomainSlug(postInstall)
196
- if (slug === null) {
197
- throw new Error(
198
- `defineDomain: \`postInstall\` must be a typed colon-path under "/:${origin}" ` +
199
- `(e.g. "/:${origin}:class.Note:seed" or "/:${origin}:interface.Ops:seed"); ` +
200
- `absolute tree paths are not accepted — got "${postInstall}".`,
201
- )
202
- }
203
- if (slug.toLowerCase() !== origin) {
176
+ function normalizePostInstall(
177
+ postInstall: AnyFunctionDef,
178
+ origin: string,
179
+ functions: Record<string, AnyFunctionDef> | undefined,
180
+ ): string {
181
+ const slug = functions
182
+ ? Object.entries(functions).find(([, def]) => def === postInstall)?.[0]
183
+ : undefined
184
+ if (slug === undefined) {
204
185
  throw new Error(
205
- `defineDomain: \`postInstall\` "${postInstall}" must resolve under the domain origin ` +
206
- `"/${origin}" — the kernel calls it as __SYSTEM__ and refuses a hook pointing at another domain.`,
186
+ "defineDomain: `postInstall` must reference a function from this domain's `functions` map " +
187
+ '(e.g. `postInstall: functions.seed`).',
207
188
  )
208
189
  }
209
- // Re-stamp the origin segment with its canonical (lowercased) form.
210
- return postInstall.startsWith('/:')
211
- ? `/:${origin}${postInstall.slice(2 + slug.length)}`
212
- : `/${origin}${postInstall.slice(1 + slug.length)}`
190
+ return `/:${origin}:function.${slug}`
213
191
  }
214
192
 
215
193
  function schemaDomain(schema: Schema): string | undefined {
@@ -9,7 +9,7 @@
9
9
  * is authored in `astrale.config.ts` — a Node-only module the CLI loads but the
10
10
  * worker never imports — so the adapter stays out of the bundle.
11
11
  *
12
- * export const domain = defineDomain({ schema, methods, deps, views, client })
12
+ * export const domain = defineDomain({ schema, methods, deps, views })
13
13
  *
14
14
  * import { domain } from './domain'
15
15
  * export default deploy(domain, cloudflare({ dev, prod }))
@@ -14,7 +14,7 @@
14
14
  */
15
15
 
16
16
  export { defineDomain } from './define-domain'
17
- export type { ClientBinding, DefineDomainConfig, DomainDefinition } from './define-domain'
17
+ export type { DefineDomainConfig, DomainDefinition } from './define-domain'
18
18
 
19
19
  export { deploy } from './deploy'
20
20
  export type { DeployConfig } from './deploy'
@@ -4,16 +4,17 @@
4
4
  * the canonical kernel `Function` class, the former distribution `RemoteFunction`.)
5
5
  *
6
6
  * Each entry in `defineRemoteDomain({ remoteFunctions: { ... } })` becomes:
7
- * - a graph node at `/${origin}/core/<functionsFolder>/<slug>`, materialized
8
- * as the kernel `Function` class by `buildCorePath` so kernel discovery /
9
- * `View.resolve` can list it. The `core/` anchor appears in the GRAPH path only.
7
+ * - a graph node at `/${origin}/functions/<slug>` — a first-class domain MEMBER,
8
+ * materialized as the kernel `Function` class and attached to the Domain via an
9
+ * `of_domain` edge (slug `function.<slug>`), so it is addressable by the
10
+ * semantic path `/:${origin}:function.<slug>` (the form a `postInstall` uses).
10
11
  * - a Hono route on the worker at the path implied by `binding`
11
- * (`/<functionsFolder>/<slug>` POST by default — no `core/` in the URL).
12
+ * (`/<functionsFolder>/<slug>` POST by default — the route URL is decoupled
13
+ * from the graph layout).
12
14
  *
13
- * The slug = the map key (single source of truth, no duplication).
14
- *
15
- * `ref` is auto-derived as `function.<slug>` if omittedused as
16
- * `Function.ref` on the graph node and as the dispatch key.
15
+ * The slug = the map key (single source of truth, no duplication). The member
16
+ * ref (`function.<slug>`), the layout (`/<origin>/functions/<slug>`), and the
17
+ * `of_domain` edge slug are all DERIVED from it there is nothing else to name.
17
18
  */
18
19
 
19
20
  import type { AuthPolicy, FunctionBinding } from '@astrale-os/kernel-api/routed'
@@ -77,12 +78,6 @@ export type RemoteFunctionDef<
77
78
  TDeps = unknown,
78
79
  TAuth extends AuthPolicy = 'required',
79
80
  > = {
80
- /**
81
- * Canonical callable identity. Auto-derived as `function.<slug>` (where
82
- * `<slug>` is the map key) when omitted. Stored as `Function.ref` on the
83
- * graph node and used by the kernel dispatcher to route the call.
84
- */
85
- ref?: string
86
81
  /** Zod schema for the call's parameters. */
87
82
  inputSchema: z.ZodType<TParams>
88
83
  /** Zod schema for the call's result. */
@@ -2,14 +2,16 @@
2
2
  * Authoring a `View` — iframe-mountable callable served by the domain worker.
3
3
  *
4
4
  * Each entry in `defineRemoteDomain({ views: { ... } })` becomes both:
5
- * - a graph node at `/${origin}/core/<viewsFolder>/<slug>` (auto-materialized
6
- * by the SDK; the `core/` segment is the universal anchor injected by
7
- * `buildCorePath`. `<slug>` is the map key, so it lives in exactly one place)
5
+ * - a first-class domain MEMBER: a `View` node at `/${origin}/views/<slug>`
6
+ * attached to the Domain via an `of_domain` edge (slug `view.<slug>`),
7
+ * addressable as `/:${origin}:view.<slug>`. `<slug>` is the map key, so it
8
+ * lives in exactly one place.
8
9
  * - a Hono route on the worker at the path implied by `binding`
9
- * (`/<viewsFolder>/<slug>` by default — note: no `core/` in the URL).
10
+ * (`/<viewsFolder>/<slug>` by default).
10
11
  *
11
- * The `core/` segment appears in the GRAPH path only; the URL path that
12
- * lands in `Function.binding.remoteUrl` is `${url}/<viewsFolder>/<slug>`.
12
+ * The graph layout (`/<origin>/views/<slug>`) is a fixed kernel convention; the
13
+ * URL path that lands in `Function.binding.remoteUrl` is
14
+ * `${url}/<viewsFolder>/<slug>` — DECOUPLED from the graph layout.
13
15
  *
14
16
  * The author can override the URL via `binding` — host and/or path
15
17
  * placeholders are supported (the kernel's `route` mechanism does the
@@ -72,6 +74,12 @@ export type ViewDef<TDeps = unknown, TAuth extends AuthPolicy = AuthPolicy> = {
72
74
  * with `render`; takes precedence over `binding`.
73
75
  */
74
76
  mount?: string
77
+ /**
78
+ * Shell handshake mode for the mounted iframe. Omit for the common case:
79
+ * SPA-mounted views (`mount`) use the shell handshake; inline `render` views
80
+ * default to `'none'` because they do not include the shell client.
81
+ */
82
+ handshake?: 'shell' | 'none'
75
83
  /** Authentication policy. Defaults to `'required'`. */
76
84
  auth?: TAuth
77
85
  /**
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * Per-callable identity — dispatcher runtime + install-time wiring.
3
3
  *
4
- * Two flavors of callable get an identity per the install-time identity
5
- * binding: Methods on classes/interfaces (sub = MethodPath) and auto-
6
- * materialized core function nodes RemoteFunctions and Views (sub =
7
- * AbsolutePath of the node under `core/{functions,views}/<slug>`).
4
+ * Three flavors of callable get an identity per the install-time identity
5
+ * binding: Methods on classes/interfaces (sub = MethodPath), standalone-function
6
+ * MEMBERS (sub = AbsolutePath at `/<origin>/functions/<slug>`), and view MEMBERS
7
+ * (sub = AbsolutePath at `/<origin>/views/<slug>`).
8
8
  *
9
9
  * At server startup the method dispatcher and the View / RemoteFunction
10
10
  * route mounter pre-compute, for every materialized callable, the
@@ -54,11 +54,12 @@ export type AuxBuckets<T> = {
54
54
  }
55
55
 
56
56
  /**
57
- * slug → `AbsolutePath.raw` for each auto-materialized aux node
58
- * (RemoteFunction / View). These nodes live at
59
- * `/<origin>/core/{functions,views}/<slug>` and have no class+method
60
- * decomposition that would justify a MethodPath — their identity is their
61
- * graph position.
57
+ * slug → `AbsolutePath.raw` for each aux callable MEMBER node:
58
+ * - standalone-function members (`kind: 'function'`) at `/<origin>/functions/<slug>`;
59
+ * - view members (`kind: 'view'`) at `/<origin>/views/<slug>` (plus any
60
+ * hand-authored/legacy core `View` nodes, `kind: 'core'` className `View`).
61
+ * Neither has a class+method decomposition that would justify a MethodPath —
62
+ * their identity is their graph position.
62
63
  */
63
64
  export type AuxIdentityPaths = AuxBuckets<string>
64
65
 
@@ -66,20 +67,13 @@ export function collectAuxIdentityPaths(compiled: CompiledDomain): AuxIdentityPa
66
67
  const views: Record<string, string> = {}
67
68
  const remoteFunctions: Record<string, string> = {}
68
69
  for (const c of resolveCallables(compiled)) {
69
- if (c.kind !== 'core' || !c.slug) continue
70
- if (c.className === 'View') {
71
- views[c.slug] = c.sub
72
- } else if (c.className === 'Function') {
73
- // Standalone callables (the former `RemoteFunction`) materialize as the
74
- // canonical kernel `Function` class.
70
+ if (!c.slug) continue
71
+ if (c.kind === 'function') {
75
72
  remoteFunctions[c.slug] = c.sub
76
- } else {
77
- throw new Error(
78
- `collectAuxIdentityPaths: unrecognised aux callable className "${c.className}" ` +
79
- `at ${c.ref}. If you've added a new auto-materialized Function class to extendCore, ` +
80
- `extend this collector + the kernel walker (resolveCallables) to handle it.`,
81
- )
73
+ } else if (c.kind === 'view' || (c.kind === 'core' && c.className === 'View')) {
74
+ views[c.slug] = c.sub
82
75
  }
76
+ // Methods (`kind: 'method'`) get their identity via `collectMethodPaths`.
83
77
  }
84
78
  return { views, remoteFunctions }
85
79
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Shared binding helpers for auto-materialized members (functions + views).
3
+ *
4
+ * The worker ROUTE URL a member is served at — `<url>/<folderSlug>/<slug>` — is
5
+ * decoupled from the member's GRAPH layout (`/<origin>/{functions,views}/<slug>`,
6
+ * a fixed kernel convention). `folderSlug` names only the URL segment.
7
+ */
8
+
9
+ import type { FunctionBinding } from '@astrale-os/kernel-api/routed'
10
+
11
+ /** Join a worker-relative mount path onto the serving url (single slash). */
12
+ export function joinWorkerPath(url: string, mount: string): string {
13
+ const base = url.replace(/\/+$/, '')
14
+ return `${base}/${mount.replace(/^\/+/, '')}`
15
+ }
16
+
17
+ /**
18
+ * Resolve a member's effective binding against the serving `url`. An explicit
19
+ * `override.remoteUrl` wins; otherwise defaults to `<url>/<folderSlug>/<slug>`.
20
+ * Same trailing-slash discipline as {@link joinWorkerPath} — a `url` ending in
21
+ * `/` must not produce `//` (the kernel pins `iss` by exact string).
22
+ */
23
+ export function resolveBinding(
24
+ override: FunctionBinding | undefined,
25
+ url: string,
26
+ folderSlug: string,
27
+ slug: string,
28
+ ): FunctionBinding {
29
+ const base = url.replace(/\/+$/, '')
30
+ if (override) {
31
+ return {
32
+ ...override,
33
+ remoteUrl: override.remoteUrl ?? `${base}/${folderSlug}/${slug}`,
34
+ }
35
+ }
36
+ return { remoteUrl: `${base}/${folderSlug}/${slug}` }
37
+ }
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import type { Graph, WireGraph } from '@astrale-os/kernel-core'
21
- import type { BoundMethod, FunctionSchema } from '@astrale-os/kernel-core/domain'
21
+ import type { BoundMethod, FunctionSchema, ViewSchema } from '@astrale-os/kernel-core/domain'
22
22
  import type { Schema } from '@astrale-os/kernel-dsl'
23
23
 
24
24
  import { hashInstallGraph, serialize, zodToJsonSchema } from '@astrale-os/kernel-core/domain'
@@ -41,8 +41,14 @@ export function buildInstallGraph<S extends Schema>(
41
41
  domain: RemoteDomain<S>,
42
42
  url: string,
43
43
  ): WireGraph {
44
- const { compiled } = materializeRemoteDomain(domain, url)
45
- return buildSpecInternal(compiled, domain.methods, url).toWire() as WireGraph
44
+ const { compiled, auxiliary } = materializeRemoteDomain(domain, url)
45
+ return buildSpecInternal(
46
+ compiled,
47
+ domain.methods,
48
+ url,
49
+ auxiliary?.functionSchemas ?? [],
50
+ auxiliary?.viewSchemas ?? [],
51
+ ).toWire() as WireGraph
46
52
  }
47
53
 
48
54
  /**
@@ -63,12 +69,17 @@ function buildSpecInternal(
63
69
  compiled: RemoteDomain['compiled'],
64
70
  methods: BoundMethod<AnyRemoteHandler>[],
65
71
  url: string,
72
+ functionSchemas: FunctionSchema[],
73
+ viewSchemas: ViewSchema[],
66
74
  ): Graph {
67
75
  const serialized = serializeMethodsWithStubs(compiled, methods, url)
68
- const tree = serialize(compiled, serialized)
69
- // Views / RemoteFunctions are auto-materialized into the Core BEFORE
70
- // `compileDomain` runs (see `extend-core.ts`). By the time we reach
71
- // `serialize` here the Tree already contains their nodes/edges.
76
+ // Method impls + standalone-function-member impls travel in the same callable
77
+ // array (distinct refs: `class.X.method.y` vs `function.<slug>`); view-member
78
+ // impls travel in `options.views` (a view differs structurally — handshake,
79
+ // view_for, View class). The serializer emits method nodes from the IR,
80
+ // function/view members from `compiled.$.refs.{functions,views}`, pulling each
81
+ // impl by ref.
82
+ const tree = serialize(compiled, [...serialized, ...functionSchemas], { views: viewSchemas })
72
83
  return tree.toGraph()
73
84
  }
74
85