babavoss 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/NOTICE +7 -0
  2. package/bin/voss.ts +48 -0
  3. package/gui/babavoss-web.js +234 -0
  4. package/gui/chunk-5hpp1ypv.js +11710 -0
  5. package/gui/chunk-9rq662fd.js +8627 -0
  6. package/gui/chunk-k3cm8j6r.js +433 -0
  7. package/gui/chunk-pk09y98y.js +50 -0
  8. package/gui/chunk-smz02qa6.js +186 -0
  9. package/gui/chunk-wwqypxre.js +49 -0
  10. package/gui/gui.js +2015 -0
  11. package/gui/react-compiler-runtime.js +39 -0
  12. package/gui/react-dom-client.js +21 -0
  13. package/gui/react-dom.js +44 -0
  14. package/gui/react-jsx-runtime.js +19 -0
  15. package/gui/react.js +105 -0
  16. package/gui/theme.css +3053 -0
  17. package/index.ts +15 -0
  18. package/package.json +50 -4
  19. package/src/baba/check.ts +90 -0
  20. package/src/baba/config.ts +261 -0
  21. package/src/baba/find.ts +12 -0
  22. package/src/baba/init.ts +176 -0
  23. package/src/baba/node.ts +483 -0
  24. package/src/baba/project.ts +63 -0
  25. package/src/baba/registry.ts +35 -0
  26. package/src/baba/worker.ts +55 -0
  27. package/src/bench/index.ts +6 -0
  28. package/src/bench/measure.ts +132 -0
  29. package/src/bench/scenarios.ts +136 -0
  30. package/src/build/builder.ts +74 -0
  31. package/src/build/failure.ts +78 -0
  32. package/src/build/guard.ts +85 -0
  33. package/src/build/mdx-register.ts +3 -0
  34. package/src/build/mdx.ts +38 -0
  35. package/src/build/project.ts +38 -0
  36. package/src/build/views.ts +146 -0
  37. package/src/builder/main.ts +29 -0
  38. package/src/desktop/bob.ts +76 -0
  39. package/src/desktop/desktop.css +111 -0
  40. package/src/desktop/icons.ts +50 -0
  41. package/src/desktop/index.ts +323 -0
  42. package/src/desktop/routes.ts +98 -0
  43. package/src/desktop/view.tsx +673 -0
  44. package/src/door/core.ts +384 -0
  45. package/src/ecs/baba.ts +431 -0
  46. package/src/ecs/codec.ts +334 -0
  47. package/src/ecs/handles.ts +91 -0
  48. package/src/ecs/replica.ts +150 -0
  49. package/src/ecs/runtime.ts +603 -0
  50. package/src/ecs/scheduler.ts +75 -0
  51. package/src/ecs/snapshot.ts +102 -0
  52. package/src/ecs/state.ts +759 -0
  53. package/src/ecs/system.ts +256 -0
  54. package/src/ecs/table.ts +420 -0
  55. package/src/ecs/testbed.ts +97 -0
  56. package/src/exec/host.ts +177 -0
  57. package/src/exec/main.ts +98 -0
  58. package/src/exec/watch.ts +7 -0
  59. package/src/exec/wire.ts +29 -0
  60. package/src/generated/build.ts +4 -0
  61. package/src/gui/css.d.ts +1 -0
  62. package/src/gui/gui.tsx +245 -0
  63. package/src/gui/index.ts +51 -0
  64. package/src/gui/inspector.tsx +47 -0
  65. package/src/gui/levels.tsx +73 -0
  66. package/src/gui/promptware.tsx +68 -0
  67. package/src/gui/runner.tsx +118 -0
  68. package/src/gui/theme.css +498 -0
  69. package/src/gui/theme.ts +25 -0
  70. package/src/gui/wizard.tsx +227 -0
  71. package/src/guide/add-a-desktop.mdx +100 -0
  72. package/src/guide/compose-an-interface.mdx +84 -0
  73. package/src/guide/index.ts +13 -0
  74. package/src/guide/reach-outside.mdx +112 -0
  75. package/src/guide/spec-a-system.mdx +93 -0
  76. package/src/guide/systems-together.mdx +72 -0
  77. package/src/guide/write-a-system.mdx +183 -0
  78. package/src/guide/write-promptware.mdx +90 -0
  79. package/src/http/server.ts +310 -0
  80. package/src/kernel/build.ts +21 -0
  81. package/src/kernel/builder.ts +105 -0
  82. package/src/kernel/children.ts +117 -0
  83. package/src/kernel/context.ts +90 -0
  84. package/src/kernel/lock.ts +46 -0
  85. package/src/kernel/names.ts +14 -0
  86. package/src/kernel/schema.ts +130 -0
  87. package/src/kernel/where.ts +12 -0
  88. package/src/kit/index.ts +232 -0
  89. package/src/maker/system.ts +213 -0
  90. package/src/mcp/daemon.ts +61 -0
  91. package/src/mcp/main.ts +208 -0
  92. package/src/mcp/rpc.ts +64 -0
  93. package/src/mcp/tools.ts +125 -0
  94. package/src/prompt/evals.ts +42 -0
  95. package/src/prompt/index.ts +242 -0
  96. package/src/prompt/jsx-dev-runtime.ts +1 -0
  97. package/src/prompt/jsx-runtime.ts +49 -0
  98. package/src/prompt/mdx.d.ts +1 -0
  99. package/src/promptware/compile.ts +183 -0
  100. package/src/promptware/define.ts +16 -0
  101. package/src/promptware/disk.ts +72 -0
  102. package/src/promptware/markdown.d.ts +6 -0
  103. package/src/promptware/sync.ts +437 -0
  104. package/src/promptware/system.ts +215 -0
  105. package/src/runtime/bridge.ts +85 -0
  106. package/src/runtime/connect.ts +54 -0
  107. package/src/runtime/env.ts +35 -0
  108. package/src/runtime/harness.ts +80 -0
  109. package/src/runtime/main.ts +119 -0
  110. package/src/runtime/worker.ts +33 -0
  111. package/src/server/edge.ts +332 -0
  112. package/src/server/main.ts +45 -0
  113. package/src/server/messages.ts +97 -0
  114. package/src/server/protocol.ts +37 -0
  115. package/src/services/args.ts +45 -0
  116. package/src/services/exec.ts +69 -0
  117. package/src/services/fs.ts +139 -0
  118. package/src/services/http.ts +30 -0
  119. package/src/services/index.ts +113 -0
  120. package/src/services/secrets.ts +18 -0
  121. package/src/shell/address.ts +21 -0
  122. package/src/shell/args.ts +219 -0
  123. package/src/shell/client.ts +107 -0
  124. package/src/shell/codes.ts +26 -0
  125. package/src/shell/positional.ts +20 -0
  126. package/src/shell/run.ts +470 -0
  127. package/src/shell/service.ts +167 -0
  128. package/src/shell/state.ts +204 -0
  129. package/src/spec/adapters.ts +72 -0
  130. package/src/spec/diff.ts +26 -0
  131. package/src/spec/files.ts +17 -0
  132. package/src/spec/index.ts +155 -0
  133. package/src/spec/run.ts +97 -0
  134. package/src/spec/take.ts +54 -0
  135. package/src/test/index.ts +8 -0
  136. package/src/test/prove.ts +56 -0
  137. package/src/test/records.ts +23 -0
  138. package/src/test/specs.ts +56 -0
  139. package/src/test/steps.ts +100 -0
  140. package/src/test/voss-dir.ts +17 -0
  141. package/src/transport/messages.ts +110 -0
  142. package/src/transport/transport.ts +62 -0
  143. package/src/wall/probe.ts +67 -0
  144. package/src/wall/profile.ts +103 -0
  145. package/src/wall/spawn.ts +59 -0
  146. package/src/web/app.tsx +53 -0
  147. package/src/web/core.tsx +140 -0
  148. package/src/web/form.ts +155 -0
  149. package/src/web/hooks.ts +135 -0
  150. package/src/web/index.tsx +17 -0
  151. package/src/web/list.ts +19 -0
  152. package/src/web/maker.tsx +766 -0
  153. package/src/web/objects.tsx +213 -0
  154. package/src/web/socket.ts +84 -0
  155. package/src/web/state.tsx +69 -0
  156. package/src/web/store.ts +221 -0
  157. package/src/web/ui.tsx +135 -0
  158. package/README.md +0 -5
@@ -0,0 +1,431 @@
1
+ // The parts of a baba: the declarations a system's model is made of
2
+ // (components, asks, resources, effects), the handles a declaration makes,
3
+ // the assembled system the runtime runs, the baba that composes systems into
4
+ // one state, and the manifest, the contract as agents, the shell and the GUI
5
+ // see it. `system()` in system.ts is how a system is written.
6
+ // Pure: no IO, no Bun.
7
+ import { s, type Schema, type Infer, type Json, type ObjectOf } from "../kernel/schema.ts";
8
+ import type { Context } from "../kernel/context.ts";
9
+ import { keyFields, keyField } from "./handles.ts";
10
+ import { State, type Component, type Ask, type Resource, type Effect, type Model, type Entity, type Args, type Result, type MirrorEvent } from "./state.ts";
11
+ import { checkPositional } from "../shell/positional.ts";
12
+ import { render, type InterpretationSuite, type Prompt } from "../prompt/index.ts";
13
+ import { routesError } from "../desktop/routes.ts";
14
+
15
+ // ---- shapes --------------------------------------------------------------
16
+
17
+ /** A schema, or a bare object of schemas, which is an object schema. */
18
+ export type Shape = Schema<any> | Record<string, Schema<any>>;
19
+ export type SchemaOf<S> = S extends AcceptDef<infer T> ? SchemaOf<T> : S extends Schema<any> ? S : S extends Record<string, Schema<any>> ? Schema<ObjectOf<S>> : never;
20
+ export type Of<S> = Infer<SchemaOf<S>>;
21
+
22
+ /**
23
+ * An ask: a component any system may add to an entity, once, that its owner
24
+ * answers and removes. `components: { ask: accept({ prompt: s.string() }) }`
25
+ * is how a system says what it does for others; the asker puts the ask on its
26
+ * own entity and reads the answer there.
27
+ */
28
+ export interface AcceptDef<S extends Shape = Shape> { readonly isAccept: true; readonly shape: S }
29
+ export function accept<const S extends Shape>(shape: S): AcceptDef<S> { return { isAccept: true, shape }; }
30
+ const isAccept = (v: unknown): v is AcceptDef => typeof v === "object" && v !== null && (v as AcceptDef).isAccept === true;
31
+
32
+ const isSchema = (v: unknown): v is Schema<any> => typeof v === "object" && v !== null && typeof (v as Schema<any>).check === "function";
33
+ export function shape<S extends Shape>(v: S): SchemaOf<S> {
34
+ return (isSchema(v) ? v : s.object(v as Record<string, Schema<any>>)) as SchemaOf<S>;
35
+ }
36
+ const empty = s.object({});
37
+
38
+ // ---- declarations -----------------------------------------------------
39
+
40
+ /** A resource as declared: its shape and its value before anything happens. */
41
+ export interface ResourceDef<T = unknown> { readonly isResource: true; schema: Schema<T>; initial: T }
42
+ export function resource<S extends Shape>(sh: S, initial: Of<S>): ResourceDef<Of<S>> {
43
+ return { isResource: true, schema: shape(sh) as Schema<Of<S>>, initial };
44
+ }
45
+
46
+ /**
47
+ * An effect as declared: the port. `every` runs it for the system on a
48
+ * schedule; `retry` re-runs a failure; `repeat: false` says a run cut off by
49
+ * a crash must not run again: it lands as failed, "interrupted", instead.
50
+ */
51
+ export interface EffectDef<A = Record<string, never>, R = unknown, V = unknown, Mi extends string | null = string | null, L = unknown> {
52
+ readonly isEffect: true; args: Schema<A>; result: Schema<R>; retry: number | null; every: number | null; repeat: boolean; summary: string; run(args: A, ctx: Context): Promise<R> | R;
53
+ shape: "request" | "source"; mirror: string | null; event: Schema<V> | null;
54
+ // Typed by source(); any here so an Effect of one shape is an Effect of the general one.
55
+ list: ((args: any, ctx: Context) => Promise<unknown[]> | unknown[]) | null;
56
+ watch: ((args: any, ctx: Context, last: any) => AsyncIterable<unknown>) | null;
57
+ /** Types only: the component a mirror keeps, by name, and what its list answers, so the declaration can check both. */
58
+ readonly $mirror?: Mi;
59
+ readonly $items?: L;
60
+ }
61
+ const timing = (what: string, def: { retry?: number; every?: number }) => { for (const [k, v] of [["retry", def.retry], ["every", def.every]] as const) if (v !== undefined && !(Number.isInteger(v) && v > 0)) throw new Error(`${what}: ${k} is whole milliseconds, above zero`); };
62
+ export function effect<A = Record<string, never>, R = unknown>(def: { summary?: string; args?: Schema<A>; result?: Schema<R>; retry?: number; every?: number; repeat?: boolean; run(args: A, ctx: Context): Promise<R> | R }): EffectDef<A, R, unknown, null> {
63
+ timing("effect", def);
64
+ return { isEffect: true, shape: "request", mirror: null, event: null, list: null, watch: null, args: def.args ?? (empty as unknown as Schema<A>), result: def.result ?? (s.unknown() as Schema<R>), retry: def.retry ?? null, every: def.every ?? null, repeat: def.repeat ?? true, summary: def.summary ?? "", run: def.run };
65
+ }
66
+
67
+ /**
68
+ * A source: a job that keeps answering. `list` answers the slice of the
69
+ * outside as it is; `watch` yields its changes, with the last event it
70
+ * landed before, for a stream that can resume. With `mirror`, the system's
71
+ * component it keeps: items are that component's values by its key, a
72
+ * watch yields items, `{ gone: key }`, or `{ changed: true }` to list again;
73
+ * `every` lists again on a schedule. Without, a stream: `event` is the
74
+ * schema of what `watch` yields, read by a step with `w.events`.
75
+ */
76
+ export function source<A = Record<string, never>, V = unknown, const Mi extends string = never, L = unknown>(def: {
77
+ summary?: string; args?: Schema<A>; retry?: number; every?: number; repeat?: boolean;
78
+ mirror?: Mi; event?: Schema<V>;
79
+ list?: (args: A, ctx: Context) => Promise<L[]> | L[];
80
+ watch?: (args: A, ctx: Context, last: V | undefined) => AsyncIterable<V | MirrorEvent>;
81
+ }): EffectDef<A, void, V, [Mi] extends [never] ? null : Mi, L> {
82
+ timing("source", def);
83
+ if (!def.list && !def.watch) throw new Error("source: a list, a watch, or both");
84
+ if (def.mirror && def.event) throw new Error("source: a mirror keeps a component, so its events have the component's shape; event is for a stream");
85
+ if (!def.mirror && !def.watch) throw new Error("source: a stream watches; a source that only lists keeps a mirror");
86
+ return {
87
+ isEffect: true, shape: "source", mirror: def.mirror ?? null, event: def.event ?? (def.mirror ? null : (s.unknown() as Schema<V>)), watch: (def.watch as EffectDef["watch"]) ?? null, list: (def.list as EffectDef["list"]) ?? null,
88
+ args: def.args ?? (empty as unknown as Schema<A>), result: s.void() as Schema<void>, retry: def.retry ?? null, every: def.every ?? null, repeat: def.repeat ?? true, summary: def.summary ?? "",
89
+ run: () => { throw new Error("a source is driven by the runtime, not run"); },
90
+ };
91
+ }
92
+
93
+ /** The contract, by name: an action writes inside the round, a query reads between rounds; both take typed args and answer a typed value. */
94
+ export interface ActionDef<A = any, R = any, X = any> { readonly isAction: true; summary: string; args: Schema<A>; positional?: string[]; result: Schema<R>; run(w: State<X>, args: A): R }
95
+ export function action<const S extends Shape = {}, R = void, X = any>(def: { summary?: string; args?: S; positional?: (keyof Of<S> & string)[]; result?: Schema<R>; run(w: State<X>, args: Of<S>): R }): ActionDef<Of<S>, R, X> {
96
+ const args = (def.args ? shape(def.args) : empty) as Schema<Of<S>>;
97
+ return { isAction: true, summary: def.summary ?? "", args, ...(def.positional?.length ? { positional: checkPositional(args.json, def.positional) } : {}), result: def.result ?? (s.unknown() as Schema<R>), run: def.run };
98
+ }
99
+ export interface QueryDef<A = any, R = any, X = any> { readonly isQuery: true; summary: string; args: Schema<A>; positional?: string[]; result: Schema<R>; read(w: State<X>, args: A): R }
100
+ export function query<const S extends Shape = {}, R = unknown, X = any>(def: { summary?: string; args?: S; positional?: (keyof Of<S> & string)[]; result?: Schema<R>; read(w: State<X>, args: Of<S>): R }): QueryDef<Of<S>, R, X> {
101
+ const args = (def.args ? shape(def.args) : empty) as Schema<Of<S>>;
102
+ return { isQuery: true, summary: def.summary ?? "", args, ...(def.positional?.length ? { positional: checkPositional(args.json, def.positional) } : {}), result: def.result ?? (s.unknown() as Schema<R>), read: def.read };
103
+ }
104
+
105
+ /** A skill as the manifest carries it, rendered: what an agent reads when it opens it, and the calls it uses, by name. */
106
+ export interface SkillDef { description: string; body: string; uses?: string[]; eval?: InterpretationSuite }
107
+ /** What the baba tells agents, as the manifest carries it: its section of the context file, its skills, its hooks; the Markdown rendered. */
108
+ export interface Promptware { context: string | null; contextEval?: InterpretationSuite; skills: Record<string, SkillDef>; hooks: HookDef[] }
109
+
110
+ /** Which harnesses get the baba's files, and Claude Code's permissions; `table` renders the contract as a table of calls; `auto` writes the files on every change. */
111
+ export interface Harness { claude: { enabled: boolean; permissions: string[] }; codex: { enabled: boolean }; table: boolean; auto: boolean }
112
+ /** `harness` as a baba writes it: a harness is on or off, or Claude Code's with permissions added to voss's own. */
113
+ export interface HarnessInput { claude?: boolean | { enabled?: boolean; permissions?: string[] }; codex?: boolean | { enabled?: boolean }; table?: boolean; auto?: boolean }
114
+
115
+ /** The permission every baba gives Claude Code: its own calls. */
116
+ export const OWN_PERMISSION = "Bash(voss baba:*)";
117
+ export const defaultHarness: Harness = Object.freeze({ claude: { enabled: true, permissions: [OWN_PERMISSION] }, codex: { enabled: false }, table: false, auto: true });
118
+
119
+ /** The harness as given over the defaults, checked. */
120
+ export function harnessOf(h: HarnessInput | undefined): Harness {
121
+ const flag = (v: unknown, what: string): boolean => { if (typeof v !== "boolean") throw new Error(`baba: harness.${what} is true or false`); return v; };
122
+ const on = (v: boolean | { enabled?: boolean } | undefined, what: string) => v === undefined ? defaultHarness[what as "codex"].enabled : typeof v === "boolean" ? v : v.enabled === undefined ? true : flag(v.enabled, `${what}.enabled`);
123
+ const given = typeof h?.claude === "object" ? h.claude.permissions ?? [] : [];
124
+ if (!Array.isArray(given) || !given.every((p) => typeof p === "string")) throw new Error("baba: harness.claude.permissions is a list of strings");
125
+ return {
126
+ claude: { enabled: on(h?.claude, "claude"), permissions: [...new Set([OWN_PERMISSION, ...given])] },
127
+ codex: { enabled: on(h?.codex, "codex") },
128
+ table: h?.table === undefined ? defaultHarness.table : flag(h.table, "table"),
129
+ auto: h?.auto === undefined ? defaultHarness.auto : flag(h.auto, "auto"),
130
+ };
131
+ }
132
+ /** A hook as the manifest carries it. */
133
+ export interface HookDef { on: string; match: string | null; action: string; strict: boolean }
134
+
135
+ // ---- the system, assembled ------------------------------------------------
136
+
137
+ /** Where a system keeps what the framework knows of it: its handles are its only string keys, so no handle name is reserved. */
138
+ export const SYSTEM: unique symbol = Symbol.for("babavoss.system");
139
+
140
+ /** A step: synchronous, over the state alone, run once per round in the order the system lists it. */
141
+ export type Step<X = any> = (w: State<X>) => void;
142
+
143
+ /** How an effect's answers land, and for which entities it runs. */
144
+ export interface Binding<E = Effect, X = any> {
145
+ /** The entities to run it for, with the args of each; one job per entity, asked again only once answered. */
146
+ for?: (w: State<X>) => Iterable<readonly [Entity, Args<E>]>;
147
+ done?: (w: State<X>, entity: Entity | null, result: Result<E>) => void;
148
+ failed?: (w: State<X>, entity: Entity | null, error: string) => void;
149
+ }
150
+
151
+ type ComponentRefs<N extends string, C> = { readonly [K in keyof C & string]: C[K] extends AcceptDef<any> ? Ask<K, Of<C[K]>, N> : Component<K, Of<C[K]>, N> };
152
+ type ResourceRefs<N extends string, R> = { readonly [K in keyof R & string]: Resource<K, R[K] extends ResourceDef<infer T> ? T : never, N> };
153
+ type EffectRefs<N extends string, E> = { readonly [K in keyof E & string]: E[K] extends EffectDef<infer A, infer R, infer V> ? Effect<K, A, R, V, N> : never };
154
+ /** A system's handles by name, each branded with the system's name. */
155
+ export type Refs<N extends string, C, R, E> = ComponentRefs<N, C> & ResourceRefs<N, R> & EffectRefs<N, E>;
156
+
157
+ /** What assembling takes: the effects' bindings, the steps in order, the contract with its bodies. */
158
+ export interface Logic {
159
+ on?: Map<Effect, Binding>;
160
+ steps?: { name: string; run: Step }[];
161
+ actions?: Record<string, ActionDef>;
162
+ queries?: Record<string, QueryDef>;
163
+ }
164
+
165
+ /** What a contract entry takes and answers, by name. */
166
+ export type Calls<D> = { [K in keyof D & string]: D[K] extends { args: Schema<infer A>; result: Schema<infer R> } ? { args: A; result: R } : never };
167
+ /** A system's contract, as types: its actions and its queries by name, each its args and result. */
168
+ export interface Contract<A = any, Q = any> { readonly act: A; readonly read: Q }
169
+
170
+ /** What the runtime reads of a system, under SYSTEM. */
171
+ export interface SystemData {
172
+ readonly name: string;
173
+ readonly model: Model;
174
+ /** The systems it reads: their handles are the only other ones its pieces may touch. */
175
+ readonly reads: readonly System[];
176
+ readonly actions: Readonly<Record<string, ActionDef>>;
177
+ readonly queries: Readonly<Record<string, QueryDef>>;
178
+ readonly steps: readonly { name: string; run: Step }[];
179
+ readonly bindings: ReadonlyMap<Effect, Binding>;
180
+ }
181
+ /** Types only: the name, the handles, the systems read, the contract. */
182
+ interface Types<N extends string, X, R, C> { readonly name: N; readonly handles: X; readonly reads: R; readonly contract: C }
183
+
184
+ /** A system: its handles by name, and under SYSTEM what the runtime runs. `N` its name, `X` its handles, `R` the systems it reads, `C` its contract. */
185
+ export type System<N extends string = string, X = any, R = any, C = any> = X & { readonly [SYSTEM]: SystemData & { readonly $types?: Types<N, X, R, C> } };
186
+
187
+ /** What the framework knows of a system; a clear error for anything else, a copy of a system included, which loses its data. */
188
+ export function dataOf(v: unknown): SystemData {
189
+ const d = typeof v === "object" && v !== null ? (v as { [SYSTEM]?: SystemData })[SYSTEM] : undefined;
190
+ if (!d) throw new Error("not a system: its data is missing (was it cloned, spread or serialized?)");
191
+ return d;
192
+ }
193
+ /** A system's name. Never String(system): a system has no prototype, so it does not stringify. */
194
+ export const nameOf = (v: unknown): string => dataOf(v).name;
195
+
196
+ /** A name: a letter, then letters, digits and dashes; a system's is lowercase. */
197
+ const NAME = /^[a-z][a-zA-Z0-9-]*$/;
198
+ const SYSTEM_NAME = /^[a-z][a-z0-9-]*$/;
199
+ /** Words the shell keeps for itself: an action or a query may not take them. */
200
+ export const RESERVED = new Set(["start", "stop", "status", "state", "follow", "run", "raw", "secret", "help", "mcp", "check", "install", "add-system", "serve", "promptware", "init", "babas", "service", "gui"]);
201
+
202
+ /** What a declaration makes: its handles by name, on an object with no prototype, and the model they form. */
203
+ export interface Declared { readonly name: string; readonly refs: Readonly<Record<string, Component | Resource | Effect>>; readonly model: Model }
204
+
205
+ /** The handles of a model, checked: names, key fields, mirrors, initial values. */
206
+ export function declareModel(name: string, model: { components?: Record<string, Shape | AcceptDef>; resources?: Record<string, ResourceDef<any>>; effects?: Record<string, EffectDef<any, any>> }): Declared {
207
+ if (typeof name !== "string" || !SYSTEM_NAME.test(name)) throw new Error(`system ${JSON.stringify(name)}: lowercase letters, digits and dashes`);
208
+ const m: Model = { components: [], resources: [], effects: [] };
209
+ // No prototype: `toString` or `constructor` are names like any other, and `in` sees only what was declared.
210
+ const refs: Record<string, Component | Resource | Effect> = Object.create(null);
211
+ const word = (n: string, what: string) => {
212
+ // `entity` is every row's own field: a component under that name would hide it.
213
+ if (!NAME.test(n) || n === "entity") throw new Error(`${what} ${JSON.stringify(n)} of ${name}: a letter, then letters, digits and dashes; not entity`);
214
+ if (Object.hasOwn(refs, n)) throw new Error(`${name} declares ${n} twice`);
215
+ return n;
216
+ };
217
+ for (const [n, def] of Object.entries(model.components ?? {})) {
218
+ const sh = isAccept(def) ? def.shape : def;
219
+ const c: Component = Object.freeze({ kind: "component", name: word(n, "component"), owner: name, schema: shape(sh), accept: isAccept(def) });
220
+ const keys = keyFields(c.schema.json);
221
+ if (keys.length > 1) throw new Error(`component ${n} of ${name} has ${keys.length} key fields, ${keys.join(", ")}; at most one`);
222
+ refs[n] = c; m.components.push(c);
223
+ }
224
+ for (const [n, d] of Object.entries(model.resources ?? {})) { const r: Resource = Object.freeze({ kind: "resource", name: word(n, "resource"), owner: name, schema: d.schema, initial: d.initial }); refs[n] = r; m.resources.push(r); }
225
+ for (const [n, d] of Object.entries(model.effects ?? {})) {
226
+ let of: string | null = null;
227
+ if (d.mirror) {
228
+ const c = m.components.find((x) => x.name === d.mirror);
229
+ if (!c) throw new Error(`source ${n} of ${name} keeps ${d.mirror}, which is not a component of ${name}`);
230
+ if (keyField(c.schema.json) === null) throw new Error(`source ${n} of ${name} keeps ${d.mirror}, which has no key field; a mirror's items are put by their key`);
231
+ const props = (c.schema.json.properties ?? {}) as Record<string, Json>;
232
+ const ofs = Object.entries(props).filter(([, p]) => p.description === "an entity").map(([k]) => k);
233
+ if (ofs.length > 1) throw new Error(`source ${n} of ${name} keeps ${d.mirror}, which has ${ofs.length} entity fields; at most one, for the job's entity`);
234
+ of = ofs[0] ?? null;
235
+ if (m.effects.some((x) => x.mirror === d.mirror)) throw new Error(`${d.mirror} of ${name} is kept by two sources`);
236
+ }
237
+ const e: Effect = Object.freeze({ kind: "effect", name: word(n, "effect"), owner: name, args: d.args, result: d.result, retry: d.retry, every: d.every, repeat: d.repeat, summary: d.summary, run: d.run, shape: d.shape, mirror: d.mirror, of, event: d.event, list: d.list, watch: d.watch });
238
+ refs[n] = e; m.effects.push(e);
239
+ }
240
+ new State(m); // initial values checked
241
+ return { name, refs: Object.freeze(refs), model: m };
242
+ }
243
+
244
+ /** The system object, built while its pieces are made: its handles now, its data once `complete` runs. */
245
+ export function systemObject(d: Declared, reads: readonly System[]): { system: System; complete(l: Logic): System } {
246
+ const { name, refs, model: m } = d;
247
+ const o: Record<string | symbol, unknown> = Object.create(null);
248
+ for (const k of Object.keys(refs)) o[k] = refs[k];
249
+ let data: SystemData = { name, model: m, reads, actions: {}, queries: {}, steps: [], bindings: new Map() };
250
+ Object.defineProperty(o, SYSTEM, { get: () => data, enumerable: false });
251
+ return {
252
+ system: o as System,
253
+ complete(l) {
254
+ const actions: Record<string, ActionDef> = {};
255
+ const queries: Record<string, QueryDef> = {};
256
+ const call = (n: string, what: string) => {
257
+ if (!NAME.test(n)) throw new Error(`${what} ${JSON.stringify(n)} of ${name}: a letter, then letters, digits and dashes`);
258
+ if (RESERVED.has(n)) throw new Error(`${what} ${n} of ${name}: ${n} is a word of the shell (reserved: ${[...RESERVED].join(", ")}); name it otherwise`);
259
+ };
260
+ for (const [n, a] of Object.entries(l.actions ?? {})) { call(n, "action"); actions[n] = a; }
261
+ for (const [n, q] of Object.entries(l.queries ?? {})) {
262
+ call(n, "query");
263
+ if (n in actions) throw new Error(`${n} of ${name} is both an action and a query`);
264
+ queries[n] = q;
265
+ }
266
+ for (const e of l.on?.keys() ?? []) if (refs[e.name] !== e) throw new Error(`${name} binds ${e.name}, which is not one of its effects`);
267
+ data = Object.freeze({ name, model: m, reads, actions, queries, steps: l.steps ?? [], bindings: l.on ?? new Map() });
268
+ return Object.freeze(o) as System;
269
+ },
270
+ };
271
+ }
272
+
273
+ /** A system and every system it reads, and theirs: what a state of it needs. */
274
+ export function closureOf(systems: readonly System[]): System[] {
275
+ const out: System[] = [];
276
+ const visit = (sys: System) => { if (out.includes(sys)) return; out.push(sys); for (const r of dataOf(sys).reads) visit(r); };
277
+ for (const sys of systems) visit(sys);
278
+ return out;
279
+ }
280
+
281
+ // ---- the baba -----------------------------------------------------------
282
+
283
+ type UnionToIntersection<U> = (U extends unknown ? (x: U) => void : never) extends (x: infer I) => void ? I : never;
284
+ type ContractOf<S> = S extends { readonly [SYSTEM]: { readonly $types?: Types<any, any, any, infer C> } } ? C : never;
285
+ /** The actions of a system or a baba, by name, each its args and result: what a typed caller fires. */
286
+ export type ActOf<V> = V extends { readonly kind: "baba"; readonly systems: infer Ss extends readonly unknown[] } ? UnionToIntersection<{ [I in keyof Ss]: ActsOf<Ss[I]> }[number]> : ActsOf<V>;
287
+ /** The queries of a system or a baba, by name. */
288
+ export type ReadOf<V> = V extends { readonly kind: "baba"; readonly systems: infer Ss extends readonly unknown[] } ? UnionToIntersection<{ [I in keyof Ss]: ReadsOf<Ss[I]> }[number]> : ReadsOf<V>;
289
+ // A system typed `any` (a baba built from untyped systems) answers anything, as an untyped caller expects.
290
+ type Loose = Record<string, { args: any; result: any }>;
291
+ type ActsOf<S> = 0 extends 1 & S ? Loose : ContractOf<S> extends Contract<infer A, any> ? A : never;
292
+ type ReadsOf<S> = 0 extends 1 & S ? Loose : ContractOf<S> extends Contract<any, infer Q> ? Q : never;
293
+
294
+ /**
295
+ * An app: a kind of window the shell opens, declared by the baba as data. `icon` is a Lucide icon's
296
+ * name (`git-merge`); `group` heads it in the launcher; `many` lets the launcher open another window
297
+ * of it, without it an app has one window per session; `systems` names the systems it shows, so the
298
+ * Maker offers it over a state that has them. `routes` are the paths its windows may be at, `/pr/:n`, the
299
+ * last segment `*` for the rest; one takes `/`, its home. An app with routes is opened only at a path one of
300
+ * them takes, and the interface may give it a component per route; without routes any path is its own.
301
+ * Its component is the interface's, by key.
302
+ */
303
+ export interface AppSpec { key: string; title: string; group?: string; icon?: string; many?: boolean; systems?: readonly string[]; routes?: readonly string[] }
304
+ /** The routes of each app, as types: what `compose<typeof baba>` checks a table of components against. */
305
+ export type RoutesOf<Sc extends readonly AppSpec[]> = { [A in Sc[number] as A["key"]]: A extends { routes: readonly (infer P extends string)[] } ? P : never };
306
+
307
+ export interface Baba<Ss extends readonly System[] = readonly System[], K extends string = string, R = {}> {
308
+ readonly kind: "baba";
309
+ readonly systems: Ss;
310
+ /** The apps the shell offers, in launcher order; the interface gives each its component. */
311
+ readonly apps: readonly AppSpec[];
312
+ /** The app keys, as a type: what `compose<typeof baba>` asks a component for. */
313
+ readonly $app: K;
314
+ /** The routes of each app, as a type: what `compose<typeof baba>` asks a table of components for. */
315
+ readonly $routes: R;
316
+ /** What the baba tells agents: `context(...)`, `skill(...)` and `hook(...)` from babavoss/prompt, in order. */
317
+ readonly promptware: readonly Prompt[];
318
+ /** Which harnesses get the files, and how. */
319
+ readonly harness: Harness;
320
+ /** Milliseconds between rounds the runtime causes on its own, for steps that need time to pass; null for a baba moved by actions and jobs. */
321
+ readonly tick: number | null;
322
+ readonly model: Model;
323
+ readonly actions: Record<string, ActionDef & { owner: string }>;
324
+ readonly queries: Record<string, QueryDef & { owner: string }>;
325
+ }
326
+
327
+ export function baba<const Ss extends readonly System[], const Sc extends readonly AppSpec[] = readonly []>(def: { systems: Ss; apps?: Sc; tick?: number; promptware?: readonly Prompt[]; harness?: HarnessInput }): Baba<Ss, Sc[number]["key"], RoutesOf<Sc>> {
328
+ const harness = harnessOf(def.harness);
329
+ if (def.tick !== undefined && !(Number.isInteger(def.tick) && def.tick > 0)) throw new Error("baba: tick is whole milliseconds, above zero");
330
+ const model: Model = { components: [], resources: [], effects: [] };
331
+ const names = new Map<string, string>();
332
+ const systemNames = new Set<string>();
333
+ const systems = (def.systems as readonly unknown[]).map((v, i) => {
334
+ if (!isSystem(v)) throw new Error(`baba: systems[${i}] is not a system; a system's module exports it: export default system({ name, … }, (it, p) => [ … ])`);
335
+ return v;
336
+ });
337
+ for (const sys of systems) {
338
+ const c = dataOf(sys);
339
+ if (systemNames.has(c.name)) throw new Error(`two systems are named ${c.name}`);
340
+ systemNames.add(c.name);
341
+ for (const d of [...c.model.components, ...c.model.resources, ...c.model.effects]) {
342
+ const other = names.get(d.name);
343
+ if (other) throw new Error(`${d.name} is declared by both ${other} and ${c.name}`);
344
+ names.set(d.name, c.name);
345
+ }
346
+ model.components.push(...c.model.components);
347
+ model.resources.push(...c.model.resources);
348
+ model.effects.push(...c.model.effects);
349
+ }
350
+ for (const sys of systems) for (const r of dataOf(sys).reads) if (!systems.includes(r)) throw new Error(`${nameOf(sys)} reads ${nameOf(r)}, which is not one of the baba's systems`);
351
+ const actions: Baba["actions"] = {};
352
+ const queries: Baba["queries"] = {};
353
+ for (const sys of systems) {
354
+ const c = dataOf(sys);
355
+ for (const [n, a] of Object.entries(c.actions)) {
356
+ if (n in actions || n in queries) throw new Error(`${n} is an action or query of two systems`);
357
+ actions[n] = { ...(a as ActionDef), owner: c.name };
358
+ }
359
+ for (const [n, q] of Object.entries(c.queries)) {
360
+ if (n in actions || n in queries) throw new Error(`${n} is an action or query of two systems`);
361
+ queries[n] = { ...(q as QueryDef), owner: c.name };
362
+ }
363
+ }
364
+ // Scenes took the name apps: an old baba is told what to rename, not left with an empty desktop.
365
+ if ("scenes" in def) throw new Error("baba: `scenes` is now `apps`; rename it in baba({ … }) and in compose({ … }), and .baba/scenes/ to .baba/apps/");
366
+ const apps = (def.apps ?? []) as readonly AppSpec[];
367
+ if (new Set(apps.map((v) => v.key)).size !== apps.length || apps.some((v) => !/^[a-z][a-z0-9-]*$/.test(v.key))) throw new Error("baba: app keys must be unique lowercase names");
368
+ for (const v of apps) for (const n of v.systems ?? []) if (!systemNames.has(n)) throw new Error(`app ${v.key} shows ${n}, which is not one of the baba's systems`);
369
+ for (const v of apps) { const e = v.routes && routesError(v.routes); if (e) throw new Error(`app ${v.key}: ${e}`); }
370
+ return Object.freeze({ kind: "baba", systems: def.systems, apps: apps.map((v) => ({ ...v, ...(v.systems ? { systems: [...v.systems] } : {}), ...(v.routes ? { routes: [...v.routes] } : {}) })), $app: undefined as never, $routes: undefined as never, tick: def.tick ?? null, promptware: [...(def.promptware ?? [])], harness, model, actions, queries });
371
+ }
372
+
373
+ export function isBaba(v: unknown): v is Baba {
374
+ return typeof v === "object" && v !== null && (v as Baba).kind === "baba" && Array.isArray((v as Baba).systems);
375
+ }
376
+
377
+ export function isSystem(v: unknown): v is System {
378
+ return typeof v === "object" && v !== null && SYSTEM in v;
379
+ }
380
+
381
+ /** A single system as a baba of its own, with the systems it reads: what a testbed takes. */
382
+ export function ofSystem(c: System): Baba { return baba({ systems: closureOf([c]) }); }
383
+
384
+ // ---- the manifest --------------------------------------------------------
385
+
386
+ /** The contract as agents, the shell and the GUI see it: everything by name, with its owner. */
387
+ export interface Manifest {
388
+ systems: { name: string }[];
389
+ /** What the baba tells agents, rendered: its context, its skills, its hooks; voss's own skills after the baba's. */
390
+ promptware: Promptware;
391
+ harness: Harness;
392
+ components: Record<string, { owner: string; schema: Json; accept?: true }>;
393
+ resources: Record<string, { owner: string; schema: Json }>;
394
+ effects: Record<string, { owner: string; summary: string; args: Json; result: Json; retry: number | null; every: number | null; repeat: boolean; shape: "request" | "source"; mirror?: string; event?: Json }>;
395
+ actions: Record<string, Call>;
396
+ queries: Record<string, Call>;
397
+ tick: number | null;
398
+ /** The apps the shell offers; absent from a manifest an older voss wrote. */
399
+ apps?: AppSpec[];
400
+ }
401
+
402
+ /** One entry of the contract as the manifest carries it. */
403
+ export interface Call { owner: string; summary: string; args: Json; positional?: string[]; result: Json }
404
+
405
+ export function manifestOf(v: Baba, program = "voss baba"): Manifest {
406
+ const m: Manifest = { systems: v.systems.map((c) => ({ name: nameOf(c) })), promptware: { context: null, skills: {}, hooks: [] }, harness: v.harness, components: {}, resources: {}, effects: {}, actions: {}, queries: {}, tick: v.tick, apps: v.apps.map((x) => ({ ...x })) };
407
+ for (const c of v.model.components) m.components[c.name] = { owner: c.owner, schema: c.schema.json, ...(c.accept ? { accept: true as const } : {}) };
408
+ for (const r of v.model.resources) m.resources[r.name] = { owner: r.owner, schema: r.schema.json };
409
+ for (const e of v.model.effects) m.effects[e.name] = { owner: e.owner, summary: e.summary, args: e.args.json, result: e.result.json, retry: e.retry, every: e.every, repeat: e.repeat, shape: e.shape, ...(e.mirror ? { mirror: e.mirror } : {}), ...(e.event ? { event: e.event.json } : {}) };
410
+ const call = (d: (ActionDef | QueryDef) & { owner: string }): Call => ({ owner: d.owner, summary: d.summary, args: d.args.json, ...(d.positional ? { positional: d.positional } : {}), result: d.result.json });
411
+ for (const [n, a] of Object.entries(v.actions)) m.actions[n] = call(a);
412
+ for (const [n, q] of Object.entries(v.queries)) m.queries[n] = call(q);
413
+ // The promptware renders once the contract is in the manifest, since it may refer to it.
414
+ const refs: string[] = [];
415
+ m.promptware = renderPromptware(v.promptware, { manifest: m, program, refs });
416
+ for (const r of refs) if (!(r in m.promptware.skills)) throw new Error(`a skill says "see ${r}", which is not a skill of the baba`);
417
+ for (const [n, sk] of Object.entries(m.promptware.skills)) for (const u of sk.uses ?? []) if (!(u in m.actions) && !(u in m.queries)) throw new Error(`skill ${n} uses ${u}, which is not an action or query of the baba`);
418
+ for (const h of m.promptware.hooks) if (!(h.action in m.actions)) throw new Error(`hook on ${h.on} calls ${h.action}, which is not an action of the baba`);
419
+ return m;
420
+ }
421
+
422
+ /** The baba's promptware as Markdown: the one context, each skill by name, the hooks. */
423
+ export function renderPromptware(ps: readonly Prompt[], context: { manifest: Manifest; program: string; refs?: string[] }): Promptware {
424
+ const contexts = ps.filter((p) => p.kind === "context");
425
+ if (contexts.length > 1) throw new Error("a baba has one context(...); put every paragraph in it");
426
+ const text = contexts[0] ? render(contexts[0].body, context) : "";
427
+ const skills: Record<string, SkillDef> = {};
428
+ for (const p of ps) if (p.kind === "skill") { if (p.name in skills) throw new Error(`two skills are named ${p.name}`); skills[p.name] = { description: p.description, body: render(p.body, context), uses: p.uses, ...(p.eval ? { eval: p.eval } : {}) }; }
429
+ const hooks = ps.filter((p) => p.kind === "hook").map((p) => ({ on: p.on, match: p.match, action: p.action, strict: p.strict }));
430
+ return { context: text || null, skills, hooks, ...(contexts[0]?.eval ? { contextEval: contexts[0].eval } : {}) };
431
+ }