unoverse 0.1.214 → 0.1.216

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.
package/lib/workspace.mjs CHANGED
@@ -40,8 +40,9 @@ export function designHome(root) {
40
40
  * a design home → this is an asset workspace, publish means item rows; a
41
41
  * docker-compose.yml first → this is a universe, publish stays the operator's.
42
42
  * Checked in that order at each level, so a universe folder that also holds a design
43
- * tree (the platform monorepo's apps/unoverse) is an asset workspace when you stand in
44
- * it, and a universe when you stand at its compose root.
43
+ * tree is an asset workspace when you stand in it, and a universe when you stand at its
44
+ * compose root. The platform monorepo holds no design tree (retired 2026-10-04), so
45
+ * anywhere inside it is the universe its compose root describes.
45
46
  */
46
47
  export function findWorkspace(from = process.cwd()) {
47
48
  for (let dir = resolve(from); ; ) {
@@ -6,10 +6,10 @@
6
6
  # - unoverse (the platform runtime — engine in-process + node plane + native MCP),
7
7
  # canvas, umap, memory. Code protected, pulled as images.
8
8
  #
9
- # LAYER 2: PLATFORM-DEV MOUNTS (monorepo only — sync-starter strips them)
10
- # - apps/unoverse/design — design (components, templates, styles)
11
- # - apps/unoverse/prompts — behavior (skills + blocks)
12
- # In a STARTER/customer universe these folders do not exist: authoring is
9
+ # LAYER 2: PLATFORM-DEV MOUNT (monorepo only; sync-starter strips it)
10
+ # - packages/marketplace/definitions the marketplace tier (components, templates,
11
+ # styles, skills, blocks, nodes, objects, pipelines)
12
+ # In a STARTER/customer universe this folder does not exist: authoring is
13
13
  # Studio's (publish over the API) and content is DB-driven (marketplace).
14
14
  # Everything else under apps/unoverse (engine, server, web, tools) is image-only.
15
15
  #
@@ -296,7 +296,7 @@ services:
296
296
  - PLUGINS_DIR=/app/plugins
297
297
  # Dev-editable folders are mounted over the image below; NODE_PATH resolves
298
298
  # their hoisted deps from the local workspace node_modules. The plugins dir is
299
- # also on NODE_PATH so locally-built component nodes (design/marketplace/nodes/*, which
299
+ # also on NODE_PATH so locally-built component nodes (packages/marketplace/definitions/nodes/*, which
300
300
  # live OUTSIDE /app/plugins) can resolve @unoverse-platform/plugin-base — the only
301
301
  # copy carrying a built dist. Without this the runtime hits MODULE_NOT_FOUND and
302
302
  # the nodes show "Package not installed".
@@ -307,10 +307,11 @@ services:
307
307
  # a `unoverse_base:/app/base` volume here between 2026-08-30 and 2026-09-03, holding a
308
308
  # base the entrypoint installed from npm at startup. Both are gone: base is compiled in
309
309
  # from the workspace, so the image and its runtime cannot disagree.
310
- # ── Platform-dev mounts (monorepo only; sync-starter strips these two
311
- # lines from the starter's copy — a customer universe serves what the DB
312
- # says). Local edits are picked up on restart; design/prompts are data, no build.
313
- - ./apps/unoverse/design:/app/apps/unoverse/design # every authored kind: components, templates, styles, skills, blocks, nodes
310
+ # ── Platform-dev mount (monorepo only; sync-starter strips this line from
311
+ # the starter's copy, so a customer universe serves what the DB says). Local
312
+ # edits are picked up on restart; definitions are data, no build. The design
313
+ # tree mount went with the design tree (2026-10-04).
314
+ - ./packages/marketplace/definitions:/app/packages/marketplace/definitions # the marketplace tier: every authored kind, read live, and also how base finds the content home (paths.ts findHome)
314
315
  - ./node_modules:/app/host_node_modules # Hoisted deps for locally-built nodes
315
316
  healthcheck:
316
317
  # Ready = internal runtime port up (nodes are loaded into memory BEFORE it binds).
@@ -32,11 +32,15 @@ cmd_db_verify() {
32
32
 
33
33
  // ── Source of truth: every table + required columns (from Prisma schema) ──
34
34
  const schema = {
35
+ audit_events: [
36
+ "id", "at", "actor", "actor_kind", "method", "path", "route",
37
+ "requirement", "outcome", "status", "reason", "ip", "user_agent"
38
+ ],
35
39
  workflows: [
36
40
  "id", "name", "description", "nodes", "edges", "active",
37
41
  "execution_mode", "test_inputs", "umap_settings", "viewport",
38
42
  "mcp_schema", "memory_config", "content_taxonomy", "created_at", "updated_at",
39
- "workflow_document", "layout_document", "org"
43
+ "workflow_document", "layout_document", "org", "deleted_at", "deleted_by", "created_by", "updated_by", "git_source"
40
44
  ],
41
45
  workflow_executions: [
42
46
  "execution_id", "workflow_id", "status", "start_time", "end_time",
@@ -50,7 +54,7 @@ cmd_db_verify() {
50
54
  "error", "created_at", "fingerprint"
51
55
  ],
52
56
  credentials: [
53
- "id", "name", "type", "scope", "data", "created_at", "updated_at"
57
+ "id", "name", "type", "scope", "data", "created_at", "updated_at", "ref"
54
58
  ],
55
59
  token_usage: [
56
60
  "id", "workflow_id", "execution_id", "node_id", "node_type",
@@ -40,6 +40,12 @@ const die = (msg) => {
40
40
  process.exit(1);
41
41
  };
42
42
 
43
+ // The platform repo's design/ was retired 2026-10-04: no org is authored here any more. An
44
+ // absent folder is no org tier, said plainly, rather than a lint error about a missing folder.
45
+ if (!existsSync(designRoot)) {
46
+ die(`no design/ folder at ${designRoot}. An org lives in its own repo and publishes from there; pass --design <folder> for a design tree kept elsewhere.`);
47
+ }
48
+
43
49
  if (!project) {
44
50
  const projects = listProjects(designRoot);
45
51
  die(`which project?\n\n unoverse publish <project> --to <universe>\n\n Found: ${projects.join(", ") || "none"}`);
@@ -286,8 +286,8 @@ build_component_nodes() {
286
286
 
287
287
  banner "Component Nodes (definition-backed)"
288
288
 
289
- echo " Building the marketplace package locally (bundle-defs + tsc → dist)..."
290
- if (cd "$ROOT/packages/marketplace" && node scripts/bundle-defs.mjs && npx tsc 2>/dev/null); then
289
+ echo " Building the marketplace package locally (tsc → dist)..."
290
+ if (cd "$ROOT/packages/marketplace" && npx tsc 2>/dev/null); then
291
291
  ok "Marketplace package built → packages/marketplace/dist"
292
292
  elif [ -d "$ROOT/packages/marketplace/dist" ]; then
293
293
  # Starters can't build it (no workspace plugin-base) — the dist SHIPS with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.214",
3
+ "version": "0.1.216",
4
4
  "description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
5
5
  "license": "SEE LICENSE IN README.md",
6
6
  "type": "module",
@@ -69,7 +69,7 @@ function packageVersion(designSystemDir) {
69
69
  }
70
70
  /**
71
71
  * Compute the version of the design system at `designSystemDir` (the resolved marketplace
72
- * directory: on-disk design/marketplace, or the installed bundle).
72
+ * directory: the tier on disk, or the installed bundle).
73
73
  *
74
74
  * Hashes components, atoms and styles together, because they only mean anything as a set:
75
75
  * a universe holding half of them renders against a contract with holes.
@@ -284,7 +284,7 @@ function collectFrom(root, project, designSystem) {
284
284
  // 2026-09-08 it looked at a `nodes/` folder beside design/ while every org already
285
285
  // kept its nodes INSIDE its project, so the same nodes were left behind again. Org
286
286
  // nodes are org-scoped like skills; the marketplace tier's own nodes sit under
287
- // design/marketplace/, which is never collected.
287
+ // the marketplace tier, which is never collected.
288
288
  const nodesHome = join(root, "nodes");
289
289
  if (existsSync(nodesHome)) {
290
290
  for (const pkg of collectNodePackages(nodesHome)) {
@@ -175,7 +175,7 @@ function checkAppTree(json, root, file) {
175
175
  if (ref.includes("/"))
176
176
  report("error", file, `state "${where}" preview names "${ref}" with an org prefix. Studio seeds a preview by the component's bare name ("${ref.split("/").pop()}"); a prefixed name resolves to nothing and the state's tab shows nothing (docs.unoverse.ai/design/apps)`);
177
177
  else if (comps && !comps.has(ref.toLowerCase()))
178
- report("error", file, `state "${where}" preview names "${ref}", which is no component or template this org can see (design/marketplace/, this org's components/ and templates/). Studio would seed nothing and the state's tab would show nothing (docs.unoverse.ai/design/apps)`);
178
+ report("error", file, `state "${where}" preview names "${ref}", which is no component or template this org can see (the marketplace's, this org's components/ and templates/). Studio would seed nothing and the state's tab would show nothing (docs.unoverse.ai/design/apps)`);
179
179
  }
180
180
  } else if (entry.preview !== undefined && (typeof entry.preview !== "object" || entry.preview === null))
181
181
  report("error", file, `state "${where}" preview must be a list of component names, or an object of app-state mock data (docs.unoverse.ai/design/apps)`);
@@ -578,7 +578,7 @@ function lintFile(file) {
578
578
  // 2026-09-11); the lookup is the name before the hash.
579
579
  for (const c of list)
580
580
  if (comps && !comps.has(String(c).split("#")[0].toLowerCase()))
581
- report("error", file, `preview."${state}" names unknown component "${c}". No match in design/marketplace/components/ or this org's components/ (org-privacy: another org's components are out of reach; lookup is case-insensitive)`);
581
+ report("error", file, `preview."${state}" names unknown component "${c}". No match in the marketplace's components/ or this org's components/ (org-privacy: another org's components are out of reach; lookup is case-insensitive)`);
582
582
  }
583
583
  }
584
584
  }
@@ -42,10 +42,13 @@ export function lintDefinitions(designRoot, options = {}) {
42
42
  const hit = overlay[resolve(f)];
43
43
  return hit === undefined ? readFileSync(f, "utf8") : hit;
44
44
  };
45
- const candidates = designRoot ? [resolve(designRoot)] : [resolve("apps/unoverse/design"), resolve("design")];
45
+ // With no root given: the platform repo's marketplace tier (linted as the root itself, as
46
+ // CI and the release do), else a developer's design/.
47
+ const candidates = designRoot ? [resolve(designRoot)] : [resolve("packages/marketplace/definitions"), resolve("design")];
46
48
 
47
- /** The monorepo's shapes: the design system at `design/marketplace/`, or the legacy one
48
- * with `components/` and `atoms/` loose at the root. */
49
+ /** A design root holding the design system at `<root>/marketplace/`, or the design system
50
+ * itself as the root, with `components/` and `atoms/` loose in it (how the platform repo
51
+ * lints its tier, `packages/marketplace/definitions`). */
49
52
  const holdsDesignSystem = (p) =>
50
53
  existsSync(join(p, "marketplace")) || existsSync(join(p, "components")) || existsSync(join(p, "atoms"));
51
54
 
@@ -53,7 +56,7 @@ export function lintDefinitions(designRoot, options = {}) {
53
56
  * A DEVELOPER'S design/ HOLDS ONLY ORG FOLDERS, and looking for the monorepo's shapes there
54
57
  * finds nothing. `design/<org>/{components,styles,apps}` is what Studio scaffolds and
55
58
  * the only layout a developer ever has, because the design system is INSTALLED rather
56
- * than authored (sync-starter.sh keeps `design/marketplace` out of a project on purpose).
59
+ * than authored (sync-starter.sh keeps the marketplace tier out of a project on purpose).
57
60
  *
58
61
  * So a project could not be linted, and since publishing lints first, it could not be
59
62
  * published either: "no design/ folder here", naming the folder it was standing in.
@@ -85,7 +88,24 @@ export function lintDefinitions(designRoot, options = {}) {
85
88
  */
86
89
  const ORG_REPO = options.org?.dir ? resolve(options.org.dir) : null;
87
90
  const ORG_NAME = options.org?.name ?? null;
88
- const DESIGN_ROOT = ORG_REPO ? dirname(ORG_REPO) : candidates.find((p) => holdsDesignSystem(p) || holdsOrgs(p));
91
+ /**
92
+ * THE MARKETPLACE TIER BY NAME, when it is not `<root>/marketplace`. The platform repo
93
+ * authors it at packages/marketplace/definitions (base paths.ts `marketplace`), outside any
94
+ * design root, and a caller that knows that says so. The tier then reads exactly as
95
+ * `<root>/marketplace` would: the design system, and a home named `marketplace`.
96
+ */
97
+ const MARKETPLACE = !ORG_REPO && options.marketplace ? resolve(options.marketplace) : null;
98
+ const DESIGN_ROOT = ORG_REPO
99
+ ? dirname(ORG_REPO)
100
+ : candidates.find((p) => holdsDesignSystem(p) || holdsOrgs(p)) ??
101
+ (MARKETPLACE && (existsSync(candidates[0]) || existsSync(MARKETPLACE)) ? candidates[0] : undefined);
102
+ /**
103
+ * NO ORG TIER HERE. The platform repo authors no orgs (its design/ was retired 2026-10-04),
104
+ * so a caller naming the marketplace tier may hand a design root that does not exist. That
105
+ * means "lint the tier alone", never "nothing to lint": an error here would fail every save
106
+ * in the platform repo's Studio on a folder nobody is meant to have.
107
+ */
108
+ const NO_ORG_TIER = !ORG_REPO && !existsSync(DESIGN_ROOT ?? "");
89
109
  if (!DESIGN_ROOT)
90
110
  return {
91
111
  problems: [{ level: "error", file: candidates[0], msg: `no design/ folder here (looked in: ${candidates.join(", ")})` }],
@@ -93,14 +113,15 @@ export function lintDefinitions(designRoot, options = {}) {
93
113
  };
94
114
 
95
115
  // ── tree layout ──
96
- // New layout: design/marketplace/{atoms,components,styles} + one top-level folder per
116
+ // New layout: <root>/marketplace/{atoms,components,styles} (or the tier named by
117
+ // options.marketplace) + one top-level folder per
97
118
  // org (design/<org>/). Legacy layout: components/ + atoms/ at the root, orgs under design/orgs/.
98
119
  // The DESIGN SYSTEM is the primary lint target; org folders get the SAME generic
99
120
  // checks — nothing here may key on a specific org's name.
100
- // WHERE THE DESIGN SYSTEM ACTUALLY IS. `design/marketplace/` exists in this monorepo and is
101
- // deliberately absent from a developer's project (sync-starter.sh): the platform installs
102
- // @unoverse-platform/marketplace, whose `definitions/` bundle carries components, atoms
103
- // AND styles (bundle-defs.mjs). All three matter — the space-scale check reads
121
+ // WHERE THE DESIGN SYSTEM ACTUALLY IS. `<root>/marketplace/` when a root carries one; it
122
+ // is deliberately absent from a developer's project (sync-starter.sh): the platform installs
123
+ // @unoverse-platform/marketplace, whose `definitions/` folder carries components, atoms
124
+ // AND styles (authored there, packages/marketplace/definitions in the platform repo). All three matter — the space-scale check reads
104
125
  // styles/base/spacing, so a fallback finding only components would build a PARTIAL scale
105
126
  // and reject valid steps, reporting false errors on correct work. Mirrors definitions.ts.
106
127
  const DS = (() => {
@@ -110,6 +131,7 @@ export function lintDefinitions(designRoot, options = {}) {
110
131
  const bundled = join(ORG_REPO, "plugins", "node_modules", "@unoverse-platform", "marketplace", "definitions");
111
132
  return existsSync(bundled) ? bundled : join(ORG_REPO, ".unoverse", "no-design-system");
112
133
  }
134
+ if (MARKETPLACE) return MARKETPLACE;
113
135
  const onDisk = join(DESIGN_ROOT, "marketplace");
114
136
  if (existsSync(onDisk)) return onDisk;
115
137
  for (const c of [
@@ -123,10 +145,15 @@ const HAS_DS = DS !== DESIGN_ROOT && existsSync(DS);
123
145
  /** The org a home folder belongs to: the declared name in an org repo, else the folder's name. */
124
146
  const orgName = (dir) => (ORG_REPO && dir === ORG_REPO ? ORG_NAME : basename(dir));
125
147
  /** Every home that can hold node packages, credentials or places: the design system and the orgs. */
126
- const tierHomes = () => (ORG_REPO ? [DS, ...orgDirs] : readdirSync(DESIGN_ROOT).map((t) => join(DESIGN_ROOT, t)));
148
+ const tierHomes = () =>
149
+ ORG_REPO ? [DS, ...orgDirs] : [...(MARKETPLACE ? [DS] : []), ...(NO_ORG_TIER ? [] : readdirSync(DESIGN_ROOT).map((t) => join(DESIGN_ROOT, t)))];
150
+ /** A file's path below the design root, the named marketplace tier reading as `marketplace/…`. */
151
+ const underRoot = (f) =>
152
+ MARKETPLACE && (f === DS || f.startsWith(DS + sep)) ? ["marketplace", ...relative(DS, f).split(sep)] : relative(DESIGN_ROOT, f).split(sep);
127
153
  const legacyOrgsDir = join(DESIGN_ROOT, "orgs");
128
154
  const orgDirs = (() => {
129
155
  if (ORG_REPO) return [ORG_REPO];
156
+ if (NO_ORG_TIER) return [];
130
157
  if (existsSync(legacyOrgsDir))
131
158
  return readdirSync(legacyOrgsDir)
132
159
  .filter((e) => !e.startsWith("."))
@@ -199,11 +226,11 @@ const isPropsExtract = (f) => {
199
226
  const isManifest = (f) => /^manifest\.(json|yaml)$/.test(basename(f));
200
227
  // WHICH HOME A FILE IS IN — asked of the path BELOW the design root, never the absolute
201
228
  // one. Testing the absolute path for an `apps` segment was true of every file in this
202
- // monorepo, because the design tree itself lives at `apps/unoverse/design`: every
229
+ // monorepo, because the design tree itself lived at `apps/unoverse/design`: every
203
230
  // component manifest in the repo was linted as an APP manifest (two bogus warnings each,
204
231
  // while the real component-manifest rules never ran once). A rule that cannot tell the
205
232
  // homes apart is worse than no rule — it trains people to ignore its output.
206
- const homeSegments = (f) => relative(DESIGN_ROOT, f).split(sep);
233
+ const homeSegments = (f) => underRoot(f);
207
234
  const isAppPath = (f) => homeSegments(f).includes("apps");
208
235
  // THE TEMPLATE KIND (UNOVERSE_TEMPLATE_MODEL.md §9b). Its folders answer to different
209
236
  // rules than an app's or a component's, so the rules that would misfire have to be able
@@ -931,7 +958,7 @@ function appSizesForFile(file) {
931
958
  return appSizesCache.get(home);
932
959
  }
933
960
 
934
- // The universal component names (design/marketplace/components/*, case-insensitive) — for
961
+ // The universal component names (the tier's components/*, case-insensitive) — for
935
962
  // validating an app manifest's `preview` map. null = the file is not under a design tree.
936
963
  const componentNamesCache = new Map();
937
964
  // Components an ORG's template may reference: the marketplace tier + that org's OWN
@@ -941,9 +968,10 @@ function componentNamesForFile(file) {
941
968
  // `${sep}orgs${sep}` and returned null for anything else, so on the flat layout — which
942
969
  // is every project now (`design/<org>/…`, "flat wins") — it answered "no list" and
943
970
  // every rule that asks it, preview names included, silently stopped checking.
944
- const segs = relative(DESIGN_ROOT, file).split(sep);
971
+ const segs = underRoot(file);
945
972
  if (segs.length < 2 || segs[0] === "..") return null;
946
973
  const projectSegs = segs[0] === "orgs" ? segs.slice(0, 2) : segs.slice(0, 1);
974
+ const projectHome = MARKETPLACE && projectSegs[0] === "marketplace" ? DS : join(DESIGN_ROOT, ...projectSegs);
947
975
  const org = projectSegs.join("/");
948
976
  const cached = componentNamesCache.get(org);
949
977
  if (cached !== undefined) return cached;
@@ -958,11 +986,11 @@ function componentNamesForFile(file) {
958
986
  }
959
987
  const dirs = [
960
988
  join(DS, "components"),
961
- join(DESIGN_ROOT, ...projectSegs, "components"),
989
+ join(projectHome, "components"),
962
990
  // TEMPLATES ARE PEERS (§9c): a state that holds one seeds it by name like any
963
991
  // component, so the name set a preview is checked against includes them.
964
992
  join(DS, "templates"),
965
- join(DESIGN_ROOT, ...projectSegs, "templates"),
993
+ join(projectHome, "templates"),
966
994
  ];
967
995
  let names = null;
968
996
  for (const dir of dirs) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The CLOSED SETS. This file is the linter's ground truth.
3
3
  *
4
- * Mirrors apps/unoverse/design/_schema/unoverse.schema.json and the server guards, and UNOVERSE_CONFORMANCE
4
+ * Mirrors packages/docs/schemas/design/unoverse.schema.json and the server guards, and UNOVERSE_CONFORMANCE
5
5
  * §5 makes keeping them equal a maintenance rule: "One source for each closed set... Keep
6
6
  * them equal or they disagree silently."
7
7
  *
@@ -10,7 +10,7 @@
10
10
  * without extending the schema and the SDK in the same change.
11
11
  */
12
12
 
13
- // ── ground truth (mirrors design/_schema/unoverse.schema.json + server guards) ──
13
+ // ── ground truth (mirrors packages/docs/schemas/design/unoverse.schema.json + server guards) ──
14
14
  export const PRIMITIVES = new Set([
15
15
  "Box", "Stack", "Row", "Column", "Each", "Switch", "ComponentSlot", "Timeline",
16
16
  "Text", "Image", "Button", "Input", "Select", "Markdown", "Skeleton", "Icon", "Ref", "Orb", "Template", "Place",
@@ -96,7 +96,7 @@
96
96
  },
97
97
  "template": {
98
98
  "type": "string",
99
- "description": "A Handlebars template resolved against the node's input context before the request is sent.\n\nFive roots:\n signal.<sourceId>.<outputHandle>.<field> upstream node output, e.g. {{signal.inputtrigger1.output.message}}\n config.<field> this node's own settings\n credentials.<name>.<field> this node's resolved credential\n services.<connector> RUNTIME service wiring, e.g. {{#unless services.mcpService.tools}}\n prompt.<blockName> a PROMPT BLOCK from the library (alias: blocks.<name>)\n\nThe services root exists because some request shaping depends on what is WIRED rather than configured, a fact only the executor knows at run time.\n\nThe prompt root is why a manifest must NEVER hardcode instruction text. Blocks live in design/marketplace/blocks/**/*.md, are authored and toggled in Studio, and are camelCased from the filename (markdown-guidelines.md becomes {{prompt.markdownGuidelines}}). A copy of a block's words baked into a node is a fork that silently stops tracking the block. Text belongs in the library; the node references it. This is the prompt-side twin of the SDK owning no styles.\n\nEvery root works in ANY template field, including a user's own systemPrompt: that is what lets an author compose blocks and upstream values into a prompt without the node knowing anything about it.\n\nThere is NO {{input.*}} root: a wrong path resolves to EMPTY silently. Array elements and object keys are dot segments, never brackets: records.0.Name, not records[0].Name."
99
+ "description": "A Handlebars template resolved against the node's input context before the request is sent.\n\nFive roots:\n signal.<sourceId>.<outputHandle>.<field> upstream node output, e.g. {{signal.inputtrigger1.output.message}}\n config.<field> this node's own settings\n credentials.<name>.<field> this node's resolved credential\n services.<connector> RUNTIME service wiring, e.g. {{#unless services.mcpService.tools}}\n prompt.<blockName> a PROMPT BLOCK from the library (alias: blocks.<name>)\n\nThe services root exists because some request shaping depends on what is WIRED rather than configured, a fact only the executor knows at run time.\n\nThe prompt root is why a manifest must NEVER hardcode instruction text. Blocks live in packages/marketplace/definitions/blocks/**/*.md, are authored and toggled in Studio, and are camelCased from the filename (markdown-guidelines.md becomes {{prompt.markdownGuidelines}}). A copy of a block's words baked into a node is a fork that silently stops tracking the block. Text belongs in the library; the node references it. This is the prompt-side twin of the SDK owning no styles.\n\nEvery root works in ANY template field, including a user's own systemPrompt: that is what lets an author compose blocks and upstream values into a prompt without the node knowing anything about it.\n\nThere is NO {{input.*}} root: a wrong path resolves to EMPTY silently. Array elements and object keys are dot segments, never brackets: records.0.Name, not records[0].Name."
100
100
  }
101
101
  }
102
102
  }
@@ -23,7 +23,7 @@ export const HANDLEBARS_HELPERS = new Set([
23
23
  ]);
24
24
 
25
25
  /**
26
- * The prompt-block library: design/marketplace/blocks/**\/*.md, keyed the way the resolver keys them
26
+ * The prompt-block library: the marketplace tier's blocks/**\/*.md, keyed the way the resolver keys them
27
27
  * (engine/src/template/promptBlocks.ts) — the filename camelCased, so markdown-guidelines.md is
28
28
  * referenced as {{prompt.markdownGuidelines}}.
29
29
  *
@@ -42,8 +42,8 @@ const CACHE = new Map();
42
42
 
43
43
  export function promptBlocks() {
44
44
  // The block library is the marketplace tier's. Beside the nodes when they ARE that
45
- // tier (design/marketplace/{nodes,blocks}); one tier over for an org's own nodes
46
- // (design/<org>/nodes → design/marketplace/blocks).
45
+ // tier (packages/marketplace/definitions/{nodes,blocks}); one tier over for an org's own
46
+ // nodes (design/<org>/nodes → design/marketplace/blocks, a design root carrying the tier).
47
47
  const home = [resolve(state.nodesHome, "../blocks"), resolve(state.nodesHome, "../../marketplace/blocks")]
48
48
  .find((d) => existsSync(d)) ?? resolve(state.nodesHome, "../../marketplace/blocks");
49
49
  const cached = CACHE.get(home);
@@ -672,7 +672,7 @@ export function lintNode(dir, pkg) {
672
672
  for (const [, b] of body.matchAll(/\b(?:prompt|blocks)\.([A-Za-z0-9_]+)/g))
673
673
  if (!promptBlocks().has(b) && !seen.has(b)) {
674
674
  seen.add(b);
675
- report("error", F.api, `references prompt block "{{prompt.${b}}}", which does not exist in design/marketplace/blocks/. It resolves to empty and the instruction silently disappears`);
675
+ report("error", F.api, `references prompt block "{{prompt.${b}}}", which does not exist in the marketplace's blocks/. It resolves to empty and the instruction silently disappears`);
676
676
  }
677
677
  }
678
678
  }
@@ -106,7 +106,13 @@ export function lintObjects(designRoot, options = {}) {
106
106
  }
107
107
  };
108
108
 
109
- if (!isDir(ROOT)) {
109
+ // The marketplace tier: `<root>/marketplace`, unless the caller names it (options.marketplace:
110
+ // the platform repo authors it outside any design root, base paths.ts `marketplace`).
111
+ const marketplace = options.marketplace ? resolve(options.marketplace) : join(ROOT, "marketplace");
112
+ // NO ORG TIER HERE: a named marketplace tier with no design root beside it (the platform
113
+ // repo, whose design/ was retired 2026-10-04) lints the tier alone. Only a root with neither
114
+ // is "nothing to lint".
115
+ if (!isDir(ROOT) && !(options.marketplace && isDir(marketplace))) {
110
116
  report("error", ROOT, "no design/ folder here");
111
117
  return { problems: [...problems], objectCount: 0, pipelineCount: 0, designRoot: ROOT };
112
118
  }
@@ -119,13 +125,12 @@ export function lintObjects(designRoot, options = {}) {
119
125
  // ── the tiers: the marketplace, then every org folder ──
120
126
  // An org's own repo is ONE tier with a DECLARED name (`options.org`, UNO-88): its folder may
121
127
  // be called anything, and the org name is also the dataset table's (`dataset_<org>_<name>`).
122
- const marketplace = join(ROOT, "marketplace");
123
128
  const tiers = [];
124
129
  if (options.org?.dir) {
125
130
  tiers.push({ org: options.org.name, dir: resolve(options.org.dir) });
126
131
  } else {
127
132
  if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
128
- for (const e of readdirSync(ROOT)) {
133
+ for (const e of isDir(ROOT) ? readdirSync(ROOT) : []) {
129
134
  if (e.startsWith(".") || e === "marketplace" || e === "_schema") continue;
130
135
  const d = join(ROOT, e);
131
136
  if (isDir(d)) tiers.push({ org: e, dir: d });
@@ -265,22 +270,38 @@ export function lintObjects(designRoot, options = {}) {
265
270
 
266
271
  // extends: resolves, no cycle, across the tiers the org can see.
267
272
  let parentProps = null;
273
+ // A BASE THAT CANNOT BE SEEN is not a missing base. An org repo carries no marketplace on
274
+ // disk (UNO-88), so `extends: need` names an object installed in every universe and present
275
+ // in none of the repo's folders. The same rule as the nodes above: step back to a hint, read
276
+ // a description-only prop as the re-brief of an inherited field (the only thing it can be),
277
+ // and leave the row keys, which need the base's fields, to the universe that has them.
278
+ let baseUnseen = false;
268
279
  const base = isRecord(f.envelope) ? f.envelope.extends : undefined;
269
280
  if (typeof base === "string") {
270
281
  const parent = findObject(base, f.org);
271
- if (!parent) report("error", rel(f.envFile), `object ${f.name}: extends "${base}", which no tier holds`);
282
+ if (!parent && options.org?.dir && !marketplaceSeen) {
283
+ baseUnseen = true;
284
+ report("hint", rel(f.envFile), `object ${f.name}: extends "${base}", which is not on disk here (the marketplace is installed, not in the repo), so what it inherits was not checked`);
285
+ parentProps = Object.fromEntries(
286
+ Object.entries(isRecord(f.props) ? f.props : {}).filter(([, p]) => isRecord(p) && Object.keys(p).every((k) => k === "description")),
287
+ );
288
+ } else if (!parent) report("error", rel(f.envFile), `object ${f.name}: extends "${base}", which no tier holds`);
272
289
  else {
273
290
  parentProps = composedProps(parent, [f.name]);
274
291
  if (parentProps === null) report("error", rel(f.envFile), `object ${f.name}: extends cycle through ${base}`);
275
292
  }
276
293
  }
277
294
  say(propsFindings(f.name, f.props, parentProps, base, basename(f.propsFile)));
278
- if (isRecord(f.props)) for (const [k, p] of Object.entries(f.props)) if (isRecord(p) && p.from != null) say(fromFindings(f.name, k, p.from, composedService(f), basename(f.propsFile)));
295
+ // With the base unseen, a function the object does not declare itself may be the base's
296
+ // (README: a prop may be written from an inherited function), so only its own are checked.
297
+ const fns = composedService(f);
298
+ const ownFn = (from) => typeof from === "string" && from.split(".")[0] in fns;
299
+ if (isRecord(f.props)) for (const [k, p] of Object.entries(f.props)) if (isRecord(p) && p.from != null && (!baseUnseen || ownFn(p.from))) say(fromFindings(f.name, k, p.from, fns, basename(f.propsFile)));
279
300
  say(serviceFindings(f.name, f.service, visibleNodes(tier)));
280
301
  say(makeFindings(f.name, f.make, f.service, visibleNodes(tier)));
281
302
  if (!marketplaceSeen && isRecord(f.service)) report("hint", rel(f.servicePath), `object ${f.name}: the marketplace is not on disk here, so the nodes its calls name were not checked`);
282
303
  const all = composedProps(f) ?? (isRecord(f.props) ? f.props : {});
283
- say(rowKeyFindings(f.name, f.envelope, all, basename(f.envFile)));
304
+ if (!baseUnseen) say(rowKeyFindings(f.name, f.envelope, all, basename(f.envFile)));
284
305
  say(objectTestFindings(f.name, f.test, ownFunctions(f)));
285
306
  // An org may never take a marketplace name: a bare object_type must stay unambiguous.
286
307
  if (f.org && marketplaceObjects.has(f.name)) report("error", rel(f.envFile), `object ${f.name}: an org's object may not take a marketplace object's name; the row's object_type would be ambiguous. Rename it, or extend ${f.name} under a name of its own`);
@@ -135,21 +135,26 @@ export function lintPrompts(designRoot, options = {}) {
135
135
  const present = (f) => existsSync(f) || overlay[resolve(f)] !== undefined;
136
136
  const rel = (f) => (relative(process.cwd(), f).startsWith("..") ? f : relative(process.cwd(), f));
137
137
 
138
- if (!isDir(ROOT)) {
138
+ // The marketplace tier: `<root>/marketplace`, unless the caller names it (options.marketplace:
139
+ // the platform repo authors it outside any design root, base paths.ts `marketplace`).
140
+ const marketplace = options.marketplace ? resolve(options.marketplace) : join(ROOT, "marketplace");
141
+ // NO ORG TIER HERE: a named marketplace tier with no design root beside it (the platform
142
+ // repo, whose design/ was retired 2026-10-04) lints the tier alone. Only a root with neither
143
+ // is "nothing to lint".
144
+ if (!isDir(ROOT) && !(options.marketplace && isDir(marketplace))) {
139
145
  report("error", ROOT, "no design/ folder here");
140
146
  return { problems: [...problems], skillCount: 0, blockCount: 0, designRoot: ROOT };
141
147
  }
142
148
 
143
149
  // ── the tiers: the marketplace, every flat org folder, and the legacy orgs/ container ──
144
150
  const tiers = [];
145
- const marketplace = join(ROOT, "marketplace");
146
151
  const orgDirs = (base) => readdirSync(base).filter((e) => !e.startsWith(".") && !e.startsWith("_") && e !== "marketplace" && e !== "orgs" && isDir(join(base, e)));
147
152
  if (options.org?.dir) {
148
153
  // An org's own repo: one tier, its name declared, never read off the folder (UNO-88).
149
154
  tiers.push({ org: options.org.name, dir: resolve(options.org.dir) });
150
155
  } else {
151
156
  if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
152
- for (const e of orgDirs(ROOT)) tiers.push({ org: e, dir: join(ROOT, e) });
157
+ if (isDir(ROOT)) for (const e of orgDirs(ROOT)) tiers.push({ org: e, dir: join(ROOT, e) });
153
158
  if (isDir(join(ROOT, "orgs"))) for (const e of orgDirs(join(ROOT, "orgs"))) tiers.push({ org: e, dir: join(ROOT, "orgs", e) });
154
159
  }
155
160