@astrale-os/adapter-cloudflare 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 (81) hide show
  1. package/package.json +1 -1
  2. package/template/.agents/skills/astrale-cli/SKILL.md +1 -1
  3. package/template/.agents/skills/astrale-domain/SKILL.md +5 -5
  4. package/template/.env.example +5 -7
  5. package/template/README.md +2 -2
  6. package/template/client/README.md +81 -62
  7. package/template/client/__tests__/app.test.tsx +143 -98
  8. package/template/client/__tests__/harness.ts +62 -12
  9. package/template/client/__tests__/kernel.test.ts +40 -51
  10. package/template/client/__tests__/seam.test.tsx +115 -0
  11. package/template/client/index.html +1 -1
  12. package/template/client/package.json +1 -0
  13. package/template/client/src/app.tsx +34 -83
  14. package/template/client/src/main.tsx +2 -2
  15. package/template/client/src/shell/client.ts +67 -0
  16. package/template/client/src/shell/index.ts +20 -0
  17. package/template/client/src/shell/invoke.ts +35 -0
  18. package/template/client/src/shell/transformers.ts +72 -0
  19. package/template/client/src/shell/use-async.ts +56 -0
  20. package/template/client/src/shell/use-capability.ts +59 -0
  21. package/template/client/src/shell/use-node.ts +61 -0
  22. package/template/client/src/shell/use-shell.ts +91 -0
  23. package/template/client/src/shell/view-router.tsx +97 -0
  24. package/template/client/src/status/components/StatusCard.tsx +50 -0
  25. package/template/client/src/status/components/index.ts +1 -0
  26. package/template/client/src/status/hooks/index.ts +3 -0
  27. package/template/client/src/status/hooks/useCheck.mutation.ts +16 -0
  28. package/template/client/src/status/hooks/useCheckable.query.ts +64 -0
  29. package/template/client/src/status/index.ts +7 -0
  30. package/template/client/src/status/status.api.ts +12 -0
  31. package/template/client/src/status/status.mappers.ts +19 -0
  32. package/template/client/src/status/status.types.ts +11 -0
  33. package/template/client/src/styles.css +182 -4
  34. package/template/client/src/ui/StatusBadge.tsx +31 -0
  35. package/template/client/src/ui/format.ts +24 -0
  36. package/template/client/src/ui/index.ts +13 -0
  37. package/template/client/src/ui/surface.tsx +56 -0
  38. package/template/client/src/ui/value.tsx +32 -0
  39. package/template/client/src/views/status.tsx +28 -0
  40. package/template/client/tsconfig.json +2 -1
  41. package/template/client/vite.config.ts +11 -13
  42. package/template/client/vitest.config.ts +11 -5
  43. package/template/core/monitor/health.ts +34 -0
  44. package/template/core/monitor/index.ts +9 -0
  45. package/template/core/monitor/keys.ts +41 -0
  46. package/template/core/monitor/node.ts +57 -0
  47. package/template/deps.ts +10 -9
  48. package/template/domain.ts +1 -1
  49. package/template/env.ts +2 -9
  50. package/template/integrations/prober/http.ts +32 -0
  51. package/template/integrations/prober/mock.ts +18 -0
  52. package/template/integrations/prober/port.ts +26 -0
  53. package/template/integrations/prober/registry.ts +65 -0
  54. package/template/package.json +1 -1
  55. package/template/pnpm-lock.yaml +2766 -0
  56. package/template/runtime/index.ts +63 -34
  57. package/template/runtime/monitor/check.ts +29 -0
  58. package/template/runtime/monitor/index.ts +9 -0
  59. package/template/runtime/monitor/seed.ts +95 -0
  60. package/template/runtime/monitor/watch.ts +31 -0
  61. package/template/runtime/shared.ts +21 -0
  62. package/template/runtime/status-page/add.ts +21 -0
  63. package/template/runtime/status-page/check.ts +50 -0
  64. package/template/runtime/status-page/create.ts +24 -0
  65. package/template/runtime/status-page/index.ts +8 -0
  66. package/template/schema/index.ts +11 -4
  67. package/template/schema/monitor.ts +94 -0
  68. package/template/views/index.ts +8 -2
  69. package/template/views/status-page.ts +16 -0
  70. package/template/client/src/lib/kernel.ts +0 -135
  71. package/template/client/src/lib/shell.ts +0 -197
  72. package/template/client/src/lib/use-node.ts +0 -66
  73. package/template/client/src/lib/use-shell.ts +0 -85
  74. package/template/core/keys.ts +0 -28
  75. package/template/core/note.ts +0 -148
  76. package/template/integrations/summary/heuristic.ts +0 -25
  77. package/template/integrations/summary/http.ts +0 -69
  78. package/template/integrations/summary/port.ts +0 -21
  79. package/template/integrations/summary/registry.ts +0 -52
  80. package/template/schema/note.ts +0 -67
  81. package/template/views/note.ts +0 -21
@@ -1,135 +0,0 @@
1
- /**
2
- * Minimal, self-contained kernel JSON client — just enough to load a node via
3
- * `@<id>::get`, without pulling in `@astrale-os/kernel-client` (and its
4
- * transitive deps). It reproduces the kernel ENVELOPE wire shape inline.
5
- *
6
- * Wire contract (authoritative source: `kernel/api/envelope/`):
7
- * - Request: POST <kernelUrl> with headers
8
- * content-type: application/vnd.astrale.kernel+json
9
- * accept: application/vnd.astrale.kernel+json
10
- * authorization: <delegationToken> (BARE token — no "Bearer " prefix)
11
- * body JSON `{ method, params, id }` (see `encode.ts:encodeKernelRequest`).
12
- * - Response: JSON, exactly one of (decode precedence error → redirect → result,
13
- * mirroring `decode.ts:decodeKernelResponse`):
14
- * { error: { code, message }, id } → throw Error("<code>: <message>")
15
- * { redirect, id } → throw (we don't follow redirects here)
16
- * { result, id } → return result
17
- *
18
- * Deliberately JSON-only: no msgpack codec, no streaming/binary, no redirect
19
- * following, no schema/batching. Those live in `@astrale-os/kernel-client`,
20
- * which this self-contained build omits on purpose.
21
- */
22
-
23
- // From `kernel/api/envelope/types.ts` (KERNEL_CONTENT_TYPE_JSON). Inlined so the
24
- // template needs no @astrale-os import.
25
- const KERNEL_CONTENT_TYPE_JSON = 'application/vnd.astrale.kernel+json'
26
-
27
- // Module-local monotonic request id. Strings keep ids stable across reloads and
28
- // distinguishable in logs; the kernel echoes `id` back but we don't correlate
29
- // (one request per fetch).
30
- let idSeq = 0
31
- function nextId(): string {
32
- idSeq += 1
33
- return `c${idSeq}`
34
- }
35
-
36
- /**
37
- * Prop key constants — the graph stores props with fully-qualified keys
38
- * (`<domain>:<member>.property.<name>`). The kernel `Named.name` key is fixed
39
- * and known; domain props (`title`, `body`) are qualified by the (build-time
40
- * unknown) domain origin — read those by suffix with `readPropBySuffix`.
41
- */
42
- export const PROP = {
43
- named: {
44
- name: 'kernel.astrale.ai:interface.Named.property.name',
45
- },
46
- } as const
47
-
48
- export type KernelNode = {
49
- id: string
50
- path: string
51
- class: string | { raw?: string }
52
- props: Record<string, unknown>
53
- }
54
-
55
- export function readProp(props: Record<string, unknown>, key: string): string | undefined {
56
- const v = props[key]
57
- return typeof v === 'string' ? v : undefined
58
- }
59
-
60
- /**
61
- * Read a string prop by key suffix — for domain-qualified props whose full key
62
- * embeds the (build-time-unknown) domain origin. e.g.
63
- * `readPropBySuffix(props, '.property.body')`.
64
- */
65
- export function readPropBySuffix(
66
- props: Record<string, unknown>,
67
- suffix: string,
68
- ): string | undefined {
69
- for (const [k, v] of Object.entries(props)) {
70
- if (k.endsWith(suffix) && typeof v === 'string') return v
71
- }
72
- return undefined
73
- }
74
-
75
- /** Short class name from a `class.raw` path `/:<domain>:class.<Name>` → `<Name>`. */
76
- export function classShortName(node: KernelNode): string {
77
- const raw = (typeof node.class === 'string' ? node.class : node.class?.raw) ?? ''
78
- const last = raw.split(':').pop() ?? ''
79
- const dot = last.indexOf('.')
80
- return dot >= 0 ? last.slice(dot + 1) : last
81
- }
82
-
83
- /**
84
- * POST a single kernel call and return its `result`. Throws on a kernel error
85
- * envelope or an unexpected redirect. `token` is the bare delegation credential
86
- * from the shell handshake; the iframe authenticates SOLELY with it (the parent
87
- * minted it for this kernel, so the audience already matches — no cookie/mint).
88
- */
89
- export async function kernelCall(
90
- kernelUrl: string,
91
- token: string,
92
- method: string,
93
- params: Record<string, unknown> = {},
94
- ): Promise<unknown> {
95
- // The kernel routes on a trailing slash; the parent absolutizes the URL, but
96
- // not always with the slash, so normalize here.
97
- const url = kernelUrl.endsWith('/') ? kernelUrl : `${kernelUrl}/`
98
- const id = nextId()
99
-
100
- const res = await fetch(url, {
101
- method: 'POST',
102
- headers: {
103
- 'content-type': KERNEL_CONTENT_TYPE_JSON,
104
- accept: KERNEL_CONTENT_TYPE_JSON,
105
- authorization: token,
106
- },
107
- body: JSON.stringify({ method, params, id }),
108
- })
109
-
110
- let body: unknown
111
- try {
112
- body = await res.json()
113
- } catch {
114
- throw new Error(`kernel returned a non-JSON response (HTTP ${res.status})`)
115
- }
116
-
117
- if (body === null || typeof body !== 'object') {
118
- throw new Error(`kernel returned an unexpected response (HTTP ${res.status})`)
119
- }
120
- const obj = body as Record<string, unknown>
121
-
122
- // Decode precedence error → redirect → result (mirrors decodeKernelResponse).
123
- if ('error' in obj && obj.error && typeof obj.error === 'object') {
124
- const err = obj.error as { code?: unknown; message?: unknown }
125
- const code = typeof err.code === 'number' ? err.code : 5000
126
- const message = typeof err.message === 'string' ? err.message : 'Unknown error'
127
- throw new Error(`${code}: ${message}`)
128
- }
129
- if ('redirect' in obj && obj.redirect) {
130
- // Redirects (remote-domain Functions) aren't followed by this minimal
131
- // client — they require credential re-minting against the target worker.
132
- throw new Error('unexpected redirect from kernel (not supported by the template client)')
133
- }
134
- return obj.result
135
- }
@@ -1,197 +0,0 @@
1
- /**
2
- * Minimal, self-contained reimplementation of the Astrale shell *child* init
3
- * handshake — just enough to learn which node this view is mounted for and to
4
- * keep its delegation token live, without pulling in `@astrale-os/shell` (and
5
- * its `kernel-client` transitive deps).
6
- *
7
- * Protocol (mirrors `shell/packages/shell/src/application/windowing/`):
8
- * 1. child posts `{ type: 'astrale-shell/init-request', version: 1 }` to the
9
- * parent window via `window.postMessage(..., '*')`.
10
- * 2. parent replies with `{ type: 'astrale-shell/init-response', windowId }`
11
- * and *transfers a MessagePort* (`event.ports[0]`).
12
- * 3. over that port the parent sends a `ctrl:handshake` carrying the session
13
- * data — `windowId`, `kernelUrl`, `delegationToken`, `tokenExpiresAt`,
14
- * `functionId`, and the `targetNodeId` (the node this view should render).
15
- * 4. child replies with `ctrl:handshakeAck` and keeps the port open.
16
- *
17
- * Steady state on the port:
18
- * - `intent` `setTarget { nodeId }` — hot-swap the target node (re-render).
19
- * - `ctrl:tokenRefresh { delegationToken, tokenExpiresAt }` — the parent
20
- * pushes a FRESH token before the old one expires. We update the mutable
21
- * `currentToken` so the next kernel call uses it (see `getToken()`).
22
- *
23
- * The iframe authenticates with the handshake `delegationToken` ONLY: the
24
- * parent minted it for this kernel, so the audience already matches — no
25
- * cookie, no whoami, no client-side minting. Actually CALLING the kernel lives
26
- * in `lib/kernel.ts` (a minimal inline JSON client).
27
- */
28
-
29
- const INIT_REQUEST_TYPE = 'astrale-shell/init-request'
30
- const INIT_RESPONSE_TYPE = 'astrale-shell/init-response'
31
-
32
- export type HandshakeData = {
33
- windowId: string
34
- kernelUrl?: string
35
- functionId?: string
36
- /** Opaque delegation credential the parent minted for this view. */
37
- delegationToken?: string
38
- /** Unix-ms expiry of `delegationToken`; advances on each `tokenRefresh`. */
39
- tokenExpiresAt?: number
40
- /** The node this view is mounted for — what we want to render. */
41
- targetNodeId?: string
42
- }
43
-
44
- export type ShellSession = {
45
- /** Handshake data captured at handshake time (token here is the INITIAL one). */
46
- readonly data: HandshakeData
47
- /** Convenience mirror of `data.kernelUrl` (satisfies `NodeSession`). */
48
- readonly kernelUrl: string | undefined
49
- /** The LIVE delegation token — reflects `tokenRefresh` pushes. */
50
- getToken(): string | undefined
51
- /** The LIVE token expiry (Unix ms) — reflects `tokenRefresh` pushes. */
52
- getTokenExpiresAt(): number | undefined
53
- /** Subscribe to `targetNodeId` hot-swaps pushed via `setTarget` intents. */
54
- onTargetChange(cb: (nodeId: string | undefined) => void): () => void
55
- /** Tear down listeners + the message port. */
56
- dispose(): void
57
- }
58
-
59
- type CtrlMessage = {
60
- type: 'ctrl'
61
- version: number
62
- action: string
63
- data?: Record<string, unknown>
64
- }
65
-
66
- type IntentMessage = {
67
- type: 'intent'
68
- envelope?: { name?: string; payload?: Record<string, unknown> }
69
- }
70
-
71
- const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null
72
-
73
- const asString = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined)
74
-
75
- /**
76
- * Run the child handshake. Resolves once the parent completes `ctrl:handshake`.
77
- * Rejects on timeout (no parent / not sandboxed) — callers should render a
78
- * standalone fallback in that case.
79
- */
80
- export function connectShell(timeoutMs = 5000): Promise<ShellSession> {
81
- return new Promise((resolve, reject) => {
82
- const parent = window.parent
83
- if (!parent || parent === window) {
84
- reject(new Error('not running inside a parent frame'))
85
- return
86
- }
87
-
88
- let port: MessagePort | null = null
89
- // Mutable token state — `tokenRefresh` updates these in place so later
90
- // kernel calls read the fresh credential via `getToken()`.
91
- let currentToken: string | undefined
92
- let currentExpiresAt: number | undefined
93
- const targetListeners = new Set<(nodeId: string | undefined) => void>()
94
-
95
- const timer = setTimeout(() => {
96
- cleanupWindow()
97
- reject(new Error(`shell handshake timed out after ${timeoutMs}ms`))
98
- }, timeoutMs)
99
-
100
- function cleanupWindow() {
101
- clearTimeout(timer)
102
- window.removeEventListener('message', onWindowMessage)
103
- }
104
-
105
- // Steady-state port listener: target hot-swaps + token refreshes.
106
- function onPortMessage(ev: MessageEvent) {
107
- const msg = ev.data as unknown
108
- if (!isObj(msg)) return
109
-
110
- // Token refresh: the parent pushed a fresh credential. Swap it in place.
111
- if (msg.type === 'ctrl' && (msg as CtrlMessage).action === 'tokenRefresh') {
112
- const d = (msg as CtrlMessage).data ?? {}
113
- const next = asString(d.delegationToken)
114
- if (next) currentToken = next
115
- if (typeof d.tokenExpiresAt === 'number') currentExpiresAt = d.tokenExpiresAt
116
- return
117
- }
118
-
119
- // Hot-swap: `setTarget` intent carries a new nodeId.
120
- if (msg.type === 'intent') {
121
- const intent = msg as IntentMessage
122
- if (intent.envelope?.name === 'setTarget') {
123
- const raw = intent.envelope.payload?.nodeId
124
- const nodeId = asString(raw)
125
- for (const cb of targetListeners) cb(nodeId)
126
- }
127
- }
128
- }
129
-
130
- function onWindowMessage(ev: MessageEvent) {
131
- const msg = ev.data as unknown
132
- if (ev.source !== parent || !isObj(msg)) return
133
- if (msg.type !== INIT_RESPONSE_TYPE) return
134
-
135
- const received = ev.ports[0]
136
- if (!received) return
137
- port = received
138
-
139
- port.onmessage = (portEv: MessageEvent) => {
140
- const ctrl = portEv.data as unknown
141
- if (!isObj(ctrl) || ctrl.type !== 'ctrl') {
142
- onPortMessage(portEv)
143
- return
144
- }
145
- const m = ctrl as CtrlMessage
146
- if (m.action !== 'handshake' || !port) return
147
-
148
- const d = (m.data ?? {}) as Record<string, unknown>
149
- currentToken = asString(d.delegationToken)
150
- currentExpiresAt = typeof d.tokenExpiresAt === 'number' ? d.tokenExpiresAt : undefined
151
- const kernelUrl = asString(d.kernelUrl)
152
-
153
- // Ack so the parent considers the child live.
154
- port.postMessage({
155
- type: 'ctrl',
156
- version: 1,
157
- action: 'handshakeAck',
158
- data: { windowId: d.windowId },
159
- })
160
- // Switch the port to the steady-state listener (intents + tokenRefresh).
161
- port.onmessage = onPortMessage
162
- cleanupWindow()
163
-
164
- resolve({
165
- data: {
166
- windowId: String(d.windowId ?? ''),
167
- kernelUrl,
168
- functionId: asString(d.functionId),
169
- delegationToken: currentToken,
170
- tokenExpiresAt: currentExpiresAt,
171
- targetNodeId: asString(d.targetNodeId),
172
- },
173
- kernelUrl,
174
- getToken: () => currentToken,
175
- getTokenExpiresAt: () => currentExpiresAt,
176
- onTargetChange(cb) {
177
- targetListeners.add(cb)
178
- return () => targetListeners.delete(cb)
179
- },
180
- dispose() {
181
- cleanupWindow()
182
- targetListeners.clear()
183
- if (port) {
184
- port.onmessage = null
185
- port.close()
186
- }
187
- },
188
- })
189
- }
190
- port.start()
191
- }
192
-
193
- window.addEventListener('message', onWindowMessage)
194
- // targetOrigin '*' — the parent verifies our origin on its side.
195
- parent.postMessage({ type: INIT_REQUEST_TYPE, version: 1 }, '*')
196
- })
197
- }
@@ -1,66 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { kernelCall, type KernelNode } from './kernel'
4
-
5
- /**
6
- * The slice of the shell session this hook needs: where to call the kernel
7
- * (`kernelUrl`) and how to read the LIVE delegation token (`getToken()` —
8
- * which advances on `ctrl:tokenRefresh`). `ShellSession` satisfies this, and is
9
- * a stable reference for the component's lifetime (set once on handshake).
10
- */
11
- export type NodeSession = {
12
- readonly kernelUrl: string | undefined
13
- getToken(): string | undefined
14
- }
15
-
16
- export type NodeState =
17
- | { status: 'idle' }
18
- | { status: 'loading' }
19
- | { status: 'ok'; node: KernelNode }
20
- | { status: 'error'; message: string }
21
-
22
- /**
23
- * Fetch a node by id via `@<id>::get`, using the session's kernel URL + live
24
- * delegation token. Re-fetches when the target node id changes (`setTarget`
25
- * hot-swap) or when a token first becomes available. Stays `idle` until both a
26
- * target node id and a token exist.
27
- *
28
- * The effect keys on the token's PRESENCE (`hasToken`), not its value: a
29
- * `ctrl:tokenRefresh` swaps the token in place WITHOUT re-firing, and the next
30
- * fetch (triggered by a `setTarget` or remount) reads the fresh token via
31
- * `session.getToken()` at call time. `session` is stable, so listing it as a
32
- * dep is safe — it never causes a re-fire on its own.
33
- */
34
- export function useNode(session: NodeSession, nodeId: string | undefined): NodeState {
35
- const hasToken = session.getToken() !== undefined
36
- const [state, setState] = useState<NodeState>({ status: 'idle' })
37
-
38
- useEffect(() => {
39
- const token = session.getToken()
40
- if (!nodeId || !session.kernelUrl || !token) {
41
- setState({ status: 'idle' })
42
- return
43
- }
44
-
45
- let cancelled = false
46
- setState({ status: 'loading' })
47
- kernelCall(session.kernelUrl, token, `@${nodeId}::get`, {})
48
- .then((result) => {
49
- if (!cancelled) setState({ status: 'ok', node: result as KernelNode })
50
- })
51
- .catch((err: unknown) => {
52
- if (!cancelled) {
53
- setState({
54
- status: 'error',
55
- message: err instanceof Error ? err.message : String(err),
56
- })
57
- }
58
- })
59
-
60
- return () => {
61
- cancelled = true
62
- }
63
- }, [session, hasToken, nodeId])
64
-
65
- return state
66
- }
@@ -1,85 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { connectShell, type HandshakeData, type ShellSession } from './shell'
4
-
5
- export type ShellStatus = 'loading' | 'ready' | 'standalone'
6
-
7
- export type ShellState = {
8
- status: ShellStatus
9
- /** Handshake data once `ready` (kernelUrl, token, etc). */
10
- data: HandshakeData | null
11
- /**
12
- * The live session once `ready` — exposes `getToken()` (which reflects
13
- * `tokenRefresh`) and `kernelUrl`. The node-fetch hook reads through this so
14
- * it always uses the CURRENT token, not the one captured at handshake time.
15
- */
16
- session: ShellSession | null
17
- /** Current target node id — updates on `setTarget` hot-swaps. */
18
- nodeId: string | undefined
19
- /** Reason we fell back to `standalone` (no parent / timeout). */
20
- reason: string | null
21
- }
22
-
23
- /**
24
- * Runs the inline shell handshake once on mount and tracks the target node id.
25
- *
26
- * Three outcomes:
27
- * - `loading` — handshake in flight
28
- * - `ready` — parent completed the handshake; `data`/`session`/`nodeId`
29
- * populated
30
- * - `standalone` — no parent or it timed out (e.g. opened directly in a tab);
31
- * the view renders a self-describing fallback
32
- */
33
- export function useShell(): ShellState {
34
- const [state, setState] = useState<ShellState>({
35
- status: 'loading',
36
- data: null,
37
- session: null,
38
- nodeId: undefined,
39
- reason: null,
40
- })
41
-
42
- useEffect(() => {
43
- let cancelled = false
44
- let dispose: (() => void) | undefined
45
-
46
- connectShell()
47
- .then((session) => {
48
- if (cancelled) {
49
- session.dispose()
50
- return
51
- }
52
- setState({
53
- status: 'ready',
54
- data: session.data,
55
- session,
56
- nodeId: session.data.targetNodeId,
57
- reason: null,
58
- })
59
- const off = session.onTargetChange((nodeId) => {
60
- setState((prev) => ({ ...prev, nodeId }))
61
- })
62
- dispose = () => {
63
- off()
64
- session.dispose()
65
- }
66
- })
67
- .catch((err: unknown) => {
68
- if (cancelled) return
69
- setState({
70
- status: 'standalone',
71
- data: null,
72
- session: null,
73
- nodeId: undefined,
74
- reason: err instanceof Error ? err.message : String(err),
75
- })
76
- })
77
-
78
- return () => {
79
- cancelled = true
80
- dispose?.()
81
- }
82
- }, [])
83
-
84
- return state
85
- }
@@ -1,28 +0,0 @@
1
- /**
2
- * Compiled-schema accessors — the ONE place class paths, method paths, and
3
- * qualified prop keys come from. Pure (schema-derived); never hand-write key
4
- * strings. `D` (the compiled domain) is the public `schema/compiled` entry,
5
- * re-exported here so `core/` and `runtime/` read it — plus the named keys —
6
- * from one place.
7
- */
8
- import { K } from '@astrale-os/kernel-core'
9
-
10
- import { D } from '../schema/compiled'
11
-
12
- export { D, K }
13
-
14
- /** Kernel ops/classes the logic addresses. */
15
- export const NODE_CREATE = K.Node.createNode.path.method.raw
16
- export const FOLDER_CLASS = K.Folder.path.class.raw
17
- export const NAME_KEY = K.Named.name.key
18
-
19
- /** Domain class paths. */
20
- export const NOTE_CLASS = D.Note.path.class.raw
21
- export const REFERENCES_EDGE = D.references.path.class.raw
22
-
23
- /** Qualified storage keys for Note node props. */
24
- export const NOTE_KEYS = {
25
- title: D.Note.title.key,
26
- body: D.Note.body.key,
27
- summary: D.Note.summary.key,
28
- } as const
@@ -1,148 +0,0 @@
1
- /**
2
- * Note context — method LOGIC, written once, transport-agnostic and testable.
3
- *
4
- * Each handler touches the kernel ONLY through `kernel.call(...)` (the universal
5
- * syscalls) plus its `params`, the resolved `Summarizer` PORT, and — for
6
- * instance methods — the source node's `path.raw`. It never names `fetch`, the
7
- * worker `env`, or a concrete provider: the port arrives ready-to-use from
8
- * `runtime/` (which built it from `deps`). Address callables/edges with
9
- * layout-independent forms — a `ClassPath` for the edge class, the `<id>::link`
10
- * instance form, and an `@<id>` id-form target.
11
- */
12
- import type { Summarizer } from '../integrations/summary/port'
13
- import {
14
- FOLDER_CLASS,
15
- NAME_KEY,
16
- NODE_CREATE,
17
- NOTE_CLASS,
18
- NOTE_KEYS,
19
- REFERENCES_EDGE,
20
- } from './keys'
21
-
22
- /** The minimal kernel surface the worker runtime exposes to a handler. */
23
- export type CallableKernel = { call(path: string, params: unknown): Promise<unknown> }
24
-
25
- /** Notes live under a `/notes` folder at the graph root (created by `seed`). */
26
- const NOTES_PARENT = '/notes'
27
-
28
- /** URL-safe slug + short random suffix to dodge same-ms collisions. */
29
- function slugify(text: string): string {
30
- const stem =
31
- text
32
- .toLowerCase()
33
- .normalize('NFD')
34
- .replace(/[̀-ͯ]/g, '')
35
- .replace(/[^a-z0-9]+/g, '-')
36
- .replace(/^-|-$/g, '')
37
- .slice(0, 40) || 'note'
38
- return `${stem}-${Date.now().toString(36).slice(-4)}${Math.random().toString(36).slice(2, 6)}`
39
- }
40
-
41
- /**
42
- * `NoteOps.createNote` (static) — create a Note under `/notes`, stamping a
43
- * one-line `summary` from the resolved summarizer port (the external-API seam).
44
- */
45
- export async function createNote(
46
- kernel: CallableKernel,
47
- summarizer: Summarizer,
48
- params: { title: string; body: string },
49
- ): Promise<{ id: string; path: string }> {
50
- const summary = await summarizer.summarize(params.body)
51
- const path = `${NOTES_PARENT}/${slugify(params.title)}`
52
- const created = (await kernel.call(NODE_CREATE, {
53
- class: NOTE_CLASS,
54
- path,
55
- props: {
56
- [NAME_KEY]: params.title,
57
- [NOTE_KEYS.title]: params.title,
58
- [NOTE_KEYS.body]: params.body,
59
- [NOTE_KEYS.summary]: summary,
60
- },
61
- })) as { id: string }
62
- return { id: created.id, path }
63
- }
64
-
65
- /** `Note.reference` (instance) — link this Note to another via `references`. */
66
- export async function reference(
67
- kernel: CallableKernel,
68
- selfPathRaw: string,
69
- params: { target: string },
70
- ): Promise<{ linked: string }> {
71
- await kernel.call(`${selfPathRaw}::link`, {
72
- edgeClass: REFERENCES_EDGE,
73
- target: params.target,
74
- })
75
- return { linked: params.target }
76
- }
77
-
78
- const STARTERS: ReadonlyArray<{ slug: string; title: string; body: string }> = [
79
- {
80
- slug: 'welcome',
81
- title: 'Welcome',
82
- body: 'This note was created by `seed` after install. Edit it freely.',
83
- },
84
- { slug: 'getting-started', title: 'Getting started', body: 'Call `createNote` to add your own.' },
85
- ]
86
-
87
- function isPathConflict(e: unknown): boolean {
88
- return e instanceof Error && e.message.includes('PATH_CONFLICT')
89
- }
90
-
91
- /**
92
- * `Note.seed` (static) — the domain's post-install bootstrap. The kernel calls
93
- * it ONCE after install, as __SYSTEM__ (see `postInstall` in `domain.ts`), so
94
- * the domain can lay down its initial state: the `/notes` folder, a couple of
95
- * starter Notes (each summarized through the port), and a `references` edge
96
- * between them. Idempotent: a re-run swallows `PATH_CONFLICT` per node.
97
- */
98
- export async function seed(
99
- kernel: CallableKernel,
100
- summarizer: Summarizer,
101
- ): Promise<{ seeded: number }> {
102
- // 1. The `/notes` folder at the graph root.
103
- try {
104
- await kernel.call(NODE_CREATE, {
105
- class: FOLDER_CLASS,
106
- path: NOTES_PARENT,
107
- props: { [NAME_KEY]: 'notes' },
108
- })
109
- } catch (e) {
110
- if (!isPathConflict(e)) throw e
111
- }
112
-
113
- // 2. A couple of starter Notes under it.
114
- const ids: Record<string, string> = {}
115
- let seeded = 0
116
- for (const s of STARTERS) {
117
- try {
118
- const created = (await kernel.call(NODE_CREATE, {
119
- class: NOTE_CLASS,
120
- path: `${NOTES_PARENT}/${s.slug}`,
121
- props: {
122
- [NAME_KEY]: s.title,
123
- [NOTE_KEYS.title]: s.title,
124
- [NOTE_KEYS.body]: s.body,
125
- [NOTE_KEYS.summary]: await summarizer.summarize(s.body),
126
- },
127
- })) as { id: string }
128
- ids[s.slug] = created.id
129
- seeded++
130
- } catch (e) {
131
- if (!isPathConflict(e)) throw e
132
- }
133
- }
134
-
135
- // 3. Link welcome → getting-started with a `references` edge (id-form on
136
- // both sides — layout-independent). Best-effort.
137
- const from = ids.welcome
138
- const to = ids['getting-started']
139
- if (from && to) {
140
- try {
141
- await kernel.call(`@${from}::link`, { edgeClass: REFERENCES_EDGE, target: `@${to}` })
142
- } catch (e) {
143
- if (!isPathConflict(e)) throw e
144
- }
145
- }
146
-
147
- return { seeded }
148
- }
@@ -1,25 +0,0 @@
1
- /**
2
- * Heuristic summarizer — the ZERO-CONFIG default adapter. No network, no
3
- * secrets: a fresh scaffold summarizes notes out of the box on `pnpm dev`.
4
- * It takes the first sentence (clamped), which is good enough to demonstrate
5
- * the seam; flip `NOTE_SUMMARIZER=http` (see `registry.ts`) for a real model.
6
- */
7
- import type { Summarizer } from './port'
8
-
9
- const DEFAULT_MAX_LENGTH = 140
10
-
11
- /** Build the local, dependency-free summarizer. */
12
- export function createHeuristicSummarizer(opts: { maxLength?: number } = {}): Summarizer {
13
- const maxLength = opts.maxLength ?? DEFAULT_MAX_LENGTH
14
- return {
15
- summarize(body) {
16
- const trimmed = body.trim()
17
- // First sentence, else the whole (clamped) body.
18
- const firstSentence = trimmed.split(/(?<=[.!?])\s/)[0] ?? trimmed
19
- const text = firstSentence || trimmed
20
- const summary =
21
- text.length <= maxLength ? text : `${text.slice(0, maxLength - 1).trimEnd()}…`
22
- return Promise.resolve(summary)
23
- },
24
- }
25
- }