unoverse 0.1.172 → 0.1.174
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/operator/lib/db-verify.sh +1 -0
- package/package.json +1 -1
- package/vendor/base/lint/design/defs.mjs +1 -1
- package/vendor/base/lint/design/file.mjs +10 -2
- package/vendor/base/lint/design/index.mjs +37 -37
- package/vendor/base/lint/design/tokens.mjs +2 -2
- package/vendor/base/lint/design/vocabulary.mjs +52 -2
- package/vendor/base/lint/design/walk.mjs +22 -2
|
@@ -108,6 +108,7 @@ cmd_db_verify() {
|
|
|
108
108
|
dictionary_regions: [
|
|
109
109
|
"region_id", "workflow_id", "depth", "name", "description", "name_locked",
|
|
110
110
|
"locked_at", "locked_by", "stage", "stage_set_by", "needs_review",
|
|
111
|
+
"skills", "skills_set_by", "skills_set_at",
|
|
111
112
|
"derived_from", "first_seen", "last_seen", "closed_at"
|
|
112
113
|
],
|
|
113
114
|
dictionary_content_chunks: [
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Definition file formats: how
|
|
2
|
+
* Definition file formats: how a design definition is named, found and parsed.
|
|
3
3
|
*
|
|
4
4
|
* Mirrors the server's fsCache.ts, deliberately. A definition is `.yaml` or `.json`, and
|
|
5
5
|
* the linter must agree with the loader about which file IS the definition, or it lints a
|
|
@@ -34,7 +34,7 @@ function checkStateOrder(order, rootFolder, file, includeLayouts = false) {
|
|
|
34
34
|
const onDisk = new Set([...stateNames, ...(includeLayouts ? dirNames("layouts") : [])]);
|
|
35
35
|
for (const name of order)
|
|
36
36
|
if (typeof name === "string" && !onDisk.has(name))
|
|
37
|
-
report("error", file, `stateOrder lists "${name}" but no states/${name}${includeLayouts ? ` or layouts/${name}` : ""} definition exists (docs/design/${includeLayouts ? "
|
|
37
|
+
report("error", file, `stateOrder lists "${name}" but no states/${name}${includeLayouts ? ` or layouts/${name}` : ""} definition exists (docs.unoverse.ai/design/${includeLayouts ? "apps" : "components"})`);
|
|
38
38
|
// Only STATES must appear in stateOrder to lock the picker order; the default layout is
|
|
39
39
|
// legitimately omitted, so never warn on layouts.
|
|
40
40
|
for (const name of stateNames)
|
|
@@ -239,7 +239,7 @@ function lintFile(file) {
|
|
|
239
239
|
}
|
|
240
240
|
for (const c of list)
|
|
241
241
|
if (comps && !comps.has(String(c).toLowerCase()))
|
|
242
|
-
report("error", file, `preview."${state}" names unknown component "${c}". No match in
|
|
242
|
+
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)`);
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
}
|
|
@@ -488,6 +488,14 @@ function lintFile(file) {
|
|
|
488
488
|
if (typeof arrival !== "string")
|
|
489
489
|
report("error", file, `a faced component must declare its base state — a v2 state.view tree \`initial\`, or (legacy) manifest.defaultState / state.defaultState (docs.unoverse.ai/design/components)`);
|
|
490
490
|
}
|
|
491
|
+
// A tree DECLARES its order, so an authored stateOrder beside one is a second source
|
|
492
|
+
// of truth that can silently disagree with it. Five docs call this form legacy and
|
|
493
|
+
// nothing enforced it, so a component kept running on one and never heard a word.
|
|
494
|
+
// The old check only fired when a states/ FOLDER existed, which a fragment-based
|
|
495
|
+
// component does not have (docs/doc-control §Guards: enforce rules as tests).
|
|
496
|
+
if (viewTree && json.stateOrder !== undefined)
|
|
497
|
+
report("error", file, `authored "stateOrder" beside a "state.view" tree. The tree declares the order: nest these as substates and delete the list (docs.unoverse.ai/design/state)`);
|
|
498
|
+
|
|
491
499
|
if (stateFiles.length) {
|
|
492
500
|
const order = Array.isArray(json.stateOrder) ? [...json.stateOrder].sort() : null;
|
|
493
501
|
if (!order || !order.length)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The design definitions linter, as a LIBRARY. Rules in, findings out.
|
|
3
3
|
*
|
|
4
4
|
* Sibling of ../nodes/ and deliberately SEPARATE: this one knows the closed primitive set,
|
|
5
5
|
* the closed style vocabulary, token-only values and Switch discriminants. Merging the two
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `yaml` in. Worth keeping now that it sits beside a package that does have dependencies.
|
|
10
10
|
*
|
|
11
11
|
* THE WHOLE BODY IS INSIDE `lintDefinitions`, on purpose. It was a script whose top-level
|
|
12
|
-
* constants derived from the
|
|
12
|
+
* constants derived from the design root, and two bare blocks that RAN RULES at import time.
|
|
13
13
|
* As a library that is wrong twice: findings would accumulate between runs, and rules would
|
|
14
14
|
* fire before anyone asked. Function scope makes both impossible rather than merely unlikely.
|
|
15
15
|
*
|
|
@@ -26,7 +26,7 @@ import { makeTokensForFile } from "./tokens.mjs";
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Lint every definition under `designRoot`. Prints nothing, exits nothing.
|
|
29
|
-
* A missing
|
|
29
|
+
* A missing design/ folder is REPORTED rather than thrown, so callers get one shape of answer.
|
|
30
30
|
*
|
|
31
31
|
* `options.overlay` maps an absolute path to text that STANDS IN for what is on disk.
|
|
32
32
|
* Studio's editor lints what the developer has typed before it is saved, and the only
|
|
@@ -44,19 +44,19 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
44
44
|
};
|
|
45
45
|
const candidates = designRoot ? [resolve(designRoot)] : [resolve("apps/unoverse/design"), resolve("design")];
|
|
46
46
|
|
|
47
|
-
/** The monorepo's shapes: the design system at `
|
|
47
|
+
/** The monorepo's shapes: the design system at `design/marketplace/`, or the legacy one
|
|
48
48
|
* with `components/` and `atoms/` loose at the root. */
|
|
49
49
|
const holdsDesignSystem = (p) =>
|
|
50
50
|
existsSync(join(p, "marketplace")) || existsSync(join(p, "components")) || existsSync(join(p, "atoms"));
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* A DEVELOPER'S
|
|
54
|
-
* finds nothing. `
|
|
53
|
+
* A DEVELOPER'S design/ HOLDS ONLY ORG FOLDERS, and looking for the monorepo's shapes there
|
|
54
|
+
* finds nothing. `design/<org>/{components,styles,apps}` is what Studio scaffolds and
|
|
55
55
|
* the only layout a developer ever has, because the design system is INSTALLED rather
|
|
56
|
-
* than authored (sync-starter.sh keeps `
|
|
56
|
+
* than authored (sync-starter.sh keeps `design/marketplace` out of a project on purpose).
|
|
57
57
|
*
|
|
58
58
|
* So a project could not be linted, and since publishing lints first, it could not be
|
|
59
|
-
* published either: "no
|
|
59
|
+
* published either: "no design/ folder here", naming the folder it was standing in.
|
|
60
60
|
*/
|
|
61
61
|
const holdsOrgs = (p) => {
|
|
62
62
|
if (!existsSync(p)) return false;
|
|
@@ -75,36 +75,36 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
75
75
|
});
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
const
|
|
79
|
-
if (!
|
|
78
|
+
const DESIGN_ROOT = candidates.find((p) => holdsDesignSystem(p) || holdsOrgs(p));
|
|
79
|
+
if (!DESIGN_ROOT)
|
|
80
80
|
return {
|
|
81
|
-
problems: [{ level: "error", file: candidates[0], msg: `no
|
|
81
|
+
problems: [{ level: "error", file: candidates[0], msg: `no design/ folder here (looked in: ${candidates.join(", ")})` }],
|
|
82
82
|
homes: [],
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
// ── tree layout ──
|
|
86
|
-
// New layout:
|
|
87
|
-
// org (
|
|
86
|
+
// New layout: design/marketplace/{atoms,components,styles} + one top-level folder per
|
|
87
|
+
// org (design/<org>/). Legacy layout: components/ + atoms/ at the root, orgs under design/orgs/.
|
|
88
88
|
// The DESIGN SYSTEM is the primary lint target; org folders get the SAME generic
|
|
89
89
|
// checks — nothing here may key on a specific org's name.
|
|
90
|
-
// WHERE THE DESIGN SYSTEM ACTUALLY IS. `
|
|
90
|
+
// WHERE THE DESIGN SYSTEM ACTUALLY IS. `design/marketplace/` exists in this monorepo and is
|
|
91
91
|
// deliberately absent from a developer's project (sync-starter.sh): the platform installs
|
|
92
92
|
// @unoverse-platform/marketplace, whose `definitions/` bundle carries components, atoms
|
|
93
93
|
// AND styles (bundle-defs.mjs). All three matter — the space-scale check reads
|
|
94
94
|
// styles/base/spacing, so a fallback finding only components would build a PARTIAL scale
|
|
95
95
|
// and reject valid steps, reporting false errors on correct work. Mirrors definitions.ts.
|
|
96
96
|
const DS = (() => {
|
|
97
|
-
const onDisk = join(
|
|
97
|
+
const onDisk = join(DESIGN_ROOT, "marketplace");
|
|
98
98
|
if (existsSync(onDisk)) return onDisk;
|
|
99
|
-
const nodesHome = resolve(
|
|
99
|
+
const nodesHome = resolve(DESIGN_ROOT, "..", "nodes");
|
|
100
100
|
for (const c of [
|
|
101
101
|
join(nodesHome, "marketplace", "definitions"),
|
|
102
|
-
join(resolve(
|
|
102
|
+
join(resolve(DESIGN_ROOT, "..", "plugins"), "node_modules", "@unoverse-platform", "marketplace", "definitions"),
|
|
103
103
|
])
|
|
104
104
|
if (existsSync(c)) return c;
|
|
105
|
-
return
|
|
105
|
+
return DESIGN_ROOT; // none anywhere: shared refs will not resolve, and the findings will say so
|
|
106
106
|
})();
|
|
107
|
-
const legacyOrgsDir = join(
|
|
107
|
+
const legacyOrgsDir = join(DESIGN_ROOT, "orgs");
|
|
108
108
|
const orgDirs = (() => {
|
|
109
109
|
if (existsSync(legacyOrgsDir))
|
|
110
110
|
return readdirSync(legacyOrgsDir)
|
|
@@ -112,16 +112,16 @@ const orgDirs = (() => {
|
|
|
112
112
|
.map((e) => join(legacyOrgsDir, e))
|
|
113
113
|
.filter((d) => statSync(d).isDirectory());
|
|
114
114
|
// THE LEGACY LAYOUT, where the design system IS the root: `components/` and `atoms/`
|
|
115
|
-
// sit directly in
|
|
115
|
+
// sit directly in design/, so listing children here would lint "components" as an org.
|
|
116
116
|
//
|
|
117
|
-
// NOT `DS ===
|
|
118
|
-
// guaranteed. DS falls back to
|
|
117
|
+
// NOT `DS === DESIGN_ROOT`, which was the same test until the design system stopped being
|
|
118
|
+
// guaranteed. DS falls back to DESIGN_ROOT when no design system is found ANYWHERE, which is the
|
|
119
119
|
// ordinary state of a developer's project — so that test read "this is the legacy
|
|
120
120
|
// layout" and returned no orgs, silently linting nothing at all.
|
|
121
|
-
if (existsSync(join(
|
|
122
|
-
return readdirSync(
|
|
121
|
+
if (existsSync(join(DESIGN_ROOT, "components")) || existsSync(join(DESIGN_ROOT, "atoms"))) return [];
|
|
122
|
+
return readdirSync(DESIGN_ROOT)
|
|
123
123
|
.filter((e) => !e.startsWith(".") && e !== "marketplace" && e !== "_schema")
|
|
124
|
-
.map((e) => join(
|
|
124
|
+
.map((e) => join(DESIGN_ROOT, e))
|
|
125
125
|
.filter((d) => statSync(d).isDirectory());
|
|
126
126
|
})();
|
|
127
127
|
|
|
@@ -449,7 +449,7 @@ const refResolves = (ref, file) => {
|
|
|
449
449
|
*
|
|
450
450
|
* Ref lookup above is case-insensitive on purpose, and the marketplace is not: an item is
|
|
451
451
|
* fetched as `items/<kind>/<key>.json` over HTTP, off a case-sensitive host. So a name that
|
|
452
|
-
* disagrees with itself resolves forever in `
|
|
452
|
+
* disagrees with itself resolves forever in `design/` and 404s the moment anyone installs it.
|
|
453
453
|
*
|
|
454
454
|
* That is not hypothetical. 2026-08-06: twelve atoms were unreachable from every universe
|
|
455
455
|
* because git held `Avatar.json` while the build wrote `avatar.json`. macOS is
|
|
@@ -457,7 +457,7 @@ const refResolves = (ref, file) => {
|
|
|
457
457
|
* served what git held. The error surfaced as `could not fetch atom/avatar (HTTP 404)` in
|
|
458
458
|
* the Installed view, months after the cause.
|
|
459
459
|
*
|
|
460
|
-
* Nothing in `
|
|
460
|
+
* Nothing in `design/` could have caught it, because `design/` was correct. What was missing was a
|
|
461
461
|
* rule that the key agrees with itself EXACTLY, which is what this checks:
|
|
462
462
|
*
|
|
463
463
|
* filename === `name:` === every Ref that points at it
|
|
@@ -494,7 +494,7 @@ const canonicalRef = (ref) => canonicalKeys.get(ref.toLowerCase()) ?? null;
|
|
|
494
494
|
* That silence has cost real screens. `form-toggle` declares `on` and `description`; a
|
|
495
495
|
* form wrote `props: { value: … }` and `with: { help: … }`, so the switch bound to nothing
|
|
496
496
|
* and the sub-line simply never appeared. Everything rendered, nothing was wrong to look
|
|
497
|
-
* at, and the control did not move. Nothing in
|
|
497
|
+
* at, and the control did not move. Nothing in design/ could catch it because every node was
|
|
498
498
|
* structurally valid.
|
|
499
499
|
*
|
|
500
500
|
* So the keys are checked against the atom's own `props` block. Read lazily and cached:
|
|
@@ -591,7 +591,7 @@ for (const orgDir of orgDirs) {
|
|
|
591
591
|
const name = (statSync(p).isDirectory() ? e : isDefFile(e) ? defName(e) : null)?.toLowerCase();
|
|
592
592
|
if (!name) continue;
|
|
593
593
|
if (out.has(name))
|
|
594
|
-
report("error", p, `component name "${name}" already exists at ${relative(
|
|
594
|
+
report("error", p, `component name "${name}" already exists at ${relative(DESIGN_ROOT, out.get(name))}. Names are UNIQUE within a home; rename one`);
|
|
595
595
|
else out.set(name, p);
|
|
596
596
|
}
|
|
597
597
|
return out;
|
|
@@ -601,7 +601,7 @@ for (const orgDir of orgDirs) {
|
|
|
601
601
|
for (const [name, p] of namesIn(join(orgDir, "components"))) {
|
|
602
602
|
const shadowed = marketplaceNames.get(name);
|
|
603
603
|
if (shadowed)
|
|
604
|
-
report("error", p, `component name "${name}" shadows the marketplace component at ${relative(
|
|
604
|
+
report("error", p, `component name "${name}" shadows the marketplace component at ${relative(DESIGN_ROOT, shadowed)}. An org may never shadow a marketplace name (bare refs must stay unambiguous); rename the org component`);
|
|
605
605
|
}
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -635,7 +635,7 @@ for (const orgDir of orgDirs) {
|
|
|
635
635
|
// A component may run server-side code at a platform fire point, and the safety of that
|
|
636
636
|
// rests on the manifest and the thing that runs agreeing. Both halves are checked here so
|
|
637
637
|
// a bad declaration is caught in the terminal and at publish, not by silence at run time.
|
|
638
|
-
// Mirrors server/tests/
|
|
638
|
+
// Mirrors apps/unoverse/server/tests/design/lifecycle-declaration.test.ts (UNOVERSE_AUTHORING.md §3c).
|
|
639
639
|
const KNOWN_LIFECYCLES = new Set(["onStart", "onEnterView"]);
|
|
640
640
|
const PHASES_WITH_LAYOUTS = new Set(["onEnterView"]); // phases that fire per VIEW
|
|
641
641
|
const PLATFORM_HANDLERS = new Set(["getDetail"]); // named handlers needing no file
|
|
@@ -648,7 +648,7 @@ const LATCH_KEYS = new Set(["title", "background", "color"]);
|
|
|
648
648
|
// silently disable every rule after it.
|
|
649
649
|
const credentialDefs = new Set();
|
|
650
650
|
{
|
|
651
|
-
const nodesHome = resolve(
|
|
651
|
+
const nodesHome = resolve(DESIGN_ROOT, "..", "nodes");
|
|
652
652
|
try {
|
|
653
653
|
for (const pkg of readdirSync(nodesHome)) {
|
|
654
654
|
try {
|
|
@@ -658,7 +658,7 @@ const credentialDefs = new Set();
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
} catch {
|
|
661
|
-
/* no nodes tree beside
|
|
661
|
+
/* no nodes tree beside design/: the rule cannot judge, so it stays quiet */
|
|
662
662
|
}
|
|
663
663
|
}
|
|
664
664
|
|
|
@@ -847,8 +847,8 @@ function appSizesForFile(file) {
|
|
|
847
847
|
return appSizesCache.get(home);
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
-
// The universal component names (
|
|
851
|
-
//
|
|
850
|
+
// The universal component names (design/marketplace/components/*, case-insensitive) — for
|
|
851
|
+
// validating an app manifest's `preview` map. null = the file is not under a design tree.
|
|
852
852
|
const componentNamesCache = new Map();
|
|
853
853
|
// Components an ORG's template may reference: the marketplace tier + that org's OWN
|
|
854
854
|
// components — never another org's (org-privacy). Cached per org.
|
|
@@ -873,7 +873,7 @@ function componentNamesForFile(file) {
|
|
|
873
873
|
// The run context: everything the extracted rule modules close over. Built here so it
|
|
874
874
|
// cannot outlive the run, and passed once rather than threaded as ten parameters.
|
|
875
875
|
const ctx = {
|
|
876
|
-
|
|
876
|
+
DESIGN_ROOT, DS, orgDirs, report, spaceSteps, stepList, checkDimension, checkToken, checkCondition,
|
|
877
877
|
appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists,
|
|
878
878
|
isFixture, isHook, isManifest, isAppPath, defRoot, readText,
|
|
879
879
|
};
|
|
@@ -881,7 +881,7 @@ function componentNamesForFile(file) {
|
|
|
881
881
|
const lintFile = makeLintFile({ ...ctx, walkNode });
|
|
882
882
|
|
|
883
883
|
for (const home of homes) for (const f of jsonFiles(home.dir)) lintFile(f);
|
|
884
|
-
return { problems, homes, designRoot:
|
|
884
|
+
return { problems, homes, designRoot: DESIGN_ROOT, designSystem: DS };
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
/** True when anything would fail a build. Warnings and hints inform, errors stop. */
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the marketplace foundation's `base` + `semantic` first, then the org's ON TOP, per token
|
|
19
19
|
* — an org omits a token to inherit it. Themes are UNIONED rather than resolved one at a
|
|
20
20
|
* time, because a name that resolves under `dark` is legitimate work; per-theme parity is
|
|
21
|
-
* a different rule with its own guard (server/tests/
|
|
21
|
+
* a different rule with its own guard (apps/unoverse/server/tests/design/theme-contract.test.ts).
|
|
22
22
|
*
|
|
23
23
|
* UNREADABLE IS NOT EMPTY. A developer's project has no design system on disk — it is
|
|
24
24
|
* installed, not authored — so the foundation half cannot be read and only the org's own
|
|
@@ -88,7 +88,7 @@ function buildTokens(dsStyles, homeStyles) {
|
|
|
88
88
|
*
|
|
89
89
|
* - across a home's own themes, because a name only `dark` defines is real work, not a
|
|
90
90
|
* typo (whether every theme defines every name is a DIFFERENT rule, guarded by
|
|
91
|
-
* server/tests/
|
|
91
|
+
* apps/unoverse/server/tests/design/theme-contract.test.ts — this one must not double as a weaker copy);
|
|
92
92
|
* - the foundation's underneath, because the colour contract is the shared one every
|
|
93
93
|
* org is written against. An org that has no themes folder of its own would otherwise
|
|
94
94
|
* read as "no colours exist", and the rule would report every colour it uses.
|
|
@@ -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 apps/unoverse/design/_schema/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,12 +10,62 @@
|
|
|
10
10
|
* without extending the schema and the SDK in the same change.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// ── ground truth (mirrors
|
|
13
|
+
// ── ground truth (mirrors design/_schema/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",
|
|
17
17
|
]);
|
|
18
18
|
export const CONDITION_KEYS = new Set(["field", "eq", "ne", "in"]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* WHAT EACH PRIMITIVE IS (UNOVERSE_CONFORMANCE §2a, UNOVERSE_PRIMITIVE_CONTRACT.md).
|
|
22
|
+
*
|
|
23
|
+
* Until 2026-08-23 nothing stated this. Every node shared one bag of 36 optional properties,
|
|
24
|
+
* so `{ type: "Image", bind: { value: photo } }` saved, linted, published and drew nothing —
|
|
25
|
+
* the silent class behind "the card streams its title but keeps its mock image".
|
|
26
|
+
*
|
|
27
|
+
* Merged from three sources: `core/types.ts` JSDoc (25 of 32 properties already named their
|
|
28
|
+
* owner in prose, where no tool could read it), the renderer (the only authority on which
|
|
29
|
+
* `bind` keys are consumed), and what shipped definitions actually use — types.ts was
|
|
30
|
+
* missing nine properties in live use, because YAML never meets a TypeScript interface.
|
|
31
|
+
*
|
|
32
|
+
* `packages/sdk/conformance/tools/build-primitive-schema.mjs` reads THIS to emit the JSON
|
|
33
|
+
* Schema halves, so the editor squiggle and the publish gate cannot disagree.
|
|
34
|
+
*/
|
|
35
|
+
export const UNIVERSAL_PROPS = new Set([
|
|
36
|
+
// read for every node before dispatch; `brief` rides the tree for the composer
|
|
37
|
+
"type", "style", "visibleWhen", "bind", "action", "analytics", "skeleton", "brief", "$include",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export const PRIMITIVE_PROPS = {
|
|
41
|
+
Box: ["children", "autoScroll", "revealOnStick", "selection", "appWidth"],
|
|
42
|
+
Stack: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
43
|
+
Row: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
44
|
+
Column: ["children", "autoScroll", "revealOnStick", "appWidth"],
|
|
45
|
+
Text: ["value"],
|
|
46
|
+
Image: ["src", "alt"],
|
|
47
|
+
Markdown: ["value", "inheritLinkColor"],
|
|
48
|
+
Icon: ["icon", "name"],
|
|
49
|
+
Skeleton: ["variant"],
|
|
50
|
+
Orb: ["state", "size"],
|
|
51
|
+
Button: ["children", "value", "label", "icon", "disabledWhen"],
|
|
52
|
+
Input: ["placeholder", "inputType", "disabledWhen", "maxLength", "multiline", "rows",
|
|
53
|
+
"inputMode", "autoComplete", "advanceOnFill"],
|
|
54
|
+
Select: ["placeholder", "options", "disabledWhen"],
|
|
55
|
+
Each: ["template", "app", "items"],
|
|
56
|
+
Switch: ["on", "cases", "fallback"],
|
|
57
|
+
Ref: ["ref", "props", "with", "children", "name"],
|
|
58
|
+
ComponentSlot: ["select", "frame", "fallback", "pinned", "appWidth"],
|
|
59
|
+
Timeline: ["user", "assistant", "userData", "assistantData", "autoScroll"],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Which `bind` keys each primitive's view actually reads. Absent = the view reads the whole
|
|
63
|
+
* bind object (Orb), so any key is legitimate. */
|
|
64
|
+
export const PRIMITIVE_BINDS = {
|
|
65
|
+
Text: ["value"], Image: ["src", "alt"], Markdown: ["value"], Icon: ["name"],
|
|
66
|
+
Button: ["label"], Input: ["value", "placeholder"], Select: ["options", "value"],
|
|
67
|
+
Each: ["items"],
|
|
68
|
+
};
|
|
19
69
|
// the portable style vocabulary — every key the SDK interpreter maps (sdk/style.ts).
|
|
20
70
|
// Each is a neutral intent every native renderer (iOS/Android/RN/Flutter) implements;
|
|
21
71
|
// an unknown key is a typo or a web-ism that renders nowhere.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { existsSync, statSync, readdirSync, readFileSync } from "node:fs";
|
|
16
16
|
import { join, dirname, basename, relative, sep } from "node:path";
|
|
17
|
-
import { PRIMITIVES, CONDITION_KEYS, STYLE_KEYS, RAW_VALUE, CHILD_NODE_KEYS, PARTIAL_DIRS, DIMENSION_KEYS } from "./vocabulary.mjs";
|
|
17
|
+
import { PRIMITIVES, CONDITION_KEYS, STYLE_KEYS, RAW_VALUE, CHILD_NODE_KEYS, PARTIAL_DIRS, DIMENSION_KEYS, UNIVERSAL_PROPS, PRIMITIVE_PROPS, PRIMITIVE_BINDS } from "./vocabulary.mjs";
|
|
18
18
|
import { isDefFile, defName, defPath, readDef } from "./defs.mjs";
|
|
19
19
|
|
|
20
20
|
export function makeWalkNode(ctx) {
|
|
@@ -39,6 +39,26 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
39
39
|
else if (!PRIMITIVES.has(t))
|
|
40
40
|
report("error", file, `unknown primitive "${t}". The set is closed; compose, don't invent (docs.unoverse.ai/design/sdui-and-mcp-apps)`);
|
|
41
41
|
|
|
42
|
+
// WHAT THIS PRIMITIVE IS (UNOVERSE_CONFORMANCE §2a). Before this existed, every node
|
|
43
|
+
// shared one bag of 36 optional properties, so a wrong field name was accepted by the
|
|
44
|
+
// schema AND the lint and only showed up as a blank on screen. A wrong name is never a
|
|
45
|
+
// judgment call, so this is an error rather than a warning.
|
|
46
|
+
else if (PRIMITIVE_PROPS[t]) {
|
|
47
|
+
const own = PRIMITIVE_PROPS[t];
|
|
48
|
+
for (const k of Object.keys(node))
|
|
49
|
+
if (!UNIVERSAL_PROPS.has(k) && !own.includes(k))
|
|
50
|
+
report("error", file, `${t} has no "${k}". It reads ${own.length ? own.map((o) => `"${o}"` ).join(", ") : "no properties of its own"} (docs.unoverse.ai/reference/primitives)`);
|
|
51
|
+
|
|
52
|
+
// `bind` maps a target onto a data field, and only some targets are read. Binding a
|
|
53
|
+
// target the view never looks at is the silent failure this whole rule exists for:
|
|
54
|
+
// the field streams in correctly and the element keeps drawing its preview default.
|
|
55
|
+
const reads = PRIMITIVE_BINDS[t];
|
|
56
|
+
if (reads && node.bind && typeof node.bind === "object" && !Array.isArray(node.bind))
|
|
57
|
+
for (const k of Object.keys(node.bind))
|
|
58
|
+
if (!reads.includes(k))
|
|
59
|
+
report("error", file, `${t} never reads "bind.${k}", so the value would arrive and render nothing. It reads ${reads.map((r) => `"bind.${r}"`).join(", ")} (docs.unoverse.ai/reference/primitives)`);
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
if (t === "Switch") {
|
|
43
63
|
if (typeof node.on !== "string" || !node.cases || typeof node.cases !== "object")
|
|
44
64
|
report("error", file, `Switch needs "on" (the discriminant field) + "cases" (docs.unoverse.ai/design/state)`);
|
|
@@ -64,7 +84,7 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
64
84
|
report("error", file, `Ref "${node.ref}". No matching atom, shared component, or own-org component (bare, or "<org>/<name>" for the file's OWN org only — org-privacy); lookup is case-insensitive by name`);
|
|
65
85
|
// RESOLVES IS NOT ENOUGH. Ref lookup ignores case; the marketplace fetches
|
|
66
86
|
// items/<kind>/<key>.json off a case-sensitive host. A Ref that differs only in case
|
|
67
|
-
// renders forever in
|
|
87
|
+
// renders forever in design/ and 404s on install (index.mjs, canonicalRef).
|
|
68
88
|
else if (canonicalRef) {
|
|
69
89
|
const exact = canonicalRef(node.ref);
|
|
70
90
|
if (exact && exact !== node.ref)
|