@unoverse-platform/studio 0.1.96 → 0.1.98

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 (118) hide show
  1. package/bin/studio.mjs +3 -3
  2. package/local/authoring.ts +1 -1
  3. package/local/catalog.ts +22 -2
  4. package/local/keys.ts +1 -1
  5. package/local/nodes.ts +29 -42
  6. package/local/plugin.ts +75 -13
  7. package/local/sample-identity/brand.yaml +24 -0
  8. package/local/sample-identity/organisation.yaml +14 -0
  9. package/local/sample-identity/purpose.yaml +14 -0
  10. package/local/sample-identity/story.yaml +18 -0
  11. package/local/scaffold.mjs +51 -22
  12. package/local/source.ts +4 -9
  13. package/package.json +1 -1
  14. package/screens/AgentsView.tsx +173 -0
  15. package/screens/ComponentsView.tsx +2 -2
  16. package/screens/EmptyLibrary.tsx +1 -1
  17. package/screens/IdentityHeader.tsx +1 -1
  18. package/screens/IdentityView.tsx +206 -0
  19. package/screens/PromptBlocksView.tsx +1 -1
  20. package/screens/SourceView.tsx +1 -1
  21. package/screens/TemplatesLibraryView.tsx +91 -6
  22. package/screens/TemplatesView.tsx +24 -9
  23. package/screens/index.ts +5 -2
  24. package/src/App.tsx +14 -4
  25. package/src/NodeKeys.tsx +1 -1
  26. package/src/host.ts +3 -16
  27. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  28. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  29. package/vendor/base/src/agent-mcp/index.ts +5 -5
  30. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  31. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  32. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  33. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  34. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  35. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  36. package/vendor/base/src/agent-mcp/types.ts +47 -10
  37. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  38. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  39. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  40. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  41. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  42. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  43. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  44. package/vendor/base/src/definitions/load/load.ts +37 -28
  45. package/vendor/base/src/definitions/model.ts +8 -12
  46. package/vendor/base/src/definitions/registry/agents.ts +111 -0
  47. package/vendor/base/src/definitions/registry/catalog.ts +28 -12
  48. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  49. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  50. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  51. package/vendor/base/src/definitions/registry/orgDocuments.ts +107 -0
  52. package/vendor/base/src/definitions/registry/orgPack.ts +50 -0
  53. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  54. package/vendor/base/src/definitions/theme.ts +1 -2
  55. package/vendor/base/src/items/collect.ts +33 -25
  56. package/vendor/base/src/items/publish.ts +31 -7
  57. package/vendor/base/src/lint/context.mjs +17 -0
  58. package/vendor/base/src/lint/design/file.mjs +199 -2
  59. package/vendor/base/src/lint/design/index.mjs +66 -73
  60. package/vendor/base/src/lint/design/vocabulary.mjs +6 -3
  61. package/vendor/base/src/lint/design/walk.mjs +14 -4
  62. package/vendor/base/src/lint/index.mjs +15 -9
  63. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  64. package/vendor/base/src/lint/nodes/index.mjs +6 -1
  65. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  66. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  67. package/vendor/base/src/lint/objects/index.mjs +324 -0
  68. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  69. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  70. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  71. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  72. package/vendor/base/src/manifests/compose.ts +81 -10
  73. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  74. package/vendor/base/src/manifests/executor/context.ts +12 -1
  75. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  76. package/vendor/base/src/manifests/executor/session.ts +50 -0
  77. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  78. package/vendor/base/src/manifests/index.ts +1 -1
  79. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  80. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  81. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  82. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  83. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  86. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  87. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  88. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  89. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  90. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  91. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  92. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  93. package/vendor/base/src/manifests/source.ts +1 -1
  94. package/vendor/base/src/paths.ts +16 -8
  95. package/vendor/base/src/platform/audioManager.ts +32 -0
  96. package/vendor/base/src/platform/executionContext.ts +14 -0
  97. package/vendor/base/src/platform/liveInput.ts +52 -0
  98. package/vendor/base/src/plugins/README.md +1 -1
  99. package/vendor/base/src/plugins/discovery.ts +1 -1
  100. package/vendor/base/src/template/SafeExpression.ts +6 -0
  101. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  102. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  103. package/vendor/sdk/appEngine.tsx +31 -12
  104. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  105. package/vendor/sdk/lib/app.tsx +70 -95
  106. package/vendor/sdk/lib/connection.tsx +47 -57
  107. package/vendor/sdk/lib/core/actions.ts +6 -0
  108. package/vendor/sdk/lib/core/apps.ts +66 -81
  109. package/vendor/sdk/lib/core/client.ts +5 -14
  110. package/vendor/sdk/lib/core/connection.ts +30 -0
  111. package/vendor/sdk/lib/core/index.ts +2 -2
  112. package/vendor/sdk/lib/core/store.ts +90 -15
  113. package/vendor/sdk/lib/core/types.ts +9 -8
  114. package/vendor/sdk/lib/primitives.tsx +113 -4
  115. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  116. package/vendor/sdk/lib/render.tsx +3 -3
  117. package/vendor/sdk/lib/voice.tsx +27 -13
  118. package/vite.config.ts +1 -1
@@ -11,6 +11,12 @@ export interface UnoverseDefinition {
11
11
  * `template` is the Template Model kind (UNOVERSE_TEMPLATE_MODEL.md). */
12
12
  kind: "component" | "app" | "atom" | "template";
13
13
  name: string;
14
+ /** APPS: the places the app's layouts declare, in the order first drawn. The screen puts
15
+ * interfaces into them by name (docs/unoverse/design/screen/UNOVERSE_SCREEN.md §2.2). */
16
+ places?: { name: string; holds: "one" | "many" }[];
17
+ /** COMPONENTS AND TEMPLATES: the place each state names. An interface entering a state
18
+ * that names a place moves there (UNOVERSE_SCREEN.md §2.3). */
19
+ statePlaces?: Record<string, string>;
14
20
  /** TEMPLATES (§9b RULED 2026-08-29): the compiled SECTIONS of each state's
15
21
  * layout — one entry per `static:` / `copywriter:` / `director:` line, in
16
22
  * arrangement order. `static`/`copywriter` link a component (face = its
@@ -89,8 +95,8 @@ export interface UnoverseDefinition {
89
95
  * `defaultState` seeds it so the SDK renders that face on arrival (merged beneath
90
96
  * live data). Server-composed from the manifest, never hand-written. */
91
97
  state?: Record<string, unknown>;
92
- /** COMPONENTS: projected capability flag — true iff the component folder carries a
93
- * manifest.json (spatial-capable). Derived from presence, never authored in a def. */
98
+ /** COMPONENTS and TEMPLATES: projected capability flag — true iff the folder carries a
99
+ * manifest (offerable to the map by its switch). Derived from presence, never authored. */
94
100
  spatial?: boolean;
95
101
  /** Display icon URL (from the manifest; catalogs/drawers only). */
96
102
  icon?: string;
@@ -147,16 +153,6 @@ export interface UnoverseDefinition {
147
153
  * ladder and arrival-scan order, from the manifest's `stateOrder`. First entry
148
154
  * outranks all below it; no name is special. Absent → legacy recency semantics. */
149
155
  stateOrder?: string[];
150
- /** THE LATCH, PER STATE (MCP guide §The Component Latch, moved here from the manifest
151
- * 2026-08-31): the states whose wearer the conversation may be ADDRESSED AT, keyed by
152
- * state name. Compiled from each state's own `latch:` block and stamped beside
153
- * `stateOrder`, because states themselves are compiled away into `layouts`.
154
- *
155
- * A state absent from this map can never hold the latch, so the SAME instance
156
- * retracted from its page state to an inline one drops the pill by construction —
157
- * which is the thing a manifest-level declaration could not say. `resolveLatch` reads
158
- * this map; nothing about the latch travels on the wire with a render. */
159
- latches?: Record<string, { title: string; icon?: string }>;
160
156
  /** COMPONENTS (STATE_MODEL v2 §5 rule 2): the PUBLIC MENU — the top-level state
161
157
  * names of the authored `state.view` tree, compiled at serve time. What hosts may
162
158
  * place at spawn and templates may react to; nested substates never appear here. */
@@ -0,0 +1,111 @@
1
+ /**
2
+ * AGENTS, READ (docs.unoverse.ai/design/tasks; docs/unoverse/data/objects/elements/task.md, ruled 2026-09-06).
3
+ *
4
+ * An Agent is a workflow with no interface of its own, listed on the map so a conversation
5
+ * can reach for it. It is the ONE thing authored for the map alone: every other asset (a
6
+ * component, a template, an app) is offered to the map by its Available switch and keeps
7
+ * its meta on its own manifest, never written twice. One file per Agent:
8
+ *
9
+ * design/<org>/agents/<name>.yaml type: agent, the discovery meta, the door
10
+ *
11
+ * The door is a canvas in the org, its Input Trigger, and the node whose output is the
12
+ * result; all three, always. The face while it runs is the shared agent-card unless the
13
+ * file names another. The input schema is the trigger's, declared on the canvas.
14
+ *
15
+ * Like the identity reader beside it, this is NOT a definition kind: nothing here composes
16
+ * a UI tree. It only reads, so Studio can list and the registry lane can project.
17
+ */
18
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
19
+ import { join } from "node:path";
20
+ import { parse } from "yaml";
21
+ import { listOrgs, projectDir } from "./homes.js";
22
+
23
+ export const AGENT_CARD = "unoverse://components/agent-card";
24
+
25
+ export interface AgentFile {
26
+ /** `<org>/<name>`: the identity every registry row and toggle keys on. */
27
+ name: string;
28
+ org: string;
29
+ /** The bare file name, which is also the tool's name on the wire. */
30
+ id: string;
31
+ title?: string;
32
+ description?: string;
33
+ whenToUse?: string;
34
+ /** What to do: the steps the Agent follows. Rides the tool description. */
35
+ instructions?: string;
36
+ /** The skills the Agent needs, by name. */
37
+ skills?: string[];
38
+ category?: string;
39
+ version?: string;
40
+ /** What the face says while the Agent runs. */
41
+ message?: string;
42
+ /** The face while it runs. Defaults to the shared agent-card. */
43
+ app: string;
44
+ /** The door. */
45
+ workflow?: string;
46
+ trigger?: string;
47
+ output?: string;
48
+ /** Where it lives, relative to design/<org>/. */
49
+ path: string;
50
+ }
51
+
52
+ /** design/<org>/agents/ */
53
+ export const agentsDir = (root: string): string => join(root, "agents");
54
+
55
+ const isDef = (f: string): boolean => /\.ya?ml$/.test(f) && !f.startsWith(".");
56
+
57
+ /** One Agent file, by its bare name. Null when the file is missing or is not an Agent. */
58
+ export function readAgent(root: string, org: string, id: string): AgentFile | null {
59
+ const dir = agentsDir(root);
60
+ const file = [join(dir, `${id}.yaml`), join(dir, `${id}.yml`)].find((f) => existsSync(f));
61
+ if (!file) return null;
62
+ const json = (parse(readFileSync(file, "utf8")) as Record<string, any>) ?? {};
63
+ if (json.type !== "agent") return null;
64
+ const str = (v: unknown): string | undefined => (typeof v === "string" && v.trim() ? v : undefined);
65
+ return {
66
+ name: `${org}/${id}`,
67
+ org,
68
+ id,
69
+ title: str(json.title),
70
+ description: str(json.description),
71
+ whenToUse: str(json.whenToUse),
72
+ instructions: str(json.instructions),
73
+ ...(Array.isArray(json.skills) ? { skills: json.skills.filter((x: unknown): x is string => typeof x === "string" && !!x.trim()) } : {}),
74
+ category: str(json.category),
75
+ version: str(json.version),
76
+ message: str(json.message),
77
+ app: str(json.app) ?? AGENT_CARD,
78
+ workflow: str(json.workflow),
79
+ trigger: str(json.trigger),
80
+ output: str(json.output),
81
+ path: `agents/${file.slice(dir.length + 1)}`,
82
+ };
83
+ }
84
+
85
+ /** Every Agent an org authored, sorted by name. */
86
+ export function listAgentsIn(root: string, org: string): AgentFile[] {
87
+ const dir = agentsDir(root);
88
+ if (!existsSync(dir)) return [];
89
+ return readdirSync(dir, { withFileTypes: true })
90
+ .filter((e) => e.isFile() && isDef(e.name))
91
+ .map((e) => readAgent(root, org, e.name.replace(/\.ya?ml$/, "")))
92
+ .filter((a): a is AgentFile => a !== null)
93
+ .sort((a, b) => a.name.localeCompare(b.name));
94
+ }
95
+
96
+ /** Every Agent across the workspace, or one org's. */
97
+ export function listAgents(org?: string): AgentFile[] {
98
+ const orgs = org && org !== "all" ? [org] : listOrgs();
99
+ return orgs.flatMap((o) => listAgentsIn(projectDir(o), o));
100
+ }
101
+
102
+ /** An Agent by its qualified ref (`<org>/<name>`), the form every toggle and row carries. */
103
+ export function loadAgent(ref: string): AgentFile | null {
104
+ const slash = ref.indexOf("/");
105
+ if (slash <= 0) return null;
106
+ const org = ref.slice(0, slash);
107
+ return readAgent(projectDir(org), org, ref.slice(slash + 1));
108
+ }
109
+
110
+ /** The door is three fields, always. */
111
+ export const hasDoor = (a: AgentFile): boolean => Boolean(a.workflow && a.trigger && a.output);
@@ -100,8 +100,12 @@ export interface AppManifest {
100
100
  * NOT `type`, which is the legacy full-surface/inline ROUTER word (`type: "template"`
101
101
  * means a fluid-height app, and says nothing about where its definition lives). Two
102
102
  * meanings on one field is how a template app would come to be loaded as a component and
103
- * fail with a resolver error nobody could read. */
104
- kind?: "component" | "template";
103
+ * fail with a resolver error nobody could read.
104
+ *
105
+ * "app" joined the union when APPS started naming themselves as their own deliverable
106
+ * (`loadAppManifest`): an app's definition lives in the app tier, so the same rule that
107
+ * keeps a template from loading as a component keeps an app from doing it too. */
108
+ kind?: "component" | "template" | "app";
105
109
  /** Human display name; falls back to `name`. */
106
110
  title?: string;
107
111
  description?: string;
@@ -171,12 +175,6 @@ export interface AppManifest {
171
175
  * the CONVERSATION (not the turn) so every re-call hydrates the same slice, and the
172
176
  * client's new-turn reset skips it — it stays on screen until replaced or closed. */
173
177
  lifetime?: "turn" | "conversation";
174
- /* NO `latch` HERE (moved 2026-08-31, MCP guide §The Component Latch). It was a manifest
175
- * field seeded onto the slice with every render, which said one thing about a component
176
- * for the whole of its life; a page is addressable when it is THE PAGE and not when it
177
- * is a summary in the flow, and the state is what knows that. It is now declared on the
178
- * app's state and compiled to `def.latches` — a fact the client looks up, not one the
179
- * server has to send. */
180
178
  /** Workbench MOCK: what each STATE's preview seeds — `{ "<state>": ["course-card",
181
179
  * "course-card"] }` (a repeated name = several instances, e.g. a card rail). The
182
180
  * author decides per state; the seeded instances are written INTO that state's view
@@ -287,7 +285,24 @@ export function loadAppManifest(ref: string): AppManifest | null {
287
285
  m.autoTrigger === undefined && typeof m.binding?.autoTrigger === "boolean"
288
286
  ? { autoTrigger: m.binding.autoTrigger }
289
287
  : {};
290
- return normalizeMode({ ...lifted, ...bindingAuto, ...m, id, org: hit.org, states: listStates("app", `${hit.org}/${id}`) });
288
+ // THE DELIVERABLE IS THE APP, and saying so is what lets a called app open.
289
+ // Both siblings below already default this to themselves (`loadComponentApp`,
290
+ // the template loader); the app loader never did, so `app.previewComponents?.[0]`
291
+ // was undefined and the app tool's whole render block — gated on that ref — was
292
+ // skipped. The call returned cleanly, nothing reached /stream, and the model told
293
+ // the person to use a surface that had never appeared (SAB sab-voice, 2026-09-07).
294
+ // `kind` rides along for the same reason it does on a template: the render path
295
+ // resolves the definition in the tier `kind` names, and an app is not a component.
296
+ return normalizeMode({
297
+ ...lifted,
298
+ ...bindingAuto,
299
+ ...m,
300
+ id,
301
+ org: hit.org,
302
+ kind: m.kind ?? "app",
303
+ previewComponents: m.previewComponents?.length ? m.previewComponents : [`${hit.org}/${id}`],
304
+ states: listStates("app", `${hit.org}/${id}`),
305
+ });
291
306
  }
292
307
 
293
308
  /** Load a COMPONENT as a native MCP app (UNOVERSE_MCP_APP_PROTOCOL §0.1 path B): a
@@ -352,9 +367,10 @@ export function loadTemplateApp(id: string, org?: string): AppManifest | null {
352
367
  // Template manifests carry `title`, never `name` (the folder is the name).
353
368
  name: m.name ?? m.title ?? lower,
354
369
  kind: "template",
355
- // A DURABLE SURFACE by nature: a composed page is refined across turns, and every
356
- // refinement must hydrate the same instance rather than stack a new page per message.
357
- lifetime: m.lifetime ?? "conversation",
370
+ // TURN unless the manifest opts in (docs.unoverse.ai/design/state, "Arrival and
371
+ // lifetime"). A durable slice is exempt from the same-state replace, so a template
372
+ // defaulted durable was never replaced by the next arrival in its state.
373
+ lifetime: m.lifetime ?? "turn",
358
374
  // The deliverable IS this template — the render path resolves the definition from this
359
375
  // ref, in the tier `kind` names.
360
376
  previewComponents: m.previewComponents?.length ? m.previewComponents : [org ? `${org}/${lower}` : lower],
@@ -0,0 +1,149 @@
1
+ /**
2
+ * A DATASET'S TABLE, AS SQL (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18).
3
+ *
4
+ * The declaration in an object's folder is the schema, so nobody writes SQL and nobody edits a
5
+ * schema by hand. This turns that declaration into a migration: a file a person reads, approves
6
+ * and can reverse. Nothing here runs anything.
7
+ *
8
+ * WHERE IT GOES. Beside the declaration, as `dataset.sql`, and never into the migrations folder:
9
+ * a dataset's table is made at publish by the platform's one door, and a file kept where the
10
+ * migration runner finds it would be a step someone has to run. This is what ran, written down,
11
+ * so a schema change can be read and compared and is never invisible.
12
+ *
13
+ * THE POLICIES ARE PART OF THE TABLE, not a later step. They are written in the same statement
14
+ * block, so a table cannot exist without them. A table created today and secured on Thursday is
15
+ * a table that was open until Thursday.
16
+ */
17
+ import type { DatasetColumn, DatasetDefinition } from "./objects.js";
18
+
19
+ /** What each declared type becomes. `number` is NUMERIC: exact decimal, never floating point,
20
+ * so a total that must reconcile against a stated figure does not drift. */
21
+ const SQL_TYPE: Record<string, string> = {
22
+ string: "TEXT",
23
+ number: "NUMERIC",
24
+ integer: "BIGINT",
25
+ date: "DATE",
26
+ datetime: "TIMESTAMPTZ",
27
+ boolean: "BOOLEAN",
28
+ };
29
+
30
+ /**
31
+ * THE TABLE A DATASET IS STORED IN. Built here, never taken from input, and never able to name a
32
+ * table the platform already owns: every one begins `dataset_`.
33
+ *
34
+ * THE ORG IS PART OF THE NAME. Two organisations may each declare an object called the same
35
+ * thing, with different columns, and without the org in the name the second would find the
36
+ * first's table already there and quietly write into it. A marketplace object, belonging to no
37
+ * org, keeps the bare name.
38
+ *
39
+ * The squeeze to letters, digits and underscores is what makes the name safe; it is also what
40
+ * would let "my-thing" and "my_thing" become one table, so the lint refuses two objects whose
41
+ * names arrive at the same table (rules.mjs).
42
+ */
43
+ const squeeze = (s: string): string => s.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
44
+ export const tableNameOf = (object: string, org?: string | null): string =>
45
+ org ? `dataset_${squeeze(org)}_${squeeze(object)}` : `dataset_${squeeze(object)}`;
46
+
47
+ // THE DESCRIPTION GOES ABOVE THE COLUMN, never after it: a trailing `-- comment` swallows the
48
+ // comma that separates one column from the next, and the statement stops parsing.
49
+ // A WORD THAT IS THE LANGUAGE IS CHECKED BY THE DATABASE. Without this, "exposure", "Exposure"
50
+ // and "exposures" are three measures that never add up with each other, and nothing says so: the
51
+ // rows are all there, and every total is quietly short. The list is quoted here and comes from a
52
+ // declaration the lint already refused if it was malformed.
53
+ const allowedList = (c: DatasetColumn): string =>
54
+ (c.enum ?? []).map((v) => `'${v.replace(/'/g, "''")}'`).join(", ");
55
+
56
+ /**
57
+ * THE WORDS A COLUMN ALLOWS, as the database will hold them. Named here and nowhere else, so the
58
+ * table a migration makes and the table the one door brings forward are checked the same way: a
59
+ * check written twice is a check that drifts, and drift here is a word that passes in one table
60
+ * and is refused in another.
61
+ *
62
+ * EMPTY IS NOT A WORD, and that is the point: a column whose words are closed still takes nothing
63
+ * at all, so a reading we have no word for yet is kept with the document's own wording and no word
64
+ * of ours (dataset.md, "The language"). `IN` passes NULL of its own accord, which is why this does
65
+ * not have to say so.
66
+ */
67
+ export const checkOf = (c: DatasetColumn): string | null =>
68
+ c.enum?.length ? `(${c.name} IN (${allowedList(c)}))` : null;
69
+
70
+ /** What that check is called on a table. One name per column, so bringing it forward replaces it. */
71
+ export const checkNameOf = (table: string, column: string): string => `chk_${table}_${column}`;
72
+
73
+ /**
74
+ * ONE ROW PER THING MEASURED. The identity a declaration gives its rows, as the index that
75
+ * enforces it. NULLS ARE DISTINCT here, as Postgres has it by default: a row whose identity is
76
+ * partly unknown collides with nothing, and two readings we have no word for yet are two rows.
77
+ */
78
+ export const uniqueIndexOf = (object: string, d: DatasetDefinition, org?: string | null): string | null => {
79
+ if (!d.uniqueBy?.length) return null;
80
+ const t = tableNameOf(object, org);
81
+ return `CREATE UNIQUE INDEX IF NOT EXISTS idx_${t}_item ON ${t} (COALESCE(org, ''), of_id, ${d.uniqueBy.join(", ")})`;
82
+ };
83
+
84
+ /** The columns that index is over, in order, as bare names: what it is compared against. */
85
+ export const uniqueColumnsOf = (d: DatasetDefinition): string[] => ["org", "of_id", ...(d.uniqueBy ?? [])];
86
+
87
+ const column = (c: DatasetColumn): string => {
88
+ const said = c.description ? ` -- ${c.description.replace(/\s+/g, " ")}\n` : "";
89
+ const only = checkOf(c) ? ` CHECK ${checkOf(c)}` : "";
90
+ return `${said} ${c.name} ${SQL_TYPE[c.type]}${c.required ? " NOT NULL" : ""}${only}`;
91
+ };
92
+
93
+ /**
94
+ * The migration for one dataset. Idempotent: every statement is IF NOT EXISTS, so applying it
95
+ * again does nothing, which is what lets a migration be re-run safely after a failure.
96
+ */
97
+ export function datasetMigration(object: string, d: DatasetDefinition, org?: string | null): string {
98
+ const t = tableNameOf(object, org);
99
+ const cols = d.columns.map(column).join(",\n");
100
+ const indexed = d.columns.filter((c) => c.indexed);
101
+ const lines: string[] = [];
102
+
103
+ lines.push(`-- THE ${object.toUpperCase()} DATASET (docs/unoverse/data/objects/dataset.md).`);
104
+ lines.push(`-- Generated from ${org ? `design/${org}` : "design"}/objects/${object}/dataset.yaml. Do not edit by hand:`);
105
+ lines.push(`-- edit the declaration and generate it again, so the folder and the database never disagree.`);
106
+ if (d.description) lines.push(`--\n-- ${d.description.replace(/\s+/g, " ")}`);
107
+ lines.push("");
108
+ lines.push(`CREATE TABLE IF NOT EXISTS ${t} (`);
109
+ lines.push(` id UUID PRIMARY KEY DEFAULT gen_random_uuid(),`);
110
+ lines.push(` -- WHOSE IT IS, and what it belongs to. Not the author's to name, and not optional:`);
111
+ lines.push(` -- these are what the access policies stand on and what makes a delete complete.`);
112
+ lines.push(` org VARCHAR(64),`);
113
+ lines.push(` of_id TEXT NOT NULL REFERENCES dictionary_need_states(universal_id) ON DELETE CASCADE,`);
114
+ lines.push(` -- WHAT IT WAS READ FROM. A row outlives neither the thing it belongs to nor the`);
115
+ lines.push(` -- document it came from: delete either and it goes, with no sweep to remember.`);
116
+ lines.push(` document_id TEXT REFERENCES dictionary_need_states(universal_id) ON DELETE CASCADE,`);
117
+ lines.push(cols + ",");
118
+ lines.push(` created_at TIMESTAMPTZ NOT NULL DEFAULT now(),`);
119
+ lines.push(` updated_at TIMESTAMPTZ NOT NULL DEFAULT now()`);
120
+ lines.push(`);`);
121
+ lines.push("");
122
+ lines.push(`-- THE POLICY'S OWN COLUMNS ARE INDEXED, or the policy filters every row of every query.`);
123
+ lines.push(`CREATE INDEX IF NOT EXISTS idx_${t}_of ON ${t} (org, of_id);`);
124
+ lines.push(`CREATE INDEX IF NOT EXISTS idx_${t}_document ON ${t} (document_id);`);
125
+ for (const c of indexed) lines.push(`CREATE INDEX IF NOT EXISTS idx_${t}_${c.name} ON ${t} (${c.name});`);
126
+ const unique = uniqueIndexOf(object, d, org);
127
+ if (unique) {
128
+ lines.push("");
129
+ lines.push(`-- ONE ROW PER THING MEASURED: a read again replaces its rows rather than adding a second`);
130
+ lines.push(`-- set beside them, so a total never counts one reading twice.`);
131
+ lines.push(`${unique};`);
132
+ }
133
+ lines.push("");
134
+ lines.push(`-- ACCESS IS THE DATABASE'S, NEVER THE QUERY'S (dataset.md). A read function of an object`);
135
+ lines.push(`-- may be called by a component, and a component is a browser: a filter written inside`);
136
+ lines.push(`-- that function is one missed condition from leaking. A policy is not.`);
137
+ lines.push(`--`);
138
+ lines.push(`-- The context is set PER TRANSACTION (SET LOCAL), never per connection: with a pooler,`);
139
+ lines.push(`-- a setting that outlives its transaction hands the next request the last one's org.`);
140
+ lines.push(`-- And the application must not own this table: an owner ignores every policy silently.`);
141
+ lines.push(`ALTER TABLE ${t} ENABLE ROW LEVEL SECURITY;`);
142
+ lines.push(`ALTER TABLE ${t} FORCE ROW LEVEL SECURITY;`);
143
+ lines.push(`DROP POLICY IF EXISTS ${t}_org ON ${t};`);
144
+ lines.push(`CREATE POLICY ${t}_org ON ${t}`);
145
+ lines.push(` USING (org IS NOT DISTINCT FROM NULLIF(current_setting('app.org', true), ''))`);
146
+ lines.push(` WITH CHECK (org IS NOT DISTINCT FROM NULLIF(current_setting('app.org', true), ''));`);
147
+ lines.push("");
148
+ return lines.join("\n") + "\n";
149
+ }
@@ -9,7 +9,7 @@ import { join, dirname } from "path";
9
9
  import { fileURLToPath } from "url";
10
10
  import { packagedDesignSystem } from "../dsPackage.js";
11
11
  import { defPath } from "../fsCache.js";
12
- import { NODES_HOME, DESIGN_HOME, INSTALLED_HOME, databaseOnly, designDir } from "../../paths.js";
12
+ import { DESIGN_HOME, INSTALLED_HOME, databaseOnly, designDir } from "../../paths.js";
13
13
 
14
14
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
15
 
@@ -63,8 +63,8 @@ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
63
63
  * or nodes, all of which are rows". A dead path that would silently adopt an npm tree if
64
64
  * one ever appeared is worse than no path. */
65
65
  const MARKETPLACE = join(DESIGN, "marketplace");
66
- const DS_BUNDLE_CANDIDATES = [join(NODES_HOME, "marketplace", "definitions")];
67
- function marketplaceDir(kind: "components" | "atoms" | "templates"): string {
66
+ const DS_BUNDLE_CANDIDATES: string[] = [];
67
+ function marketplaceDir(kind: "components" | "atoms" | "templates" | "objects" | "pipelines"): string {
68
68
  const onDisk = join(MARKETPLACE, kind);
69
69
  // Under the switch the authored tiers are skipped, so what a deployed universe would
70
70
  // resolve is what a developer resolves.
@@ -95,7 +95,15 @@ export const SHARED_DIR = {
95
95
  get template() {
96
96
  return marketplaceDir("templates");
97
97
  },
98
- } as Record<"component" | "atom" | "template", string>;
98
+ // `object` (2026-09-08): the kinds of thing that live on the map, docs/unoverse/data/objects/README.md.
99
+ get object() {
100
+ return marketplaceDir("objects");
101
+ },
102
+ // `pipeline` (2026-09-09): the source side of ingestion, docs/unoverse/data/pipelines/README.md layer 1.
103
+ get pipeline() {
104
+ return marketplaceDir("pipelines");
105
+ },
106
+ } as Record<"component" | "atom" | "template" | "object" | "pipeline", string>;
99
107
 
100
108
  // rx-root entries that are NOT projects: the marketplace, the schema, and the legacy
101
109
  // orgs/ container itself. Everything else at the root is a flat project (the target model).
@@ -124,7 +132,8 @@ export function projectDir(name: string): string {
124
132
  export function listOrgs(): string[] {
125
133
  const isProject = (dir: string) =>
126
134
  existsSync(join(dir, "styles")) || existsSync(join(dir, "apps")) || existsSync(join(dir, "components")) ||
127
- existsSync(join(dir, "templates"));
135
+ existsSync(join(dir, "templates")) || existsSync(join(dir, "agents")) ||
136
+ existsSync(join(dir, "objects")) || existsSync(join(dir, "identity")) || existsSync(join(dir, "pipelines"));
128
137
  const flat = existsSync(DESIGN) && !databaseOnly()
129
138
  ? readdirSync(DESIGN, { withFileTypes: true })
130
139
  .filter((e) => e.isDirectory() && !RESERVED_DESIGN.has(e.name) && isProject(join(DESIGN, e.name)))
@@ -178,6 +187,30 @@ export function templateDirs(refOrg: string | null): { org: string | undefined;
178
187
  return refOrg === null ? [shared, ...orgDirs] : [...orgDirs, shared];
179
188
  }
180
189
 
190
+ /** The OBJECT homes a ref resolves against (docs/unoverse/data/objects/README.md): the same two
191
+ * tiers as components, the marketplace (`design/marketplace/objects`, the platform's kinds,
192
+ * and `need`, the atom every object extends) and the org packs (`design/<org>/objects`, an
193
+ * org's own kinds). An org-qualified ref searches its org first, then the marketplace, so
194
+ * `extends: need` resolves from an org's object; a bare ref searches the marketplace first. */
195
+ export function objectDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
196
+ const shared = { org: undefined as string | undefined, dir: SHARED_DIR.object };
197
+ const orgDirs = (refOrg === null ? listOrgs() : [refOrg])
198
+ .filter((o) => existsSync(join(projectDir(o), "objects")))
199
+ .map((o) => ({ org: o as string | undefined, dir: join(projectDir(o), "objects") }));
200
+ return refOrg === null ? [shared, ...orgDirs] : [...orgDirs, shared];
201
+ }
202
+
203
+ /** The PIPELINE homes a ref resolves against (docs/unoverse/data/pipelines/README.md layer 1): the marketplace
204
+ * (`design/marketplace/pipelines`, the sources everyone gets: website, spreadsheet, images) and
205
+ * the org packs (`design/<org>/pipelines`, a source nobody else has). Same order as objects. */
206
+ export function pipelineDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
207
+ const shared = { org: undefined as string | undefined, dir: SHARED_DIR.pipeline };
208
+ const orgDirs = (refOrg === null ? listOrgs() : [refOrg])
209
+ .filter((o) => existsSync(join(projectDir(o), "pipelines")))
210
+ .map((o) => ({ org: o as string | undefined, dir: join(projectDir(o), "pipelines") }));
211
+ return refOrg === null ? [shared, ...orgDirs] : [...orgDirs, shared];
212
+ }
213
+
181
214
  /** The component homes a ref resolves against. Components live in TWO tiers: the
182
215
  * marketplace (`rx/components`, generic, every org may use) and the org packs
183
216
  * (`rx/orgs/<org>/components`, the client's own microapps — org-private). Names are
@@ -210,6 +243,37 @@ export function componentHit(dir: string, lower: string): boolean {
210
243
  return !!defPath(dir, lower) || !!defPath(join(dir, lower), lower);
211
244
  }
212
245
 
246
+ /**
247
+ * WHOSE item this is, from the homes ALONE — nothing is parsed, expanded or compiled.
248
+ *
249
+ * A catalogue that is scoped to one org must never READ another org's items and drop
250
+ * them at the end: loading a definition expands its includes and inlines every Ref, and
251
+ * an app manifest opens its binding, so a universe carrying five clients paid five
252
+ * clients' worth of work to answer one client's question (the Content Library's Studio
253
+ * list, seen doing exactly that). This answers the only question the scope needs —
254
+ * whose is it — with a directory probe, so an out-of-scope item is skipped BEFORE it
255
+ * is read.
256
+ *
257
+ * Returns the owning org, `undefined` for the marketplace tier (everyone's), and `null`
258
+ * when no home carries the name: the caller must load it to find out, which is the same
259
+ * thing it did before.
260
+ */
261
+ export function refOwnerOrg(kind: "component" | "template" | "app", ref: string): string | undefined | null {
262
+ const { org: refOrg, name } = parseRef(ref);
263
+ const lower = name.toLowerCase();
264
+ // An org-qualified ref already says whose it is, as long as the org is a real pack —
265
+ // a qualifier naming no org is a template-local part, which componentDirs resolves.
266
+ if (refOrg !== null && listOrgs().includes(refOrg)) return refOrg;
267
+ const dirs = kind === "app" ? appDirs(refOrg) : kind === "template" ? templateDirs(refOrg) : componentDirs(refOrg);
268
+ // Either authored form, plus the manifest-only folder an app may be.
269
+ const hits = dirs.filter(({ dir }) => componentHit(dir, lower) || !!defPath(join(dir, lower), "manifest"));
270
+ // The marketplace wins a bare name (an org may never shadow it), which is the
271
+ // precedence resolveComponentDirs applies; two orgs and no marketplace copy is
272
+ // ambiguous, and guessing here would hide an item from its owner.
273
+ if (hits.some((h) => h.org === undefined)) return undefined;
274
+ return hits.length === 1 ? (hits[0].org as string | undefined) : null;
275
+ }
276
+
213
277
  /** The dirs a component ref may actually load from. Qualified refs pass through
214
278
  * untouched. A bare ref narrows to the ONE home that carries the name: the CONTEXT
215
279
  * org first when given (the org of the definition/app doing the asking — a cloned