@unoverse-platform/studio 0.1.97 → 0.1.99

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 (120) hide show
  1. package/bin/studio.mjs +2 -2
  2. package/local/catalog.ts +5 -1
  3. package/local/nodes.ts +28 -41
  4. package/local/plugin.ts +71 -13
  5. package/local/sample-identity/brand.yaml +24 -0
  6. package/local/sample-identity/organisation.yaml +14 -0
  7. package/local/sample-identity/purpose.yaml +14 -0
  8. package/local/sample-identity/story.yaml +18 -0
  9. package/local/scaffold.mjs +43 -23
  10. package/local/source.ts +3 -8
  11. package/package.json +1 -1
  12. package/screens/AgentsView.tsx +173 -0
  13. package/screens/ComponentsView.tsx +2 -2
  14. package/screens/EmptyLibrary.tsx +1 -1
  15. package/screens/IdentityView.tsx +3 -3
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/screens/TemplatesLibraryView.tsx +91 -6
  18. package/screens/TemplatesView.tsx +24 -9
  19. package/screens/index.ts +2 -0
  20. package/src/App.tsx +8 -3
  21. package/src/host.ts +1 -14
  22. package/vendor/base/src/agent-mcp/discovery.ts +76 -18
  23. package/vendor/base/src/agent-mcp/handoff.ts +11 -31
  24. package/vendor/base/src/agent-mcp/index.ts +5 -5
  25. package/vendor/base/src/agent-mcp/invoke.ts +37 -19
  26. package/vendor/base/src/agent-mcp/lean.ts +19 -0
  27. package/vendor/base/src/agent-mcp/memoryContext.ts +15 -9
  28. package/vendor/base/src/agent-mcp/memoryIngest.ts +8 -3
  29. package/vendor/base/src/agent-mcp/rowComponents.ts +184 -72
  30. package/vendor/base/src/agent-mcp/tools.ts +136 -7
  31. package/vendor/base/src/agent-mcp/types.ts +47 -10
  32. package/vendor/base/src/auth/auth.ts +35 -5
  33. package/vendor/base/src/definitions/isolated.ts +37 -0
  34. package/vendor/base/src/definitions/load/compile/component.ts +59 -4
  35. package/vendor/base/src/definitions/load/compile/sections.ts +7 -1
  36. package/vendor/base/src/definitions/load/compile/template.ts +19 -15
  37. package/vendor/base/src/definitions/load/expand/inherit.ts +5 -1
  38. package/vendor/base/src/definitions/load/expand/referenced.ts +21 -2
  39. package/vendor/base/src/definitions/load/expand/refs.ts +14 -0
  40. package/vendor/base/src/definitions/load/expand/walk.ts +88 -38
  41. package/vendor/base/src/definitions/load/load.ts +37 -28
  42. package/vendor/base/src/definitions/model.ts +8 -12
  43. package/vendor/base/src/definitions/registry/agents.ts +117 -0
  44. package/vendor/base/src/definitions/registry/catalog.ts +40 -24
  45. package/vendor/base/src/definitions/registry/datasetSql.ts +149 -0
  46. package/vendor/base/src/definitions/registry/homes.ts +69 -5
  47. package/vendor/base/src/definitions/registry/objects.ts +413 -0
  48. package/vendor/base/src/definitions/registry/orgDocuments.ts +69 -29
  49. package/vendor/base/src/definitions/registry/orgPack.ts +11 -11
  50. package/vendor/base/src/definitions/registry/pipelines.ts +197 -0
  51. package/vendor/base/src/definitions/theme.ts +1 -2
  52. package/vendor/base/src/items/collect.ts +29 -25
  53. package/vendor/base/src/items/publish.ts +30 -6
  54. package/vendor/base/src/lint/context.mjs +17 -0
  55. package/vendor/base/src/lint/design/file.mjs +199 -2
  56. package/vendor/base/src/lint/design/index.mjs +66 -73
  57. package/vendor/base/src/lint/design/vocabulary.mjs +5 -2
  58. package/vendor/base/src/lint/design/walk.mjs +14 -4
  59. package/vendor/base/src/lint/index.mjs +15 -9
  60. package/vendor/base/src/lint/nodes/blocks.mjs +6 -2
  61. package/vendor/base/src/lint/nodes/index.mjs +5 -0
  62. package/vendor/base/src/lint/nodes/node.mjs +157 -7
  63. package/vendor/base/src/lint/objects/index.d.mts +32 -0
  64. package/vendor/base/src/lint/objects/index.mjs +324 -0
  65. package/vendor/base/src/lint/objects/rules.d.mts +44 -0
  66. package/vendor/base/src/lint/objects/rules.mjs +446 -0
  67. package/vendor/base/src/lint/prompts/index.d.mts +34 -0
  68. package/vendor/base/src/lint/prompts/index.mjs +198 -0
  69. package/vendor/base/src/manifests/compose.ts +80 -9
  70. package/vendor/base/src/manifests/executor/callback.ts +15 -4
  71. package/vendor/base/src/manifests/executor/context.ts +9 -1
  72. package/vendor/base/src/manifests/executor/memory.ts +83 -3
  73. package/vendor/base/src/manifests/executor/session.ts +50 -0
  74. package/vendor/base/src/manifests/executor/toolBridge.ts +114 -49
  75. package/vendor/base/src/manifests/runtime/auth/aws.ts +14 -1
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +46 -34
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +30 -32
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +6 -11
  79. package/vendor/base/src/manifests/runtime/context.ts +18 -6
  80. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +159 -3
  81. package/vendor/base/src/manifests/runtime/duplex/session.ts +304 -9
  82. package/vendor/base/src/manifests/runtime/events.ts +39 -3
  83. package/vendor/base/src/manifests/runtime/http/concurrency.ts +44 -0
  84. package/vendor/base/src/manifests/runtime/http/request.ts +36 -7
  85. package/vendor/base/src/manifests/runtime/index.ts +3 -2
  86. package/vendor/base/src/manifests/runtime/loops/repeat.ts +86 -0
  87. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +30 -3
  88. package/vendor/base/src/manifests/runtime/usage.ts +10 -0
  89. package/vendor/base/src/paths.ts +16 -8
  90. package/vendor/base/src/platform/audioManager.ts +32 -0
  91. package/vendor/base/src/platform/executionContext.ts +14 -0
  92. package/vendor/base/src/platform/liveInput.ts +52 -0
  93. package/vendor/base/src/plugins/discovery.ts +1 -1
  94. package/vendor/base/src/template/SafeExpression.ts +6 -0
  95. package/vendor/base/src/workflow/GetMemoryService.ts +12 -11
  96. package/vendor/base/src/workflow/PlatformMemoryCore.ts +23 -10
  97. package/vendor/sdk/appEngine.tsx +30 -11
  98. package/vendor/sdk/lib/UnoverseComponent.tsx +8 -4
  99. package/vendor/sdk/lib/app.tsx +70 -95
  100. package/vendor/sdk/lib/connection.tsx +47 -57
  101. package/vendor/sdk/lib/core/actions.ts +6 -0
  102. package/vendor/sdk/lib/core/apps.ts +66 -81
  103. package/vendor/sdk/lib/core/client.ts +5 -14
  104. package/vendor/sdk/lib/core/connection.ts +30 -0
  105. package/vendor/sdk/lib/core/index.ts +2 -2
  106. package/vendor/sdk/lib/core/store.ts +90 -15
  107. package/vendor/sdk/lib/core/types.ts +9 -8
  108. package/vendor/sdk/lib/primitives.tsx +113 -4
  109. package/vendor/sdk/lib/realtime/useAudioPlayback.ts +8 -1
  110. package/vendor/sdk/lib/render.tsx +3 -3
  111. package/vendor/sdk/lib/voice.tsx +27 -13
  112. package/local/sample-identity/brand/brand.yaml +0 -88
  113. package/local/sample-identity/brand/manifest.yaml +0 -13
  114. package/local/sample-identity/organisation/manifest.yaml +0 -15
  115. package/local/sample-identity/organisation/organisation.yaml +0 -53
  116. package/local/sample-identity/purpose/manifest.yaml +0 -12
  117. package/local/sample-identity/purpose/purpose.yaml +0 -46
  118. package/local/sample-identity/story/manifest.yaml +0 -12
  119. package/local/sample-identity/story/story.yaml +0 -63
  120. package/vendor/base/src/definitions/registry/identitySeed.ts +0 -17
@@ -0,0 +1,413 @@
1
+ /**
2
+ * OBJECTS, READ (docs/unoverse/data/objects/README.md, ruled 2026-09-08).
3
+ *
4
+ * An object is a kind of thing that lives in the world: a need, a product, a purpose. Its
5
+ * definition is a folder in the one grammar,
6
+ *
7
+ * objects/<name>/<name>.yaml the envelope, as a node's node.yaml: type: object, name, extends,
8
+ * the row keys, and the face (title, description, whenToUse,
9
+ * category, version) plus the credentials and hosts its calls use
10
+ * objects/<name>/props.yaml this object's OWN fields, one prop each, every prop a brief;
11
+ * a prop written from made detail says `from: <function>.<return>`
12
+ * objects/<name>/api/make.yaml the private make function, when it has one (README 4b)
13
+ * objects/<name>/api/service.yaml the object's public functions by name (getDetail, searchDetail), as a
14
+ * service node's tools; found by name, declared nowhere else
15
+ *
16
+ * in two tiers, the marketplace's (`design/marketplace/objects/`, shared by every org) and an
17
+ * org's own (`design/<org>/objects/`). `extends` names another object and is resolved here,
18
+ * across the tiers, never as a path: the base's props compose under the object's own, later
19
+ * keys over earlier. `need` is the atom every other positioned object extends (owner ruling
20
+ * 2026-09-08): a need's fields are what every row carries. An object with no embed never
21
+ * sits on the map.
22
+ *
23
+ * This module only reads. Studio authors, the platform reads.
24
+ */
25
+ import { existsSync, readdirSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { readDefCached, defPath } from "../fsCache.js";
28
+ import { composeIncludes } from "../load/expand/includes.js";
29
+ import { objectDirs } from "./homes.js";
30
+ import { envelopeFindings, propsFindings, fromFindings, functionFindings, makeFindings, firstRefusal } from "../../lint/objects/rules.mjs";
31
+
32
+ /** Refuse a folder with the linter's own sentence: one spelling of every rule (README 9a). */
33
+ const refuse = (findings: ReturnType<typeof envelopeFindings>) => {
34
+ const msg = firstRefusal(findings);
35
+ if (msg) throw new Error(msg);
36
+ };
37
+
38
+ export interface ObjectProp {
39
+ type: string;
40
+ input: boolean;
41
+ description: string;
42
+ /** The made detail this prop is written from, `<function>.<return>`: `make` for api/make.yaml, or a function of api/service.yaml (README 4b). Absent: written from the thin item. */
43
+ from?: string;
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ /**
48
+ * ONE CALL OF A FUNCTION, in the node's own run.yaml grammar (README 4b; owner 2026-09-09
49
+ * "keep in line with the core nodes API"): `name`, the `node` it runs with that node's
50
+ * `config`, `when` to skip it, `for` to run it once per item of a list, `inputs` for what the
51
+ * node takes wired on a canvas (a model's content and schema), `optional` for a best-effort
52
+ * call whose failure the function survives.
53
+ */
54
+ export interface ObjectCall {
55
+ name: string;
56
+ node?: string;
57
+ config: Record<string, unknown>;
58
+ when?: string;
59
+ for?: string;
60
+ inputs?: Record<string, unknown>;
61
+ optional?: boolean;
62
+ /**
63
+ * REMEMBERED, WHERE THE CALL KNOWS SOMETHING THE NODE CANNOT. A node declares whether its kind
64
+ * of answer may be reused; a general model call declares `false`, and rightly, because a
65
+ * prompt and a model are the caller's and may be anything.
66
+ *
67
+ * A CALL is narrower than its node. This one fixes the prompt, fixes the model, and hands it a
68
+ * PICTURE cut from a page that cannot change. Asking the same question of the same picture
69
+ * twice costs the same money and the same seconds for the same answer, and only the call's
70
+ * author is in a position to say so.
71
+ *
72
+ * So a call may declare its own memory, and it must name what makes its input the same input
73
+ * (`key`) - never simply `true`, because "this call is repeatable" is a claim about the
74
+ * INPUT, and a claim with nothing to point at is a hope.
75
+ */
76
+ cacheable?: { ignore?: string[]; key: string[] };
77
+ }
78
+
79
+ /**
80
+ * A LINE AN OBJECT DRAWS BETWEEN ITS OWN ROWS (docs/unoverse/data/objects/relationships-and-labels.md):
81
+ * rows of one folder whose `between` fields are equal are ordered by `order`, and each draws the
82
+ * line to the one just before it, when `when` holds for it. The engine knows no name.
83
+ */
84
+ export interface ObjectLink {
85
+ type: "link" | "flow" | "tree";
86
+ /** Among rows of this object: the fields whose equal values make one line of rows. Absent on a line to another object. */
87
+ between?: string[];
88
+ /** The field the rows are put in order by, each drawing its line to the one before it. Absent on a line to another object. */
89
+ order?: string;
90
+ when?: string;
91
+ /** A LINE TO ANOTHER OBJECT (relationships-and-labels.md, 2026-09-15): the object it points at... */
92
+ to?: string;
93
+ /** ...and which of this row's fields names which of that row's: `{ manager: legalName }`. One pair. */
94
+ match?: Record<string, string>;
95
+ /** Fields (of `between`, or the row's side of `match`) whose values may name one thing several ways: one question per set of names says which are the same. */
96
+ alike?: string[];
97
+ }
98
+
99
+ /**
100
+ * A COLUMN OF A DATASET (dataset.md, "the declaration is the schema"). The same words a prop
101
+ * uses, so an author learns one grammar: a name, a type, whether it must be there, and whether
102
+ * it is searched by. A number is a real numeric column and not a field of a JSON document, so a
103
+ * sum is a sum and a value that is not a number is refused at the door.
104
+ */
105
+ export interface DatasetColumn {
106
+ name: string;
107
+ /** THE ONLY VALUES IT MAY HOLD. A column that IS the language (a measure, a unit, a side) says
108
+ * its words here and the database refuses the rest: a misspelling cannot become a second
109
+ * thing that never sums with the first. Absent on a column carrying a document's own words. */
110
+ enum?: string[];
111
+ /** string | number | integer | date | datetime | boolean. Nothing else: a type this does not
112
+ * name is one the generated table cannot promise anything about. */
113
+ type: string;
114
+ required?: boolean;
115
+ /** Searched or grouped by, so the generated table indexes it. */
116
+ indexed?: boolean;
117
+ description?: string;
118
+ }
119
+
120
+ export interface DatasetDefinition {
121
+ columns: DatasetColumn[];
122
+ /** Columns that together identify one row, so a read again replaces rather than duplicates. */
123
+ uniqueBy?: string[];
124
+ /**
125
+ * WHICH COLUMN SAYS WHOSE THESE ROWS ARE, and which says when they speak as of. The one row a
126
+ * dataset leaves on the map is titled from them, and until 2026-09-20 the engine simply read
127
+ * the columns named "fund" and "as_of": one organisation's words, inside the platform. A
128
+ * dataset that names neither gets a handle titled by its document, which is always true.
129
+ */
130
+ subject?: string;
131
+ asOf?: string;
132
+ description?: string;
133
+ }
134
+
135
+ export interface ObjectFunction {
136
+ description?: string;
137
+ /** What it takes when called from outside, as a JSON schema (README 4b): the Agent's tool arguments beside the row's universal_id. */
138
+ input?: Record<string, unknown>;
139
+ calls: ObjectCall[];
140
+ /** `{{ }}` references or `return` expressions over `row`, `calls` and `item`; a string or a record of them. */
141
+ returns?: unknown;
142
+ /** True when every call names a node: the engine runs this from the file. */
143
+ runnable: boolean;
144
+ /**
145
+ * Callable from outside, by an Agent in Spatial or a component (docs/unoverse/data/objects/README.md
146
+ * 4b): true for a function of api/service.yaml, false for `make`, the private api/make.yaml. The
147
+ * file is the switch; nothing inside a function says who may call it.
148
+ */
149
+ offered: boolean;
150
+ }
151
+
152
+ /**
153
+ * THE KEYS AN OBJECT IS KNOWN BY, in order of trust (README 4a, 2026-09-15): a flat list is
154
+ * one key of one or more fields; a list of lists is several keys, tried first to last.
155
+ */
156
+ export function identityKeysOf(uniqueBy: string[] | string[][] | null | undefined): string[][] {
157
+ if (!Array.isArray(uniqueBy) || !uniqueBy.length) return [];
158
+ return Array.isArray(uniqueBy[0]) ? (uniqueBy as string[][]).filter((k) => Array.isArray(k) && k.length > 0) : [uniqueBy as string[]];
159
+ }
160
+
161
+ export interface ObjectDefinition {
162
+ /** The object type, as a row's `object_type` names it. */
163
+ name: string;
164
+ /** The org that owns it; undefined for a marketplace object. */
165
+ org?: string;
166
+ /** The base it extends, by name. */
167
+ extends?: string;
168
+ /** Which fields make two rows the same one: one key (a list of fields), or several keys tried in order (a list of lists; README 4a). Absent on an object that is never a row. */
169
+ uniqueBy?: string[] | string[][];
170
+ /** The fields whose text makes the vector. Absent on an object that is never a row. */
171
+ embed?: string[];
172
+ /** What a search hit carries; the rest comes with getDetail. `all` = the row whole. */
173
+ search?: string[] | "all";
174
+ /** The stored `object_type` when it differs from the name (interface rows are stored as mcp). */
175
+ storedAs?: string;
176
+ /** The interface every row of this object wears, as unoverse://components/<org>/<name>; stamped on the row at save when the row has none (README 4a). */
177
+ app?: string;
178
+ /** The glyph every row of this object is drawn with, a lucide icon name in kebab-case (`file-text`); the base's unless the object says its own. */
179
+ icon?: string;
180
+ /** Which of the twelve elements this object IS (README 4a): product, content, task, ... A classification, never a shape; not inherited. */
181
+ element?: string;
182
+ meta?: "open";
183
+ /** The lines it draws between its own rows, by name (README 7; relationships-and-labels.md). */
184
+ links?: Record<string, ObjectLink>;
185
+ /** THE TABLE THIS OBJECT OWNS (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18),
186
+ * from its folder's table.yaml: an object that holds many small facts read together rather
187
+ * than one row read alone. Absent on every ordinary object, which stays a row. */
188
+ dataset?: DatasetDefinition;
189
+ /** Every prop, the base's composed under this object's own. */
190
+ props: Record<string, ObjectProp>;
191
+ /** The props this object declares itself, without its base's. */
192
+ ownProps: Record<string, ObjectProp>;
193
+ /** The face, read from the envelope: what this is and when an Agent reaches for it. */
194
+ manifest: {
195
+ title?: string; description?: string; whenToUse?: string; category?: string; version?: string;
196
+ allowedHosts?: string[];
197
+ credentials?: string[];
198
+ };
199
+ /** The functions by name (docs/unoverse/data/objects/README.md 4b): `make` from api/make.yaml, and api/service.yaml's own (getDetail, searchDetail). */
200
+ api: string[];
201
+ functions: Record<string, ObjectFunction>;
202
+ /** The folder on disk. */
203
+ dir: string;
204
+ }
205
+
206
+ // NO RESULT MAP (2026-09-09). The files are the one shared place, and `readDefCached`
207
+ // already knows when one changed (its mtime). A map of composed results above it remembered
208
+ // a definition until the process restarted: a publish or an edit was not seen. Composing an
209
+ // object again is a few stat calls; the freshness check IS the cache.
210
+
211
+ function folderOf(name: string, org?: string | null): { dir: string; org?: string } | null {
212
+ for (const { org: o, dir } of objectDirs(org ?? null)) {
213
+ const folder = join(dir, name);
214
+ if (existsSync(folder) && statSync(folder).isDirectory() && defPath(folder, name)) return { dir: folder, org: o };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ // THE TYPES A DATASET COLUMN MAY BE, and nothing else: a type this does not name is one the
220
+ // generated table can promise nothing about, and a promise is the whole point of having columns
221
+ // rather than a JSON field. `number` is DECIMAL and never floating point (dataset.md): a total
222
+ // that must reconcile against a stated figure drifts under binary floating point, and the drift
223
+ // grows with the number of rows.
224
+ const DATASET_TYPES = new Set(["string", "number", "integer", "date", "datetime", "boolean"]);
225
+
226
+ // A COLUMN NAME IS A PLAIN WORD. Lower case, letters, digits and underscores, starting with a
227
+ // letter. Nothing that has to be quoted, nothing that could be read as anything but a name, and
228
+ // nothing that collides with what the platform puts on every dataset itself.
229
+ const COLUMN_NAME = /^[a-z][a-z0-9_]*$/;
230
+ const RESERVED = new Set(["id", "org", "of", "document", "created_at", "updated_at"]);
231
+
232
+ /** The table an object owns, from its folder's table.yaml, refused when it says anything the
233
+ * generated table could not keep (README 4f; dataset.md). */
234
+ export function readDataset(name: string, file: Record<string, any>, dir: string): DatasetDefinition {
235
+ const findings: string[] = [];
236
+ const raw = Array.isArray(file.columns) ? file.columns : [];
237
+ if (!raw.length) findings.push("table.yaml names no columns; a dataset is its columns");
238
+ const columns: DatasetColumn[] = [];
239
+ const seen = new Set<string>();
240
+ for (const c of raw) {
241
+ const col = String(c?.name ?? "").trim();
242
+ const type = String(c?.type ?? "").trim();
243
+ if (!COLUMN_NAME.test(col)) findings.push(`column "${col}": a name is lower case letters, digits and underscores, starting with a letter`);
244
+ else if (RESERVED.has(col)) findings.push(`column "${col}" is one the platform puts on every dataset itself (${[...RESERVED].join(", ")})`);
245
+ else if (seen.has(col)) findings.push(`column "${col}" is declared twice`);
246
+ else seen.add(col);
247
+ if (!DATASET_TYPES.has(type)) findings.push(`column "${col}": type "${type}" is not one of ${[...DATASET_TYPES].join(", ")}`);
248
+ const allowed = Array.isArray(c?.enum) ? c.enum.map((v: unknown) => String(v)) : undefined;
249
+ // A LIST OF ALLOWED VALUES ONLY MEANS ANYTHING FOR A WORD. A number or a date is already
250
+ // constrained by its type, and a list of allowed numbers is a rule about the world, not
251
+ // about the column.
252
+ if (allowed && type !== "string") findings.push(`column "${col}": only a string column may say which values it allows`);
253
+ if (allowed && new Set(allowed).size !== allowed.length) findings.push(`column "${col}": its allowed values repeat`);
254
+ columns.push({ name: col, type, ...(allowed ? { enum: allowed } : {}), ...(c?.required === true ? { required: true } : {}), ...(c?.indexed === true ? { indexed: true } : {}), ...(c?.description ? { description: String(c.description) } : {}) });
255
+ }
256
+ const uniqueBy = Array.isArray(file.uniqueBy) ? file.uniqueBy.map((u: unknown) => String(u)) : undefined;
257
+ for (const u of uniqueBy ?? []) if (!seen.has(u) && !RESERVED.has(u)) findings.push(`uniqueBy names "${u}", which is not a column`);
258
+ if (findings.length) throw new Error(`object ${name}: ${findings.join("; ")} (${dir})`);
259
+ return { columns, ...(uniqueBy ? { uniqueBy } : {}), ...(file.description ? { description: String(file.description) } : {}) };
260
+ }
261
+
262
+ function readFolder(dir: string, name: string, org: string | undefined, seen: string[]): ObjectDefinition {
263
+ const envelope = composeIncludes(readDefCached<Record<string, any>>(defPath(dir, name)!), dir) as Record<string, any>;
264
+ refuse(envelopeFindings(name, envelope).map((f) => ({ ...f, msg: `${f.msg} (${dir})` })));
265
+ const manifest = envelope as Record<string, any>;
266
+ // THE TABLE IT OWNS, when it owns one (dataset.md). Absent on every ordinary object.
267
+ // The file is `dataset.yaml` and never `table.yaml`: an object may be NAMED table, and its own
268
+ // envelope is objects/table/table.yaml, which a declaration file of that name would shadow.
269
+ const datasetPath = defPath(dir, "dataset");
270
+ const dataset = datasetPath && datasetPath !== defPath(dir, name)
271
+ ? readDataset(name, readDefCached<Record<string, any>>(datasetPath) ?? {}, dir)
272
+ : undefined;
273
+ const servicePath = join(dir, "api", "service.yaml");
274
+ const service = existsSync(servicePath) ? (readDefCached<Record<string, any>>(servicePath) ?? {}) : {};
275
+ // THE PRIVATE MAKE FUNCTION (README 4b, 2026-09-14): api/make.yaml is one function, `make`, run
276
+ // only by the pipeline; api/service.yaml holds what may be called from outside.
277
+ const makePath = join(dir, "api", "make.yaml");
278
+ const make = existsSync(makePath) ? readDefCached<Record<string, any>>(makePath) : undefined;
279
+ refuse(makeFindings(name, make, service).map((f) => ({ ...f, msg: `${f.msg} (${dir})` })));
280
+ // THE BASE, FIRST (objects README 5, "Inheritance is whole", 2026-09-13): an object that
281
+ // extends another inherits its fields, its functions, its lines and its row keys, and a prop
282
+ // may be written from an inherited function, so the base is known before the props are.
283
+ let parent: ObjectDefinition | null = null;
284
+ if (envelope.extends) {
285
+ const base = String(envelope.extends);
286
+ const chain = [...seen, name];
287
+ if (chain.includes(base)) throw new Error(`object ${name}: extends cycle ${[...chain, base].join(" -> ")}`);
288
+ parent = readObject(base, org, chain);
289
+ if (!parent) throw new Error(`object ${name}: extends "${base}", which no tier holds`);
290
+ }
291
+ const functions: Record<string, ObjectFunction> = { ...(parent?.functions ?? {}) };
292
+ for (const fn of Object.keys(service)) functions[fn] = readFunction(name, fn, service[fn], true);
293
+ if (make != null) functions.make = readFunction(name, "make", make, false);
294
+ const api = Object.keys(functions);
295
+ // What `from` is checked against: the object's own functions over the ones it inherits.
296
+ const callable: Record<string, unknown> = { ...Object.fromEntries(Object.entries(parent?.functions ?? {}).map(([k, f]) => [k, { returns: (f as { returns?: unknown }).returns ?? {} }])), ...service, ...(make != null ? { make } : {}) };
297
+ const ownProps: Record<string, ObjectProp> = {};
298
+ for (const [k, p] of Object.entries((envelope.props ?? {}) as Record<string, any>)) {
299
+ ownProps[k] = { ...(p ?? {}), type: String(p?.type ?? "string"), input: p?.input === true, description: String(p?.description ?? "") };
300
+ // THE PROP PULLS THE FUNCTION: `from` names a function of this object (`make`, or api/service.yaml's)
301
+ // and a key of its returns. A prop that asks for detail no function makes is a lie the
302
+ // folder tells, and the object does not load.
303
+ if (p?.from != null) refuse(fromFindings(name, k, p.from, callable));
304
+ }
305
+ let props: Record<string, ObjectProp> = {};
306
+ let inheritedIcon: string | undefined;
307
+ if (parent) {
308
+ const base = String(envelope.extends);
309
+ inheritedIcon = parent.icon;
310
+ // RE-BRIEF (README 5): an object may say its own brief for a field it inherits, and
311
+ // nothing else about it. A product's title is the offering's name, not the need's outcome.
312
+ refuse(propsFindings(name, envelope.props ?? {}, parent.props, base));
313
+ for (const [k, p] of Object.entries(ownProps)) {
314
+ if (!(k in parent.props)) continue;
315
+ ownProps[k] = { ...parent.props[k], description: p.description };
316
+ }
317
+ props = { ...parent.props };
318
+ }
319
+ props = { ...props, ...ownProps };
320
+ // ITS LINES name only its own fields: a line grouped or ordered by a field the object does
321
+ // not have could never be drawn, and the object does not load.
322
+ const links = { ...(parent?.links ?? {}), ...((envelope.links ?? {}) as Record<string, ObjectLink>) } as Record<string, ObjectLink>;
323
+ for (const [ln, l] of Object.entries(links ?? {})) {
324
+ const mine = l?.to ? Object.keys(l.match ?? {}) : [...(l?.between ?? []), l?.order];
325
+ for (const f of mine) if (typeof f === "string" && !(f in props)) throw new Error(`object ${name}: link ${ln} names the field "${f}", which the object does not have (${dir})`);
326
+ for (const f of l?.alike ?? []) if (!mine.includes(f)) throw new Error(`object ${name}: link ${ln}: alike names "${f}", which is not one of its ${l?.to ? "match" : "between"} fields (${dir})`);
327
+ }
328
+ return {
329
+ name,
330
+ ...(org ? { org } : {}),
331
+ ...(envelope.extends ? { extends: String(envelope.extends) } : {}),
332
+ ...(envelope.uniqueBy ? { uniqueBy: envelope.uniqueBy as string[] | string[][] } : parent?.uniqueBy ? { uniqueBy: parent.uniqueBy } : {}),
333
+ ...(envelope.embed ? { embed: envelope.embed as string[] } : parent?.embed ? { embed: parent.embed } : {}),
334
+ ...(envelope.search ? { search: envelope.search } : parent?.search ? { search: parent.search } : {}),
335
+ ...(envelope.storedAs ? { storedAs: String(envelope.storedAs) } : {}),
336
+ ...(typeof envelope.app === "string" && envelope.app ? { app: envelope.app } : {}),
337
+ ...(typeof envelope.icon === "string" && envelope.icon ? { icon: envelope.icon } : inheritedIcon ? { icon: inheritedIcon } : {}),
338
+ ...(typeof envelope.element === "string" && envelope.element ? { element: envelope.element } : parent?.element ? { element: parent.element } : {}),
339
+ ...(envelope.meta ? { meta: envelope.meta } : parent?.meta ? { meta: parent.meta } : {}),
340
+ ...(links && Object.keys(links).length ? { links } : {}),
341
+ ...(dataset ? { dataset } : {}),
342
+ props,
343
+ ownProps,
344
+ manifest: {
345
+ title: manifest.title, description: manifest.description, whenToUse: manifest.whenToUse, category: manifest.category, version: manifest.version,
346
+ ...(Array.isArray(manifest.allowedHosts) ? { allowedHosts: manifest.allowedHosts } : {}),
347
+ ...(Array.isArray(manifest.credentials) ? { credentials: manifest.credentials } : {}),
348
+ },
349
+ api,
350
+ functions,
351
+ dir,
352
+ };
353
+ }
354
+
355
+ /**
356
+ * A function as the file declares it (README 4b): `calls` in the node's grammar, `returns`.
357
+ * The rules are the linter's (lint/objects/rules.mjs functionFindings): never `steps`, a
358
+ * name and a node per call, unique names, `for` and `when` strings. Refused here with the
359
+ * same sentence the linter prints.
360
+ */
361
+ function readFunction(object: string, fn: string, raw: unknown, offered: boolean): ObjectFunction {
362
+ refuse(functionFindings(object, fn, raw));
363
+ const f = raw as Record<string, any>;
364
+ const list: any[] = Array.isArray(f.calls) ? f.calls : [];
365
+ const calls: ObjectCall[] = list.map((c: any) => {
366
+ const config = c.config != null && typeof c.config === "object" && !Array.isArray(c.config) ? c.config : {};
367
+ const inputs = c.inputs != null && typeof c.inputs === "object" && !Array.isArray(c.inputs) ? { inputs: c.inputs } : {};
368
+ // WHAT THE CALL SAYS ABOUT BEING REMEMBERED (ObjectCall.cacheable). Kept only when it names
369
+ // what makes its input the same input: a declaration with no `key` is not a declaration, and
370
+ // a call that cannot say why it is repeatable must not be treated as though it had.
371
+ const cacheable = c.cacheable && typeof c.cacheable === "object" && Array.isArray((c.cacheable as any).key) && (c.cacheable as any).key.length
372
+ ? { ...((c.cacheable as any).ignore ? { ignore: ((c.cacheable as any).ignore as unknown[]).map(String) } : {}), key: ((c.cacheable as any).key as unknown[]).map(String) }
373
+ : null;
374
+ return { name: c.name, ...(typeof c.node === "string" ? { node: c.node } : {}), config, ...(typeof c.when === "string" ? { when: c.when } : {}), ...(typeof c.for === "string" ? { for: c.for } : {}), ...inputs, ...(c.optional === true ? { optional: true } : {}), ...(cacheable ? { cacheable } : {}) };
375
+ });
376
+ return {
377
+ ...(typeof f.description === "string" ? { description: f.description } : {}),
378
+ ...(f.input && typeof f.input === "object" && !Array.isArray(f.input) ? { input: f.input as Record<string, unknown> } : {}),
379
+ calls,
380
+ ...(f.returns != null ? { returns: f.returns } : {}),
381
+ runnable: calls.every((c) => typeof c.node === "string"),
382
+ offered,
383
+ };
384
+ }
385
+
386
+ /** One object by name, the org's tier first then the marketplace. Null when no tier holds it. */
387
+ export function readObject(name: string, org?: string | null, seen: string[] = []): ObjectDefinition | null {
388
+ const hit = folderOf(name, org);
389
+ return hit ? readFolder(hit.dir, name, hit.org, seen) : null;
390
+ }
391
+
392
+ /** Every object an org can see: the marketplace's and its own. Bare (no org) lists the marketplace's plus every org's. */
393
+ export function listObjects(org?: string | null): ObjectDefinition[] {
394
+ const out: ObjectDefinition[] = [];
395
+ const seen = new Set<string>();
396
+ for (const { org: o, dir } of objectDirs(org ?? null)) {
397
+ if (!existsSync(dir)) continue;
398
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
399
+ if (!e.isDirectory() || e.name.startsWith(".")) continue;
400
+ const key = `${o ?? ""}/${e.name}`;
401
+ if (seen.has(key)) continue;
402
+ seen.add(key);
403
+ const def = readObject(e.name, o ?? null);
404
+ if (def) out.push(def);
405
+ }
406
+ }
407
+ return out;
408
+ }
409
+
410
+ /** The objects that are rows: those with an `embed`. */
411
+ export function rowObjects(org?: string | null): ObjectDefinition[] {
412
+ return listObjects(org).filter((o) => Array.isArray(o.embed));
413
+ }
@@ -1,18 +1,25 @@
1
1
  /**
2
- * THE ORG'S IDENTITY DOCUMENTS, READ (docs/unoverse/UNOVERSE_ORGANISATION.md section 5).
2
+ * THE ORG'S IDENTITY DOCUMENTS, READ (docs/unoverse/data/objects/README.md section 6, ruled
3
+ * 2026-09-08; UNOVERSE_ORGANISATION.md section 5).
3
4
  *
4
- * Four definitions in design/<slug>/identity/, each a folder like a component's:
5
+ * Four objects in the marketplace pack say the SHAPE, brief by brief:
5
6
  *
6
- * identity/<doc>/manifest.yaml title, description, whenToUse, category, version
7
- * identity/<doc>/<doc>.yaml type: identity, props: each with its brief and its default
7
+ * design/marketplace/objects/<doc>/ organisation, brand, purpose, story
8
8
  *
9
- * The defaults ARE the document: this org's own words. The briefs are the instructions a
10
- * writer (an Agent with the create skill, or a person) follows to fill them. This module
11
- * only reads; Studio authors, the platform renders. Nothing here writes a byte.
9
+ * and the org's pack holds its ANSWERS, one instance a file, in the one grammar:
10
+ *
11
+ * design/<slug>/identity/<doc>.yaml type: identity, extends: <doc>, props with `default` only
12
+ *
13
+ * The instance carries nothing but defaults: the briefs, the limits, the item shapes and the
14
+ * manifest are the object's. This module composes the two into what every reader has always
15
+ * had, the props with their briefs and their values, and the manifest words. It only reads;
16
+ * Studio authors, the platform renders. `instanceText` is the one serializer, used by the
17
+ * pack writer and the Studio scaffold so a new org and a new project start identical.
12
18
  */
13
19
  import { existsSync, readFileSync } from "node:fs";
14
- import { join } from "node:path";
15
- import { parse } from "yaml";
20
+ import { basename, join } from "node:path";
21
+ import { parse, stringify } from "yaml";
22
+ import { readObject, type ObjectDefinition } from "./objects.js";
16
23
 
17
24
  export const ORG_DOCUMENTS = ["organisation", "brand", "purpose", "story"] as const;
18
25
  export type OrgDocument = (typeof ORG_DOCUMENTS)[number];
@@ -20,11 +27,11 @@ export type OrgDocument = (typeof ORG_DOCUMENTS)[number];
20
27
  export interface IdentityProp {
21
28
  name: string;
22
29
  type: string;
23
- /** The brief: what belongs in the field, as direction to a writer. */
30
+ /** The brief: what belongs in the field, as direction to a writer. From the object. */
24
31
  description: string;
25
32
  maxLength?: number;
26
33
  maxItems?: number;
27
- /** The org's own words, as they stand. */
34
+ /** The org's own words, as they stand. From the instance. */
28
35
  default: unknown;
29
36
  }
30
37
 
@@ -34,34 +41,67 @@ export interface IdentityDocument {
34
41
  props: IdentityProp[];
35
42
  /** prop name -> default, the document as a plain object. */
36
43
  values: Record<string, unknown>;
37
- /** Where it lives in the pack, relative to design/<slug>/. */
44
+ /** Where the instance lives in the pack, relative to design/<slug>/. */
38
45
  path: string;
39
46
  }
40
47
 
41
- /** design/<slug>/identity/<doc>/ */
42
- export const identityDir = (root: string, doc: OrgDocument): string => join(root, "identity", doc);
48
+ /** design/<slug>/identity/ */
49
+ export const identityDir = (root: string): string => join(root, "identity");
50
+
51
+ /** The object a document is an instance of: the marketplace's, or the org's own override of it. */
52
+ export function identityObject(doc: OrgDocument, org?: string | null): ObjectDefinition | null {
53
+ return readObject(doc, org ?? null);
54
+ }
55
+
56
+ /** The props a writer fills: the object's own, `input: true` ones. Engine-assigned fields (whenToUse) are not the document's. */
57
+ function writable(object: ObjectDefinition): Array<[string, ObjectDefinition["ownProps"][string]]> {
58
+ return Object.entries(object.ownProps).filter(([, p]) => p.input);
59
+ }
60
+
61
+ /** An empty value for a prop: an empty list for an array, an empty string otherwise. */
62
+ export const emptyValue = (type: string): unknown => (type === "array" ? [] : "");
63
+
64
+ /**
65
+ * The instance file's text for a document with these values (missing values empty). The one
66
+ * serializer: the org pack writer, the Studio scaffold and any regeneration go through it.
67
+ */
68
+ export function instanceText(doc: OrgDocument, values: Record<string, unknown> = {}, org?: string | null): string {
69
+ const object = identityObject(doc, org);
70
+ if (!object) throw new Error(`identity/${doc}: no object named ${doc} in any tier`);
71
+ const props: Record<string, { default: unknown }> = {};
72
+ for (const [name, p] of writable(object)) props[name] = { default: name in values ? values[name] : emptyValue(p.type) };
73
+ const head = `# The org's ${doc}: its own words, and only those. The briefs, the limits and the shape are the\n# ${doc} object's (design/marketplace/objects/${doc}/); this file carries the answers as defaults.\n`;
74
+ return head + stringify({ unoverse: "1.0", type: "identity", extends: doc, props }, { lineWidth: 100 });
75
+ }
43
76
 
44
- /** One identity definition as the pack holds it. Null when the folder is missing. */
77
+ /** One identity document as the pack holds it, composed with its object. Null when the instance is missing. */
45
78
  export function readOrgDocument(root: string, doc: OrgDocument): IdentityDocument | null {
46
- const dir = identityDir(root, doc);
47
- const file = join(dir, `${doc}.yaml`);
79
+ const file = join(identityDir(root), `${doc}.yaml`);
48
80
  if (!existsSync(file)) return null;
49
- const definition = (parse(readFileSync(file, "utf8")) as Record<string, any>) ?? {};
50
- const manifestFile = join(dir, "manifest.yaml");
51
- const manifest = existsSync(manifestFile) ? ((parse(readFileSync(manifestFile, "utf8")) as Record<string, any>) ?? {}) : {};
52
- const props: IdentityProp[] = Object.entries((definition.props ?? {}) as Record<string, any>).map(([name, p]) => ({
81
+ const org = basename(root);
82
+ const object = identityObject(doc, org);
83
+ if (!object) throw new Error(`identity/${doc}.yaml: no object named ${doc} in any tier`);
84
+ const instance = (parse(readFileSync(file, "utf8")) as Record<string, any>) ?? {};
85
+ if (instance.type !== "identity") throw new Error(`${file}: type must be identity`);
86
+ if (instance.extends !== doc) throw new Error(`${file}: extends must be ${doc}`);
87
+ const given = (instance.props ?? {}) as Record<string, { default?: unknown }>;
88
+ for (const k of Object.keys(given)) {
89
+ if (!(k in object.ownProps) || !object.ownProps[k].input) throw new Error(`${file}: "${k}" is not a field of ${doc} a writer fills`);
90
+ for (const key of Object.keys(given[k] ?? {})) if (key !== "default") throw new Error(`${file}: "${k}" carries "${key}"; an instance prop carries default only, the brief is the object's`);
91
+ }
92
+ const props: IdentityProp[] = writable(object).map(([name, p]) => ({
53
93
  name,
54
- type: String(p?.type ?? "string"),
55
- description: String(p?.description ?? ""),
56
- ...(p?.maxLength != null ? { maxLength: Number(p.maxLength) } : {}),
57
- ...(p?.maxItems != null ? { maxItems: Number(p.maxItems) } : {}),
58
- default: p?.default,
94
+ type: p.type,
95
+ description: p.description,
96
+ ...(p.maxLength != null ? { maxLength: Number(p.maxLength) } : {}),
97
+ ...(p.maxItems != null ? { maxItems: Number(p.maxItems) } : {}),
98
+ default: name in given ? given[name]?.default : emptyValue(p.type),
59
99
  }));
60
100
  return {
61
- manifest: { title: manifest.title, description: manifest.description, whenToUse: manifest.whenToUse, category: manifest.category, version: manifest.version },
62
- definition: { type: definition.type, name: definition.name, category: definition.category },
101
+ manifest: object.manifest,
102
+ definition: { type: "identity", name: doc, category: object.manifest.category },
63
103
  props,
64
104
  values: Object.fromEntries(props.map((p) => [p.name, p.default])),
65
- path: `identity/${doc}/${doc}.yaml`,
105
+ path: `identity/${doc}.yaml`,
66
106
  };
67
107
  }
@@ -15,12 +15,14 @@
15
15
  * styles/themes/ brand themes seeded from the marketplace foundation; base and
16
16
  * semantic tokens are INHERITED at resolve time (an org is a theme)
17
17
  *
18
- * Shapes: each definition declares its own props with their briefs (identitySeed.ts). Nothing is
19
- * invented: the defaults are empty until a writer fills them from the org's own sources.
18
+ * Shapes (ruled 2026-09-08): each identity file is an INSTANCE of its marketplace object
19
+ * (design/marketplace/objects/<doc>/), the one grammar with `extends: <doc>` and props
20
+ * carrying `default` only. The briefs are the object's. Nothing is invented: the defaults
21
+ * are empty until a writer fills them from the org's own sources.
20
22
  */
21
23
  import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
22
- import { dirname, join } from "node:path";
23
- import { IDENTITY_SEED } from "./identitySeed.js";
24
+ import { join } from "node:path";
25
+ import { ORG_DOCUMENTS, instanceText } from "./orgDocuments.js";
24
26
 
25
27
  export const ORG_SLUG = /^[a-z][a-z0-9-]{0,63}$/;
26
28
 
@@ -39,12 +41,10 @@ export function createOrgPack(designHome: string, slug: string, name: string): s
39
41
  mkdirSync(join(dest, "components"), { recursive: true });
40
42
  mkdirSync(join(dest, "identity"), { recursive: true });
41
43
  cpSync(themes, join(dest, "styles", "themes"), { recursive: true });
42
- // THE IDENTITY SEED: four definitions, briefs written, defaults empty (identitySeed.ts,
43
- // mirrored by the Studio scaffold's sample-identity). The name is the org row's, not the
44
- // document's: the writer fills `name` from the organisation's own site.
45
- for (const [rel, text] of Object.entries(IDENTITY_SEED)) {
46
- mkdirSync(dirname(join(dest, "identity", rel)), { recursive: true });
47
- writeFileSync(join(dest, "identity", rel), text);
48
- }
44
+ // THE IDENTITY: four instances, defaults empty, written by the one serializer the Studio
45
+ // scaffold's sample-identity is generated with, so a new org and a new project start
46
+ // identical. The name is the org row's, not the document's: the writer fills `name` from
47
+ // the organisation's own site.
48
+ for (const doc of ORG_DOCUMENTS) writeFileSync(join(dest, "identity", `${doc}.yaml`), instanceText(doc));
49
49
  return dest;
50
50
  }