@astrale-os/adapter-cloudflare 0.3.1 → 0.4.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 (63) hide show
  1. package/LICENSE +202 -0
  2. package/package.json +4 -4
  3. package/template/.agents/skills/astrale-cli/SKILL.md +633 -0
  4. package/template/.domain-studio/comments.json +4 -0
  5. package/template/.env.example +3 -6
  6. package/template/CLAUDE.md +8 -6
  7. package/template/README.md +28 -20
  8. package/template/client/__tests__/app.test.tsx +39 -248
  9. package/template/client/__tests__/harness.ts +0 -47
  10. package/template/client/__tests__/shell.test.ts +46 -0
  11. package/template/client/package.json +1 -1
  12. package/template/client/src/app.tsx +6 -8
  13. package/template/client/src/shell/index.ts +1 -0
  14. package/template/client/src/shell/transformers.ts +36 -33
  15. package/template/client/src/shell/use-node.ts +21 -13
  16. package/template/client/src/ui/index.ts +4 -7
  17. package/template/core/README.md +23 -0
  18. package/template/deps.ts +10 -9
  19. package/template/domain.ts +11 -9
  20. package/template/env.ts +3 -5
  21. package/template/functions/index.ts +14 -37
  22. package/template/icons.ts +25 -0
  23. package/template/integrations/README.md +22 -0
  24. package/template/package.json +3 -3
  25. package/template/runtime/index.ts +25 -72
  26. package/template/schema/index.ts +10 -10
  27. package/template/tsconfig.json +1 -0
  28. package/template/views/index.ts +15 -12
  29. package/template/client/__tests__/kernel.test.ts +0 -77
  30. package/template/client/__tests__/seam.test.tsx +0 -115
  31. package/template/client/src/status/components/StatusCard.tsx +0 -160
  32. package/template/client/src/status/components/index.ts +0 -1
  33. package/template/client/src/status/hooks/index.ts +0 -3
  34. package/template/client/src/status/hooks/useCheck.mutation.ts +0 -16
  35. package/template/client/src/status/hooks/useCheckable.query.ts +0 -72
  36. package/template/client/src/status/index.ts +0 -7
  37. package/template/client/src/status/status.api.ts +0 -29
  38. package/template/client/src/status/status.mappers.ts +0 -102
  39. package/template/client/src/status/status.types.ts +0 -26
  40. package/template/client/src/ui/StatusBadge.tsx +0 -31
  41. package/template/client/src/views/status.tsx +0 -28
  42. package/template/core/monitor/health.ts +0 -34
  43. package/template/core/monitor/index.ts +0 -9
  44. package/template/core/monitor/keys.ts +0 -41
  45. package/template/core/monitor/node.ts +0 -63
  46. package/template/integrations/prober/http.ts +0 -32
  47. package/template/integrations/prober/mock.ts +0 -18
  48. package/template/integrations/prober/port.ts +0 -26
  49. package/template/integrations/prober/registry.ts +0 -65
  50. package/template/pnpm-lock.yaml +0 -2780
  51. package/template/runtime/monitoring/index.ts +0 -8
  52. package/template/runtime/monitoring/monitor/check.ts +0 -29
  53. package/template/runtime/monitoring/monitor/index.ts +0 -10
  54. package/template/runtime/monitoring/monitor/seed.ts +0 -104
  55. package/template/runtime/monitoring/monitor/watch.ts +0 -31
  56. package/template/runtime/monitoring/page/add.ts +0 -21
  57. package/template/runtime/monitoring/page/check.ts +0 -50
  58. package/template/runtime/monitoring/page/create.ts +0 -24
  59. package/template/runtime/monitoring/page/index.ts +0 -9
  60. package/template/runtime/shared.ts +0 -21
  61. package/template/schema/monitor.ts +0 -92
  62. package/template/views/status-page.ts +0 -16
  63. package/template/views/welcome.ts +0 -35
@@ -4,6 +4,12 @@ import { useCallback, useEffect, useState } from 'react'
4
4
 
5
5
  import { errorMessage, type KernelNode } from './client'
6
6
 
7
+ type KernelReadClient = KernelClient & {
8
+ get(
9
+ path: string,
10
+ ): Promise<{ node: { id: string } | null; wire: { nodes: readonly KernelNode[] } }>
11
+ }
12
+
7
13
  export type NodeState =
8
14
  | { status: 'idle' }
9
15
  | { status: 'loading' }
@@ -11,16 +17,8 @@ export type NodeState =
11
17
  | { status: 'error'; message: string }
12
18
 
13
19
  /**
14
- * Fetch a node by id via `@<id>::get`. Re-fetches when the target node id
15
- * changes (`setTarget` hot-swap); stays `idle` until a target node id exists.
16
- * `session` is a stable reference for the view's lifetime, so listing it as a
17
- * dep never re-fires on its own — and the shell's client reads the fresh token
18
- * at call time, so a `ctrl:tokenRefresh` needs no re-fetch.
19
- *
20
- * Returns the node state plus a `reload()` that re-fetches the current node —
21
- * the view calls it after an instance method (e.g. `::check`) mutates the node
22
- * so the fresh props render. It bumps an internal `nonce` dep, re-firing the
23
- * effect.
20
+ * Fetch a node by id through `shell.kernel.get('@<id>')`.
21
+ * `null` means missing or masked; `reload()` refetches the current id.
24
22
  */
25
23
  export function useNode(
26
24
  session: KernelClient,
@@ -40,11 +38,21 @@ export function useNode(
40
38
  }
41
39
 
42
40
  let cancelled = false
41
+ const kernel = session as KernelReadClient
43
42
  setState({ status: 'loading' })
44
- session
45
- .call(`@${nodeId}::get`, {})
43
+ kernel
44
+ .get(`@${nodeId}`)
46
45
  .then((result) => {
47
- if (!cancelled) setState({ status: 'ok', node: result as KernelNode })
46
+ if (cancelled) return
47
+ const node =
48
+ result.node === null
49
+ ? null
50
+ : (result.wire.nodes.find((n) => n.id === result.node?.id) ?? null)
51
+ if (node === null) {
52
+ setState({ status: 'error', message: 'Node not found or not visible' })
53
+ return
54
+ }
55
+ setState({ status: 'ok', node })
48
56
  })
49
57
  .catch((err: unknown) => {
50
58
  if (!cancelled) {
@@ -1,13 +1,10 @@
1
1
  /**
2
2
  * The design system — generic, feature-AGNOSTIC presentational primitives +
3
- * display formatters, no domain knowledge and no kernel session. The one
4
- * domain-shaped exception is `StatusBadge`: it renders the up/degraded/down/
5
- * unknown health vocabulary that any `Checkable` node reports, so it's a shared
6
- * primitive here rather than living inside the status feature.
7
- * Import from the barrel: `import { Panel, KV, relativeTime } from '@/ui'`.
3
+ * display formatters, no domain knowledge and no kernel session. Import from the
4
+ * barrel: `import { Panel, KV, relativeTime } from '@/ui'`. A view's own
5
+ * health/status chips and other domain-shaped widgets live alongside their
6
+ * feature, not here.
8
7
  */
9
8
  export { EmptyState, ErrorBanner, Panel, Spinner } from './surface'
10
- export { StatusBadge } from './StatusBadge'
11
- export type { HealthStatus } from './StatusBadge'
12
9
  export { ExternalLink, KV, Mono } from './value'
13
10
  export { relativeTime } from './format'
@@ -0,0 +1,23 @@
1
+ # core/ — pure domain logic
2
+
3
+ This folder holds the **pure, transport-agnostic logic** for each bounded
4
+ context — no I/O, no `fetch`, no clock/RNG, no kernel session. Just functions
5
+ over plain data that `runtime/` handlers call. Being pure makes it the easy part
6
+ to unit-test.
7
+
8
+ A context usually gets its own subfolder:
9
+
10
+ ```
11
+ core/<context>/
12
+ keys.ts compiled-schema accessors (class paths, prop keys) — the ONE
13
+ place graph-facing strings come from; read them off `D` from
14
+ `schema/`, never hand-write them
15
+ logic.ts the pure rules (validation, computation, state transitions)
16
+ index.ts re-export the context's surface
17
+ ```
18
+
19
+ Keep the impure bits (network calls, kernel reads/writes, entropy) out of here —
20
+ those live behind a port in `integrations/` and in the `runtime/` handlers.
21
+
22
+ Delete this README once you add your first context. Load the `astrale-domain`
23
+ skill for the modeling patterns.
package/template/deps.ts CHANGED
@@ -7,20 +7,21 @@ import type { Env } from './env'
7
7
  * it's where ports/clients are wired once instead of being re-derived in every
8
8
  * handler.
9
9
  *
10
- * The prober port is resolved PER-REQUEST and PER-NODE (each `prober(target)`
11
- * call), not here so this stays a cheap synchronous wiring step and the backend
12
- * can be chosen from the monitor being probed, not just the env.
10
+ * It starts as a PASSTHROUGH handlers get the raw `Env` as `ctx.deps`.
11
+ * Transform here the moment a handler should depend on a PORT (built from
12
+ * `integrations/`) instead of raw config that's the whole point of this seam:
13
13
  *
14
- * Omit the `deps` field in `domain.ts` for the trivial case (handlers then get
15
- * raw `Env`). Transform here the moment a handler should depend on a PORT instead
16
- * of raw config that's the whole point of this seam.
14
+ * import { buildExampleRegistry, type ExampleRegistry } from './integrations/example/registry'
15
+ * export interface Deps extends ExampleRegistry {}
16
+ * export function deps(env: Env): Deps {
17
+ * return buildExampleRegistry(env)
18
+ * }
17
19
  */
18
- import { buildProberRegistry, type ProberRegistry } from './integrations/prober/registry'
19
20
 
20
21
  /** Typed dependency container handed to every method as `ctx.deps`. */
21
- export interface Deps extends ProberRegistry {}
22
+ export interface Deps extends Env {}
22
23
 
23
24
  /** Map the worker env to the handler deps (the seam `defineDomain({ deps })` mounts). */
24
25
  export function deps(env: Env): Deps {
25
- return buildProberRegistry(env)
26
+ return env
26
27
  }
@@ -1,11 +1,17 @@
1
1
  /**
2
2
  * domain.ts — the WORKER-SAFE domain definition: what the domain IS (its
3
3
  * `schema`, `methods`, `deps`, `views`, `functions`) plus its identity
4
- * (`origin` defaults to `schema.domain`; `postInstall`). Everything is wired
5
- * EXPLICITLY here — a renamed or mistyped module is a compile error at this
6
- * call, never a silently-missing route. The handlers live in `runtime/` (the
7
- * composition root), the pure logic in `core/`, the external-API ports in
8
- * `integrations/` — but the worker only ever sees this one wired definition.
4
+ * (`origin` defaults to `schema.domain`; `requires`; optional `postInstall`).
5
+ * Everything is wired EXPLICITLY here — a renamed or mistyped module is a
6
+ * compile error at this call, never a silently-missing route. The handlers live
7
+ * in `runtime/` (the composition root), the pure logic in `core/`, the
8
+ * external-API ports in `integrations/` — but the worker only ever sees this one
9
+ * wired definition.
10
+ *
11
+ * The maps it wires (`methods`, `views`, `functions`) start empty — this is a
12
+ * blank skeleton. Fill them in their folders and they flow through here with no
13
+ * change needed. Add `postInstall: functions.<key>` once you author a seed
14
+ * function — the kernel calls it once after install, as __SYSTEM__.
9
15
  *
10
16
  * The generated worker (`.astrale/worker.gen.ts`) imports THIS and spreads it,
11
17
  * so your folder layout is invisible to it — reorganize freely; only this wiring
@@ -27,9 +33,5 @@ export const domain = defineDomain({
27
33
  deps,
28
34
  views,
29
35
  functions,
30
- // Reference the `seed` function directly — the SDK derives its path from the
31
- // `functions` map, so a rename can't leave a stale string. The kernel calls it
32
- // once after install, as __SYSTEM__. (A class method would be `'class.X:seed'`.)
33
- postInstall: functions.seed,
34
36
  requires: [],
35
37
  })
package/template/env.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Worker runtime env. The adapter injects `WORKER_URL` and the bindings; your
3
- * handlers receive this as `ctx.deps`. Secrets from `.env.<env>` arrive as
4
- * extra string fields here. Add typed fields as you reference new secrets/vars.
3
+ * handlers receive this (mapped by `deps.ts`) as `ctx.deps`. Secrets from
4
+ * `.env.<env>` arrive as extra string fields here. Add typed fields as you
5
+ * reference new secrets/vars.
5
6
  */
6
7
  export interface Env {
7
8
  /** The worker's canonical serving URL (its `iss` identity), injected by the
@@ -19,8 +20,5 @@ export interface Env {
19
20
  /** Dev-only: forward /ui/* to a running Vite dev server. */
20
21
  VIEW_DEV_URL?: string
21
22
 
22
- /** `http` only — per-probe timeout in ms (default 10000). */
23
- PROBE_TIMEOUT_MS?: string
24
-
25
23
  [key: string]: unknown
26
24
  }
@@ -2,44 +2,21 @@
2
2
  * Standalone-function registry — one entry per callable that is NOT a method of
3
3
  * a class. Each becomes a first-class domain MEMBER: a `Function` node at
4
4
  * `/<origin>/functions/<key>` with an `of_domain` edge (slug `function.<key>`),
5
- * so it is addressable by the semantic domain path `/:<origin>:function.<key>`
6
- * (e.g. as a `postInstall` target see `domain.ts`) on top of its worker route
7
- * `/functions/<key>`. The SDK stamps `binding.remoteUrl` with the worker's public
8
- * URL at install. The map key is the slug (single source of truth).
5
+ * addressable by the semantic domain path `/:<origin>:function.<key>` on top of
6
+ * its worker route `/functions/<key>`. The map key is the slug.
7
+ *
8
+ * It starts EMPTY. Add a function (e.g. a webhook, or a `postInstall` seed) with
9
+ * `defineRemoteFunction` and list it here; reference it from `domain.ts` (e.g.
10
+ * `postInstall: functions.seed`) so a rename can't leave a stale path string:
11
+ *
12
+ * seed: defineRemoteFunction({
13
+ * inputSchema: z.object({}),
14
+ * outputSchema: z.object({ seeded: z.number().int() }),
15
+ * authorize: async () => undefined,
16
+ * execute: ({ kernel }) => ({ seeded: 0 }),
17
+ * }),
9
18
  *
10
19
  * A standalone function's `execute` receives the same request context as a
11
20
  * method MINUS `self` (it is not node-bound): `{ params, kernel, deps, auth, … }`.
12
21
  */
13
- import { __SYSTEM__ } from '@astrale-os/kernel-core'
14
- import { assertPerm, defineRemoteFunction, EDIT } from '@astrale-os/sdk'
15
- import { z } from 'zod'
16
-
17
- import type { Deps } from '../deps'
18
-
19
- import { MONITORS_PARENT } from '../core/monitor'
20
- import { monitor } from '../runtime/monitoring'
21
-
22
- export const functions = {
23
- // Post-install bootstrap, wired as `postInstall` in `domain.ts`. The kernel
24
- // calls it ONCE after install, as __SYSTEM__ (its delegation minted for this
25
- // function's own identity), so the domain comes up demonstrable. Must stay
26
- // idempotent — a re-install runs it again. `auth: 'required'` (the default)
27
- // makes `ctx.kernel` non-null; the body delegates to the per-operation seed
28
- // logic in `runtime/monitoring/monitor/seed.ts`.
29
- seed: defineRemoteFunction<Record<string, never>, { seeded: number }, Deps>({
30
- inputSchema: z.object({}),
31
- outputSchema: z.object({ seeded: z.number().int() }),
32
- // Additive throw-to-deny pre-check (the kernel ALSO enforces `has_perm` on the
33
- // create downstream — this just fails fast with the exact target). Two cases:
34
- // • postInstall runs as __SYSTEM__ (full authority) and on the FIRST install
35
- // `/monitoring/monitors` doesn't exist yet — seed creates it — so the
36
- // short-circuit MUST skip the check, or first install would wrongly deny.
37
- // • A later DIRECT call carries a real principal (and by then the folder
38
- // exists), so require EDIT — the bit a child-create under it needs.
39
- authorize: async ({ kernel, auth }) => {
40
- if (auth.principal === __SYSTEM__) return
41
- await assertPerm(kernel, MONITORS_PARENT, auth.principal, EDIT)
42
- },
43
- execute: ({ kernel, deps }) => monitor.seed(kernel, deps.prober()),
44
- }),
45
- }
22
+ export const functions = {}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Class icons — raw SVG markup attached to a class via `nodeClass({ icon })`.
3
+ * The kernel materializes the icon onto the class `self` node (the `Iconable`
4
+ * interface) at boot; clients resolve an instance's icon through its class.
5
+ *
6
+ * Markup is raw inner SVG — lift glyphs verbatim from lucide (ISC) and the
7
+ * `svg()` wrapper supplies the `<svg>` shell. `stroke="currentColor"` so the
8
+ * glyph follows text color. Add one constant per class:
9
+ *
10
+ * import { BOX_ICON } from '../icons'
11
+ * export const Widget = nodeClass({ icon: BOX_ICON, props: { … } })
12
+ */
13
+
14
+ const svg = (inner: string): string =>
15
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`
16
+
17
+ /** lucide `box` — a generic starter glyph for your first class. */
18
+ export const BOX_ICON = svg(
19
+ '<path d="M21 8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16Z"/><path d="m3.3 7 8.7 5 8.7-5"/><path d="M12 22V12"/>',
20
+ )
21
+
22
+ /** lucide `sparkles` — for an AI / generative class. */
23
+ export const SPARKLES_ICON = svg(
24
+ '<path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/>',
25
+ )
@@ -0,0 +1,22 @@
1
+ # integrations/ — external-API ports + adapters
2
+
3
+ This is the seam between your domain's logic and the outside world (an HTTP API,
4
+ an SDK, a queue, …). The pattern keeps `runtime/` logic depending on a narrow
5
+ **port** (an interface), never on `fetch` or a vendor SDK directly — so a handler
6
+ is testable against a fake and the backend can be swapped per env or per node.
7
+
8
+ ```
9
+ integrations/<service>/
10
+ port.ts the interface the logic depends on (the narrowest surface)
11
+ http.ts a real adapter implementing the port
12
+ mock.ts an offline/deterministic adapter, for tests
13
+ registry.ts picks an adapter from `Env` (and optionally the target node),
14
+ exposed to handlers as part of `Deps`
15
+ ```
16
+
17
+ `deps.ts` builds the registry from the worker `Env` once per isolate and mounts
18
+ it as `ctx.deps`; handlers resolve the concrete port per request. Until you add a
19
+ port, `deps.ts` is a passthrough (`Deps = Env`).
20
+
21
+ Delete this README once you add your first integration. Load the
22
+ `astrale-domain` skill for the DI / secrets / idempotency patterns.
@@ -14,10 +14,10 @@
14
14
  "typecheck": "tsgo --noEmit"
15
15
  },
16
16
  "dependencies": {
17
- "@astrale-os/adapter-cloudflare": ">=0.3.1 <1.0.0",
18
- "@astrale-os/kernel-core": ">=0.5.0 <1.0.0",
17
+ "@astrale-os/adapter-cloudflare": ">=0.3.2 <1.0.0",
18
+ "@astrale-os/kernel-core": ">=0.6.2 <1.0.0",
19
19
  "@astrale-os/kernel-dsl": ">=0.1.2 <1.0.0",
20
- "@astrale-os/sdk": ">=0.2.0 <1.0.0",
20
+ "@astrale-os/sdk": ">=0.3.0 <1.0.0",
21
21
  "@hono/node-server": "^1.19.0",
22
22
  "zod": "^4.3.6"
23
23
  },
@@ -1,82 +1,35 @@
1
1
  /**
2
2
  * Runtime composition root — the ONLY place request context (kernel/self/params/
3
- * auth) and `deps` meet the per-feature logic. Each `execute` resolves what it
4
- * needs (the `Prober` port, the self node) and delegates to the monitoring
5
- * bounded context in `runtime/monitoring/{monitor,page}`. No business logic
6
- * lives here.
3
+ * auth) and `deps` meet the per-feature logic. The `methods` map is what
4
+ * `domain.ts` mounts: one entry per class method, each delegating to the pure
5
+ * logic in `core/` and resolving its ports from `deps`. No business logic lives
6
+ * here — this file is just the wiring.
7
7
  *
8
- * `check` is the `Checkable` interface method, declared `abstract`, so it is
9
- * implemented PER CLASS (Monitor probes; StatusPage rolls up) — wired under
10
- * `class.Monitor` / `class.StatusPage`, NOT a shared `interface` block. The
11
- * kernel dispatches `@<node>::check` to the impl for that node's class.
8
+ * It starts EMPTY (`class: {}`). Once you declare a class with methods in
9
+ * `schema/`, wire its handlers here:
12
10
  *
13
- * SDK-level `authorize` is an additive throw-to-deny check (returns void). For
14
- * finer worker checks see `assertPerm` / `requireOwnership` from `@astrale-os/sdk`.
11
+ * import { remoteClassMethods, remoteMethod } from '@astrale-os/sdk'
12
+ * const method = remoteMethod<Deps>()
13
+ * const classMethods = remoteClassMethods<Deps>()
14
+ *
15
+ * const greetMethod = method(schema, 'Greeter', 'greet', {
16
+ * authorize: async () => undefined,
17
+ * execute: ({ params }) => ({ message: `hi ${params.name}` }),
18
+ * })
19
+ *
20
+ * export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
21
+ * class: { Greeter: classMethods(schema, 'Greeter', { greet: greetMethod }) },
22
+ * }
23
+ *
24
+ * Load the `astrale-domain` skill for the full handler/authorize/port patterns.
15
25
  */
16
- import { remoteClassMethods, remoteMethod, type SchemaMethodsImpl } from '@astrale-os/sdk'
26
+ import type { SchemaMethodsImpl } from '@astrale-os/sdk'
17
27
 
18
28
  import type { Deps } from '../deps'
19
-
20
- import { MONITOR_KEYS } from '../core/monitor'
21
- import { schema } from '../schema'
22
- import { monitor, page } from './monitoring'
23
-
24
- const method = remoteMethod<Deps>()
25
- const classMethods = remoteClassMethods<Deps>()
26
-
27
- // ── Monitor ─────────────────────────────────────────────────────────
28
-
29
- const monitorCheckMethod = method(schema, 'Monitor', 'check', {
30
- authorize: async () => undefined,
31
- execute: async ({ kernel, self, deps }) => {
32
- const node = await self.node()
33
- const url = node.props[MONITOR_KEYS.url]
34
- if (typeof url !== 'string') throw new Error('Monitor node is missing its url property')
35
- // Pass the node to the picker — the prober is chosen FOR this monitor.
36
- return monitor.check(kernel, deps.prober({ class: node.class.raw, url }), self.path.raw, url)
37
- },
38
- })
39
-
40
- const watchMethod = method(schema, 'Monitor', 'watch', {
41
- authorize: async () => undefined,
42
- execute: ({ kernel, params }) => {
43
- return monitor.watch(kernel, params)
44
- },
45
- })
46
-
47
- // ── StatusPage ──────────────────────────────────────────────────────
48
-
49
- const pageCheckMethod = method(schema, 'StatusPage', 'check', {
50
- authorize: async () => undefined,
51
- execute: ({ kernel, self }) => {
52
- return page.check(kernel, self.path.raw)
53
- },
54
- })
55
-
56
- const createMethod = method(schema, 'StatusPage', 'create', {
57
- authorize: async () => undefined,
58
- execute: ({ kernel, params }) => {
59
- return page.create(kernel, params)
60
- },
61
- })
62
-
63
- const addMethod = method(schema, 'StatusPage', 'add', {
64
- authorize: async () => undefined,
65
- execute: ({ kernel, self, params }) => {
66
- return page.add(kernel, self.path.raw, params)
67
- },
68
- })
29
+ // While the map is empty, `schema` is only referenced as a type (`typeof schema`).
30
+ // Drop the `type` keyword once you pass `schema` to `method()` / `classMethods()`.
31
+ import type { schema } from '../schema'
69
32
 
70
33
  export const methods: SchemaMethodsImpl<typeof schema, Deps> = {
71
- class: {
72
- Monitor: classMethods(schema, 'Monitor', {
73
- check: monitorCheckMethod,
74
- watch: watchMethod,
75
- }),
76
- StatusPage: classMethods(schema, 'StatusPage', {
77
- check: pageCheckMethod,
78
- create: createMethod,
79
- add: addMethod,
80
- }),
81
- },
34
+ class: {},
82
35
  }
@@ -5,26 +5,26 @@
5
5
  * and the worker signs JWTs under it. The scaffolder set this string from your
6
6
  * slug; change it here to rename the domain's identity.
7
7
  *
8
- * Add a context: author `schema/<context>.ts`, then import its members and list
9
- * them in the `interfaces` / `classes` maps below.
8
+ * The maps start EMPTY this is prepared terrain, not a working domain. Add a
9
+ * context by authoring `schema/<context>.ts` (declare classes with `nodeClass`,
10
+ * interfaces with `nodeInterface`, edges with `edgeClass`), then import its
11
+ * members and list them in the `interfaces` / `classes` maps below. Wire the
12
+ * matching handlers in `runtime/`. Load the `astrale-domain` skill for the
13
+ * authoring patterns.
10
14
  */
11
15
  import { defineSchema, KernelSchema } from '@astrale-os/kernel-core'
12
16
  import { compileDomain, type Domain } from '@astrale-os/kernel-core/domain'
13
17
 
14
- import { Checkable, Monitor, StatusPage, watches } from './monitor'
15
-
16
18
  export const schema = defineSchema('astrale-domain.example.dev', {
17
- interfaces: { Checkable },
18
- classes: { Monitor, StatusPage, watches },
19
+ interfaces: {},
20
+ classes: {},
19
21
  imports: [KernelSchema],
20
22
  })
21
23
 
22
24
  /**
23
25
  * The compiled domain — resolved class/interface paths + qualified prop keys.
24
26
  * `compileDomain` is pure (no env), so this is safe to import from the worker
25
- * and from build tooling alike. `core/monitor/keys.ts` re-exports it as the single
26
- * source of graph-facing strings (class paths, prop keys) — never hand-write those.
27
+ * and from build tooling alike. Read graph-facing strings (class paths, prop
28
+ * keys) off `D` once you add classes — never hand-write them.
27
29
  */
28
30
  export const D: Domain<typeof schema> = compileDomain(schema)
29
-
30
- export * from './monitor'
@@ -22,6 +22,7 @@
22
22
  "deps.ts",
23
23
  "env.ts",
24
24
  "domain.ts",
25
+ "icons.ts",
25
26
  "astrale.config.ts"
26
27
  ],
27
28
  "exclude": ["node_modules", ".astrale", ".dist"]
@@ -1,16 +1,19 @@
1
1
  /**
2
2
  * View registry — one file per view under `views/`, assembled here. Slug = map
3
- * key; each becomes a View node at `/<origin>/views/<slug>` whose iframe
4
- * binding the SDK stamps with the worker's live serving URL when it builds the
5
- * install bundle.
3
+ * key; each becomes a View node at `/<origin>/views/<slug>` whose iframe binding
4
+ * the SDK stamps with the worker's live serving URL when it builds the install
5
+ * bundle.
6
6
  *
7
- * `welcome` is a worker-rendered inline-HTML view (no SPA). `ui-status-page` is
8
- * the `client/` SPA (mounted at `/ui/status-page`, offered for any StatusPage).
7
+ * It starts EMPTY. Two shapes of view:
8
+ *
9
+ * - Inline HTML, rendered straight from the worker (no client bundle):
10
+ * export const welcome = defineView({ auth: 'public', render: ({ c }) =>
11
+ * c.html('<h1>hello</h1>') })
12
+ * - A `client/` SPA view, mounted under `/ui/<route>` and offered for a class:
13
+ * export const panel = defineView({ auth: 'public', mount: '/ui/panel',
14
+ * viewFor: selfOf(SomeClass) })
15
+ * then register a matching route in `client/src/app.tsx`.
16
+ *
17
+ * Add the view file under `views/`, import it, and list it below (key = slug).
9
18
  */
10
- import { statusPage } from './status-page'
11
- import { welcome } from './welcome'
12
-
13
- export const views = {
14
- welcome,
15
- 'ui-status-page': statusPage,
16
- }
19
+ export const views = {}
@@ -1,77 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
-
3
- import { qualifiedString, readProp, readPropBySuffix } from '@/shell'
4
- import { checkableFromNode, watchedMonitorRefs } from '@/status'
5
- import { relativeTime } from '@/ui'
6
-
7
- import { checkableNode } from './harness'
8
-
9
- describe('prop readers', () => {
10
- it('readProp returns the string value at the exact key', () => {
11
- expect(readProp({ a: 'x', b: 1 }, 'a')).toBe('x')
12
- expect(readProp({ a: 'x' }, 'missing')).toBeUndefined()
13
- expect(readProp({ a: 1 }, 'a')).toBeUndefined() // non-string
14
- })
15
-
16
- it('readPropBySuffix matches the first key ending with the suffix', () => {
17
- const props = {
18
- 'monitors.astrale.ai:class.Monitor.property.url': 'https://x.test',
19
- 'kernel.astrale.ai:interface.Named.property.name': 'n',
20
- }
21
- expect(readPropBySuffix(props, '.property.url')).toBe('https://x.test')
22
- expect(readPropBySuffix(props, '.property.status')).toBeUndefined()
23
- })
24
-
25
- it('qualifiedString prefers a domain key over the kernel one for the same suffix', () => {
26
- const props = {
27
- 'kernel.astrale.ai:interface.Named.property.name': 'kernel name',
28
- 'monitors.astrale.ai:class.Monitor.property.name': 'domain name',
29
- }
30
- expect(qualifiedString(props, 'name')).toBe('domain name')
31
- })
32
- })
33
-
34
- describe('checkableFromNode mapper', () => {
35
- it('projects the name and rolled-up status of a StatusPage node', () => {
36
- const record = checkableFromNode(
37
- checkableNode({ id: 'page-1', name: 'Public', status: 'degraded', className: 'StatusPage' }),
38
- )
39
- expect(record).toMatchObject({
40
- id: 'page-1',
41
- name: 'Public',
42
- status: 'degraded',
43
- className: 'StatusPage',
44
- })
45
- })
46
-
47
- it('falls back to the path segment for the name and unknown for a missing status', () => {
48
- const record = checkableFromNode(
49
- checkableNode({ id: 'page-9', path: '/monitoring/pages/edge' }),
50
- )
51
- expect(record.name).toBe('edge')
52
- expect(record.status).toBe('unknown')
53
- })
54
- })
55
-
56
- describe('watchedMonitorRefs mapper', () => {
57
- it('projects target refs and critical edge weights', () => {
58
- const refs = watchedMonitorRefs([
59
- {
60
- class: { raw: '/:monitors.astrale.ai:class.watches' },
61
- target: { raw: '/monitoring/monitors/astrale' },
62
- props: { 'monitors.astrale.ai:class.watches.property.critical': true },
63
- },
64
- ])
65
- expect(refs).toEqual([{ target: '/monitoring/monitors/astrale', critical: true }])
66
- })
67
- })
68
-
69
- describe('relativeTime', () => {
70
- const now = Date.parse('2026-06-14T12:00:00Z')
71
- it('renders coarse buckets and a dash for missing input', () => {
72
- expect(relativeTime(undefined, now)).toBe('—')
73
- expect(relativeTime('2026-06-14T11:55:00Z', now)).toBe('5m ago')
74
- expect(relativeTime('2026-06-14T10:00:00Z', now)).toBe('2h ago')
75
- expect(relativeTime('not-a-date', now)).toBe('not-a-date')
76
- })
77
- })