@unoverse-platform/studio 0.1.109 → 0.1.111
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/bin/studio.mjs +3 -3
- package/local/authoring.ts +2 -2
- package/local/catalog.ts +8 -4
- package/local/design-home.d.mts +2 -0
- package/local/design-home.mjs +11 -0
- package/local/plugin.ts +51 -22
- package/local/sample-identity/brand.yaml +1 -1
- package/local/sample-identity/organisation.yaml +1 -1
- package/local/sample-identity/purpose.yaml +1 -1
- package/local/sample-identity/story.yaml +1 -1
- package/local/scaffold.mjs +20 -4
- package/local/source.ts +13 -7
- package/package.json +1 -1
- package/screens/DesignSystemView.tsx +1 -1
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/PromptBlocksView.tsx +1 -1
- package/src/App.tsx +5 -5
- package/vendor/base/src/definitions/load/assets.ts +2 -2
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/datasetSql.ts +7 -12
- package/vendor/base/src/definitions/registry/homes.ts +15 -5
- package/vendor/base/src/definitions/registry/objects.ts +1 -1
- package/vendor/base/src/definitions/registry/orgDocuments.ts +2 -2
- package/vendor/base/src/definitions/registry/orgPack.ts +3 -2
- package/vendor/base/src/definitions/registry/pipelines.ts +1 -1
- package/vendor/base/src/definitions/theme.ts +5 -6
- package/vendor/base/src/items/baseVersion.ts +1 -1
- package/vendor/base/src/items/collect.ts +1 -1
- package/vendor/base/src/lint/design/file.mjs +2 -2
- package/vendor/base/src/lint/design/index.d.mts +6 -0
- package/vendor/base/src/lint/design/index.mjs +45 -17
- package/vendor/base/src/lint/design/vocabulary.mjs +2 -2
- package/vendor/base/src/lint/nodes/blocks.mjs +3 -3
- package/vendor/base/src/lint/nodes/node.mjs +1 -1
- package/vendor/base/src/lint/objects/index.d.mts +6 -0
- package/vendor/base/src/lint/objects/index.mjs +27 -6
- package/vendor/base/src/lint/prompts/index.d.mts +6 -0
- package/vendor/base/src/lint/prompts/index.mjs +8 -3
- package/vendor/base/src/paths.ts +65 -20
- package/vendor/base/src/plugins/discovery.ts +1 -1
- package/vendor/base/src/plugins/loader.ts +5 -7
- package/vendor/base/src/security/rules.ts +6 -0
|
@@ -48,7 +48,8 @@ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
|
|
|
48
48
|
|
|
49
49
|
/** The shared homes for the universal kinds — the marketplace.
|
|
50
50
|
*
|
|
51
|
-
* Resolve each shared dir from the on-disk
|
|
51
|
+
* Resolve each shared dir from the on-disk tier (paths.ts `marketplace`: the monorepo's
|
|
52
|
+
* packages/marketplace/definitions, so a local universe reads it live) if present,
|
|
52
53
|
* else from a bundle beside the home node, else from what this universe INSTALLED (rows,
|
|
53
54
|
* hydrated). Mirrors theme.ts's styles fallback, and the getters resolve LAZILY so a tier
|
|
54
55
|
* that appears after boot is picked up on first use.
|
|
@@ -62,7 +63,7 @@ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
|
|
|
62
63
|
* server and Canvas — "they do not carry components, atoms, styles, skills, prompt blocks
|
|
63
64
|
* or nodes, all of which are rows". A dead path that would silently adopt an npm tree if
|
|
64
65
|
* one ever appeared is worse than no path. */
|
|
65
|
-
const MARKETPLACE =
|
|
66
|
+
const MARKETPLACE = getPaths().marketplace;
|
|
66
67
|
const DS_BUNDLE_CANDIDATES: string[] = [];
|
|
67
68
|
function marketplaceDir(kind: "components" | "atoms" | "templates" | "objects" | "pipelines"): string {
|
|
68
69
|
const onDisk = join(MARKETPLACE, kind);
|
|
@@ -105,6 +106,15 @@ export const SHARED_DIR = {
|
|
|
105
106
|
},
|
|
106
107
|
} as Record<"component" | "atom" | "template" | "object" | "pipeline", string>;
|
|
107
108
|
|
|
109
|
+
/** The marketplace tier's own folder, for what is not one of the kinds above (its shared
|
|
110
|
+
* `assets/`): on disk when this checkout authors it, else what this universe installed.
|
|
111
|
+
* The same order `projectDir` applies to an org. */
|
|
112
|
+
export function marketplaceHome(): string {
|
|
113
|
+
const installed = join(INSTALLED_DESIGN, "marketplace");
|
|
114
|
+
if (databaseOnly()) return installed;
|
|
115
|
+
return existsSync(MARKETPLACE) ? MARKETPLACE : installed;
|
|
116
|
+
}
|
|
117
|
+
|
|
108
118
|
// rx-root entries that are NOT projects: the marketplace, the schema, and the legacy
|
|
109
119
|
// orgs/ container itself. Everything else at the root is a flat project (the target model).
|
|
110
120
|
const RESERVED_DESIGN = new Set(["marketplace", "_schema"]);
|
|
@@ -181,7 +191,7 @@ export function appDirs(refOrg: string | null): { org: string; dir: string }[] {
|
|
|
181
191
|
|
|
182
192
|
/** The TEMPLATE homes a ref resolves against — the Template Model's kind
|
|
183
193
|
* (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md). Two tiers like components: the
|
|
184
|
-
* marketplace shelf (`
|
|
194
|
+
* marketplace shelf (`packages/marketplace/definitions/templates`, the BASE shapes) and the
|
|
185
195
|
* org packs (`design/<org>/templates`). The word is free in code because the app
|
|
186
196
|
* kind's in-code name is `app` (rename finished in code 2026-08-28; stored words
|
|
187
197
|
* unchanged until their own migration). */
|
|
@@ -194,7 +204,7 @@ export function templateDirs(refOrg: string | null): { org: string | undefined;
|
|
|
194
204
|
}
|
|
195
205
|
|
|
196
206
|
/** The OBJECT homes a ref resolves against (docs/unoverse/data/objects/README.md): the same two
|
|
197
|
-
* tiers as components, the marketplace (`
|
|
207
|
+
* tiers as components, the marketplace (`packages/marketplace/definitions/objects`, the platform's kinds,
|
|
198
208
|
* and `need`, the atom every object extends) and the org packs (`design/<org>/objects`, an
|
|
199
209
|
* org's own kinds). An org-qualified ref searches its org first, then the marketplace, so
|
|
200
210
|
* `extends: need` resolves from an org's object; a bare ref searches the marketplace first. */
|
|
@@ -207,7 +217,7 @@ export function objectDirs(refOrg: string | null): { org: string | undefined; di
|
|
|
207
217
|
}
|
|
208
218
|
|
|
209
219
|
/** The PIPELINE homes a ref resolves against (docs/unoverse/data/pipelines/README.md layer 1): the marketplace
|
|
210
|
-
* (`
|
|
220
|
+
* (`packages/marketplace/definitions/pipelines`, the sources everyone gets: website, spreadsheet, images) and
|
|
211
221
|
* the org packs (`design/<org>/pipelines`, a source nobody else has). Same order as objects. */
|
|
212
222
|
export function pipelineDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
|
|
213
223
|
const shared = { org: undefined as string | undefined, dir: SHARED_DIR.pipeline };
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* objects/<name>/api/service.yaml the object's public functions by name (getDetail, searchDetail), as a
|
|
14
14
|
* service node's tools; found by name, declared nowhere else
|
|
15
15
|
*
|
|
16
|
-
* in two tiers, the marketplace's (`
|
|
16
|
+
* in two tiers, the marketplace's (`packages/marketplace/definitions/objects/`, shared by every org) and an
|
|
17
17
|
* org's own (`design/<org>/objects/`). `extends` names another object and is resolved here,
|
|
18
18
|
* across the tiers, never as a path: the base's props compose under the object's own, later
|
|
19
19
|
* keys over earlier. `need` is the atom every other positioned object extends (owner ruling
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Four objects in the marketplace pack say the SHAPE, brief by brief:
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* packages/marketplace/definitions/objects/<doc>/ organisation, brand, purpose, story
|
|
8
8
|
*
|
|
9
9
|
* and the org's pack holds its ANSWERS, one instance a file, in the one grammar:
|
|
10
10
|
*
|
|
@@ -71,7 +71,7 @@ export function instanceText(doc: OrgDocument, values: Record<string, unknown> =
|
|
|
71
71
|
if (!object) throw new Error(`identity/${doc}: no object named ${doc} in any tier`);
|
|
72
72
|
const props: Record<string, { default: unknown }> = {};
|
|
73
73
|
for (const [name, p] of writable(object)) props[name] = { default: name in values ? values[name] : emptyValue(p.type) };
|
|
74
|
-
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 (
|
|
74
|
+
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 (packages/marketplace/definitions/objects/${doc}/); this file carries the answers as defaults.\n`;
|
|
75
75
|
return head + stringify({ unoverse: "1.0", type: "identity", extends: doc, props }, { lineWidth: 100 });
|
|
76
76
|
}
|
|
77
77
|
|
|
@@ -16,12 +16,13 @@
|
|
|
16
16
|
* semantic tokens are INHERITED at resolve time (an org is a theme)
|
|
17
17
|
*
|
|
18
18
|
* Shapes (ruled 2026-09-08): each identity file is an INSTANCE of its marketplace object
|
|
19
|
-
* (
|
|
19
|
+
* (the marketplace tier's objects/<doc>/), the one grammar with `extends: <doc>` and props
|
|
20
20
|
* carrying `default` only. The briefs are the object's. Nothing is invented: the defaults
|
|
21
21
|
* are empty until a writer fills them from the org's own sources.
|
|
22
22
|
*/
|
|
23
23
|
import { cpSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
24
24
|
import { join } from "node:path";
|
|
25
|
+
import { getPaths } from "../../paths.js";
|
|
25
26
|
import { ORG_DOCUMENTS, instanceText } from "./orgDocuments.js";
|
|
26
27
|
|
|
27
28
|
export const ORG_SLUG = /^[a-z][a-z0-9-]{0,63}$/;
|
|
@@ -34,7 +35,7 @@ export function createOrgPack(designHome: string, slug: string, name: string): s
|
|
|
34
35
|
if (!ORG_SLUG.test(slug)) throw new Error("slug: lowercase letters, digits and hyphens, starting with a letter");
|
|
35
36
|
const dest = join(designHome, slug);
|
|
36
37
|
if (existsSync(dest)) throw new Error(`design/${slug} already exists`);
|
|
37
|
-
const themes = join(
|
|
38
|
+
const themes = join(getPaths().marketplace, "styles", "themes");
|
|
38
39
|
if (!existsSync(themes)) throw new Error("the marketplace theme foundation is missing; cannot seed the org's themes");
|
|
39
40
|
|
|
40
41
|
mkdirSync(join(dest, "apps"), { recursive: true });
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* pipelines/<name>/interface.yaml the pipeline as a node on a canvas: signal in, rows out
|
|
16
16
|
* pipelines/<name>/test.yaml the collect steps faked, the rows asserted
|
|
17
17
|
*
|
|
18
|
-
* in two tiers, the marketplace's (`
|
|
18
|
+
* in two tiers, the marketplace's (`packages/marketplace/definitions/pipelines/`) and an org's own
|
|
19
19
|
* (`design/<org>/pipelines/`). Nodes are the only way a pipeline touches the world: no HTTP,
|
|
20
20
|
* no credentials, no hosts in the file; the nodes it names carry them.
|
|
21
21
|
*
|
|
@@ -36,9 +36,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
36
36
|
// VALUES are each org's own — that's how two clients get different fonts/colors/spacing
|
|
37
37
|
// from the same components, without re-copying the foundation.
|
|
38
38
|
import { listOrgs, projectDir } from "./definitions.js";
|
|
39
|
-
import { PLUGINS_DIR,
|
|
39
|
+
import { PLUGINS_DIR, MARKETPLACE_HOME, INSTALLED_HOME, databaseOnly, designDir } from "../paths.js";
|
|
40
40
|
|
|
41
|
-
const DESIGN = DESIGN_HOME;
|
|
42
41
|
export const DEFAULT_ORG = "default";
|
|
43
42
|
|
|
44
43
|
/** The styles dir for a project, or null if it has none. `projectDir` resolves the
|
|
@@ -51,11 +50,11 @@ function orgStyles(org: string): string | null {
|
|
|
51
50
|
/**
|
|
52
51
|
* The marketplace FOUNDATION — base + semantic + default themes, the shared token
|
|
53
52
|
* contract every org inherits (buildTheme's fallback layer) AND the `default` theme set
|
|
54
|
-
* itself. Its home is
|
|
55
|
-
*
|
|
53
|
+
* itself. Its home is the marketplace tier's `styles/` (paths.ts `marketplace`; no longer
|
|
54
|
+
* an org costume under `orgs/default`). Resolution mirrors `findDesignComponentsDir`:
|
|
56
55
|
* the on-disk marketplace dir wins (dev/local), else the bundled package — the monorepo
|
|
57
56
|
* home node, then the installed marketplace package. This lets the foundation ship WITH
|
|
58
|
-
* the package (no
|
|
57
|
+
* the package (no marketplace tier on disk) once purged from the image (Phase 4).
|
|
59
58
|
*/
|
|
60
59
|
const DS_STYLES_CANDIDATES = [
|
|
61
60
|
join(PLUGINS_DIR, "node_modules", "@unoverse-platform", "marketplace", "definitions", "styles"),
|
|
@@ -80,7 +79,7 @@ function foundationStyles(): string | null {
|
|
|
80
79
|
// Under the switch the authored tiers are skipped, so what a deployed universe would
|
|
81
80
|
// resolve is what a developer resolves. Same shape as `marketplaceDir`.
|
|
82
81
|
if (databaseOnly()) return existsSync(INSTALLED_STYLES) ? INSTALLED_STYLES : null;
|
|
83
|
-
const designDs = join(
|
|
82
|
+
const designDs = join(MARKETPLACE_HOME, "styles");
|
|
84
83
|
if (existsSync(designDs)) return designDs; // on-disk marketplace wins (dev/local)
|
|
85
84
|
for (const c of DS_STYLES_CANDIDATES) if (existsSync(c)) return c;
|
|
86
85
|
// SEARCHED LAST, so anything on disk still wins — the same precedence the node loader
|
|
@@ -67,7 +67,7 @@ function packageVersion(designSystemDir: string): string | null {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Compute the version of the design system at `designSystemDir` (the resolved marketplace
|
|
70
|
-
* directory: on
|
|
70
|
+
* directory: the tier on disk, or the installed bundle).
|
|
71
71
|
*
|
|
72
72
|
* Hashes components, atoms and styles together, because they only mean anything as a set:
|
|
73
73
|
* a universe holding half of them renders against a contract with holes.
|
|
@@ -300,7 +300,7 @@ function collectFrom(root: string, project: string, designSystem: string): Colle
|
|
|
300
300
|
// 2026-09-08 it looked at a `nodes/` folder beside design/ while every org already
|
|
301
301
|
// kept its nodes INSIDE its project, so the same nodes were left behind again. Org
|
|
302
302
|
// nodes are org-scoped like skills; the marketplace tier's own nodes sit under
|
|
303
|
-
//
|
|
303
|
+
// the marketplace tier, which is never collected.
|
|
304
304
|
const nodesHome = join(root, "nodes");
|
|
305
305
|
if (existsSync(nodesHome)) {
|
|
306
306
|
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 (
|
|
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
|
|
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
|
}
|
|
@@ -29,6 +29,12 @@ export interface LintDefinitionsOptions {
|
|
|
29
29
|
overlay?: Record<string, string>;
|
|
30
30
|
/** An org's own repo (UNO-88): its folder and its DECLARED name. The org list is then this one. */
|
|
31
31
|
org?: { name: string; dir: string };
|
|
32
|
+
/**
|
|
33
|
+
* The marketplace tier's folder, when it is not `<root>/marketplace`: the platform repo
|
|
34
|
+
* authors it at packages/marketplace/definitions (base paths.ts `marketplace`), outside
|
|
35
|
+
* any design root. Only a caller that knows the folder exists passes it.
|
|
36
|
+
*/
|
|
37
|
+
marketplace?: string;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export function lintDefinitions(rxHome?: string, options?: LintDefinitionsOptions): DefinitionsResult;
|
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
48
|
-
* with `components/` and `atoms/` loose
|
|
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
|
|
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
|
-
|
|
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:
|
|
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.
|
|
101
|
-
// deliberately absent from a developer's project (sync-starter.sh): the platform installs
|
|
102
|
-
// @unoverse-platform/marketplace, whose `definitions/`
|
|
103
|
-
// AND styles (
|
|
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 = () =>
|
|
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
|
|
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) =>
|
|
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 (
|
|
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 =
|
|
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(
|
|
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(
|
|
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
|
|
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/
|
|
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",
|
|
@@ -23,7 +23,7 @@ export const HANDLEBARS_HELPERS = new Set([
|
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
|
-
* The prompt-block library:
|
|
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 (
|
|
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
|
|
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
|
}
|
|
@@ -25,6 +25,12 @@ export interface LintObjectsOptions {
|
|
|
25
25
|
overlay?: Record<string, string>;
|
|
26
26
|
/** An org's own repo (UNO-88): its folder and its DECLARED name. The org list is then this one. */
|
|
27
27
|
org?: { name: string; dir: string };
|
|
28
|
+
/**
|
|
29
|
+
* The marketplace tier's folder, when it is not `<root>/marketplace`: the platform repo
|
|
30
|
+
* authors it at packages/marketplace/definitions (base paths.ts `marketplace`), outside
|
|
31
|
+
* any design root. Only a caller that knows the folder exists passes it.
|
|
32
|
+
*/
|
|
33
|
+
marketplace?: string;
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
/** Lint every object and pipeline folder under `designRoot`, every tier. Prints nothing, throws nothing for a lint failure. */
|
|
@@ -106,7 +106,13 @@ export function lintObjects(designRoot, options = {}) {
|
|
|
106
106
|
}
|
|
107
107
|
};
|
|
108
108
|
|
|
109
|
-
|
|
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
|
|
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
|
-
|
|
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`);
|
|
@@ -24,6 +24,12 @@ export interface LintPromptsOptions {
|
|
|
24
24
|
overlay?: Record<string, string>;
|
|
25
25
|
/** An org's own repo (UNO-88): its folder and its DECLARED name. The org list is then this one. */
|
|
26
26
|
org?: { name: string; dir: string };
|
|
27
|
+
/**
|
|
28
|
+
* The marketplace tier's folder, when it is not `<root>/marketplace`: the platform repo
|
|
29
|
+
* authors it at packages/marketplace/definitions (base paths.ts `marketplace`), outside
|
|
30
|
+
* any design root. Only a caller that knows the folder exists passes it.
|
|
31
|
+
*/
|
|
32
|
+
marketplace?: string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
/** Lint every skill and prompt block under `designRoot`, every tier. Prints nothing, throws nothing for a lint failure. */
|
|
@@ -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
|
-
|
|
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
|
|