unoverse 0.1.166 → 0.1.168
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/package.json
CHANGED
|
@@ -112,6 +112,12 @@ function lintFile(file) {
|
|
|
112
112
|
// (layouts/<name>); every base substate needs its state file
|
|
113
113
|
// (states/<name>); a name cannot be both; an authored stateOrder is
|
|
114
114
|
// superseded and should be deleted.
|
|
115
|
+
// ONE DRAWINGS FOLDER (owner ruling 2026-08-22): templates work like
|
|
116
|
+
// components — every drawing lives in layouts/, the tree decides which state
|
|
117
|
+
// owns which. A states/ folder is the pre-unification anatomy: move its files
|
|
118
|
+
// into layouts/ and update $include paths.
|
|
119
|
+
if (existsSync(join(root, "states")))
|
|
120
|
+
report("error", file, `template carries a states/ folder — templates have ONE drawings folder. Move states/* into layouts/ and update every "$include: states/<name>" to "layouts/<name>" (anatomy unified with components, LAYERS §3)`);
|
|
115
121
|
if (json.states !== undefined) {
|
|
116
122
|
if (!json.states || typeof json.states !== "object" || Array.isArray(json.states)) {
|
|
117
123
|
report("error", file, `manifest "states" must be an object — the template tree: { <base>: { states: {…} }, <reaction>: {}, … } (STATE_MODEL §5)`);
|
|
@@ -124,19 +130,43 @@ function lintFile(file) {
|
|
|
124
130
|
for (const [n, s] of Object.entries(json.states)) {
|
|
125
131
|
if (s !== null && (typeof s !== "object" || Array.isArray(s)))
|
|
126
132
|
report("error", file, `tree state "${n}" must be an object ({} is a complete state) (STATE_MODEL §5)`);
|
|
127
|
-
if (n !== base && !defPath(join(root, "layouts"), n)
|
|
128
|
-
report("error", file, `tree state "${n}" has no layouts/${n}
|
|
133
|
+
if (n !== base && !defPath(join(root, "layouts"), n))
|
|
134
|
+
report("error", file, `tree state "${n}" has no layouts/${n}. Every state's drawing — a full arrangement or a stacked overlay — lives in layouts/, the template's ONE drawings folder (anatomy unified with components, LAYERS §3) (STATE_MODEL §5)`);
|
|
129
135
|
const nested = s && typeof s === "object" ? s.states : undefined;
|
|
130
136
|
if (nested && typeof nested === "object" && !Array.isArray(nested))
|
|
131
137
|
for (const sub of Object.keys(nested)) {
|
|
132
138
|
subs.push(sub);
|
|
133
|
-
if (!defPath(join(root, "
|
|
134
|
-
report("error", file, `substate "${sub}" (under "${n}") has no
|
|
139
|
+
if (!defPath(join(root, "layouts"), `${n}-${sub}`))
|
|
140
|
+
report("error", file, `substate "${sub}" (under "${n}") has no layouts/${n}-${sub} file — a substate's drawing is OWNER-PREFIXED in layouts/ (the step-* convention: bare names are the app's screens, <owner>-<sub> their sub-drawings) (LAYERS §3)`);
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
for (const sub of subs)
|
|
138
144
|
if (names.includes(sub))
|
|
139
145
|
report("error", file, `"${sub}" is declared both as a top-level state and a substate — nesting IS containment; a name lives at exactly one level (STATE_MODEL §5)`);
|
|
146
|
+
// THE FOLDER CANNOT LIE (owner ruling 2026-08-22): every layouts/ file is
|
|
147
|
+
// either a SCREEN (a bare name declaring a top-level state) or a substate
|
|
148
|
+
// drawing (<owner>-<sub>, both declared). Anything else is an error — a
|
|
149
|
+
// shared part belongs in components/.
|
|
150
|
+
{
|
|
151
|
+
const ldir = join(root, "layouts");
|
|
152
|
+
if (existsSync(ldir)) {
|
|
153
|
+
const declaredSubs = new Map();
|
|
154
|
+
for (const [n2, s2] of Object.entries(json.states)) {
|
|
155
|
+
const nested2 = s2 && typeof s2 === "object" ? s2.states : undefined;
|
|
156
|
+
if (nested2 && typeof nested2 === "object" && !Array.isArray(nested2))
|
|
157
|
+
for (const sub2 of Object.keys(nested2)) declaredSubs.set(`${n2}-${sub2}`, n2);
|
|
158
|
+
}
|
|
159
|
+
for (const f of readdirSync(ldir).filter(isDefFile)) {
|
|
160
|
+
const nm = defName(f);
|
|
161
|
+
if (names.includes(nm) || declaredSubs.has(nm)) continue;
|
|
162
|
+
const owner = names.find((n2) => nm.startsWith(`${n2}-`));
|
|
163
|
+
if (owner)
|
|
164
|
+
report("error", file, `layouts/${nm} is prefixed for state "${owner}" but "${nm.slice(owner.length + 1)}" is not declared as its substate — declare it in the tree, or move the file to components/ if it is a shared part (LAYERS §3)`);
|
|
165
|
+
else
|
|
166
|
+
report("error", file, `layouts/${nm} is neither a declared state nor an <owner>-<sub> substate drawing — bare names are the app's screens; declare it, prefix it with its owner, or move it to components/ (LAYERS §3)`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
140
170
|
if (json.stateOrder !== undefined)
|
|
141
171
|
report("warn", file, `"stateOrder" is superseded by the "states" tree (the ladder derives from the top level minus the base) — delete it (STATE_MODEL §5)`);
|
|
142
172
|
}
|
|
@@ -187,6 +217,14 @@ function lintFile(file) {
|
|
|
187
217
|
: [];
|
|
188
218
|
};
|
|
189
219
|
const states = new Set([...viewsIn("states"), ...viewsIn("layouts")]);
|
|
220
|
+
// ANATOMY UNIFIED (2026-08-22): a substate's drawing lives in layouts/ under
|
|
221
|
+
// `<arrangement>-<substate>`, so the STATE's own name is no longer a filename.
|
|
222
|
+
// The resolver already looks both ways (definitions.ts, template tree); without
|
|
223
|
+
// the same lookup here a correctly-migrated template lints as though every one
|
|
224
|
+
// of its substates had vanished.
|
|
225
|
+
const declared = json.states && typeof json.states === "object" && !Array.isArray(json.states) ? json.states : {};
|
|
226
|
+
for (const [parent, node] of Object.entries(declared))
|
|
227
|
+
for (const sub of Object.keys(node?.states ?? {})) if (states.has(`${parent}-${sub}`)) states.add(sub);
|
|
190
228
|
const comps = componentNamesForFile(file);
|
|
191
229
|
for (const [state, list] of Object.entries(json.preview)) {
|
|
192
230
|
if (!states.has(state))
|
|
@@ -270,11 +308,16 @@ function lintFile(file) {
|
|
|
270
308
|
|
|
271
309
|
// only components that ADOPTED the structure are held to the full discipline
|
|
272
310
|
if (hasLayouts || stateFiles.length || hasStateBlock) {
|
|
273
|
-
|
|
274
|
-
|
|
311
|
+
// Every prop declares what fills it. Was "must be input:true", from when props
|
|
312
|
+
// only held workflow data; shared components added props nothing streams in (the
|
|
313
|
+
// preview axis, literals passed via `Ref with`) — input:true there is false AND
|
|
314
|
+
// load-bearing, since configSchema derives from it. Silence is the error:
|
|
315
|
+
// unflagged props default to INPUTS.
|
|
316
|
+
const undeclared = Object.entries(json.props ?? {})
|
|
317
|
+
.filter(([, v]) => !(v && typeof v === "object" && typeof v.input === "boolean"))
|
|
275
318
|
.map(([k]) => k);
|
|
276
|
-
if (
|
|
277
|
-
report("error", file, `microapp props [${
|
|
319
|
+
if (undeclared.length)
|
|
320
|
+
report("error", file, `microapp props [${undeclared.join(", ")}] do not declare "input". Every prop says what fills it: input:true = a workflow streams it (it joins the node's configSchema); input:false = nothing does (a preview axis, or a literal the host passes via Ref with). Unflagged props default to INPUTS (docs/design/03)`);
|
|
278
321
|
|
|
279
322
|
// STATE MODEL v2 (UNOVERSE_STATE_MODEL §5): an authored `state.view` TREE is
|
|
280
323
|
// the component's state machine — the ONE object the scalar rule admits.
|
|
@@ -407,8 +407,42 @@ const dsComponentNames = new Set(
|
|
|
407
407
|
.filter((e) => !e.name.startsWith("."))
|
|
408
408
|
.map((e) => (e.isDirectory() ? e.name : defName(e.name)).toLowerCase()),
|
|
409
409
|
);
|
|
410
|
-
//
|
|
411
|
-
|
|
410
|
+
// The ORG tier's component names, per org (docs/unoverse/UNOVERSE_COMPONENT_ORGS.md):
|
|
411
|
+
// a Ref may also resolve an org component — bare from inside that org's own tree (the
|
|
412
|
+
// resolver's context rule), or org-qualified as `<org>/<name>` when two orgs share the
|
|
413
|
+
// name. Org-privacy holds: a file only ever reaches its OWN org's components, and a
|
|
414
|
+
// design-system file reaches none.
|
|
415
|
+
const orgComponentNames = new Map(); // org dir basename -> Set(lower names)
|
|
416
|
+
for (const orgDir of orgDirs) {
|
|
417
|
+
const home = join(orgDir, "components");
|
|
418
|
+
const set = new Set();
|
|
419
|
+
if (existsSync(home))
|
|
420
|
+
for (const e of readdirSync(home)) {
|
|
421
|
+
if (e.startsWith(".")) continue;
|
|
422
|
+
const name = statSync(join(home, e)).isDirectory() ? e : isDefFile(e) ? defName(e) : null;
|
|
423
|
+
if (name) set.add(name.toLowerCase());
|
|
424
|
+
}
|
|
425
|
+
orgComponentNames.set(basename(orgDir), set);
|
|
426
|
+
}
|
|
427
|
+
const orgOfFile = (file) => {
|
|
428
|
+
const home = orgDirs.find((d) => file.startsWith(d + sep));
|
|
429
|
+
return home ? basename(home) : null;
|
|
430
|
+
};
|
|
431
|
+
// A valid Ref target if there's ANY resolvable home the FILE may reach: atoms, DS
|
|
432
|
+
// components, or (from inside an org tree) that org's own components — bare or
|
|
433
|
+
// `<org>/<name>`-qualified.
|
|
434
|
+
const refResolves = (ref, file) => {
|
|
435
|
+
const lower = ref.toLowerCase();
|
|
436
|
+
const fileOrg = file ? orgOfFile(file) : null;
|
|
437
|
+
const slash = lower.indexOf("/");
|
|
438
|
+
if (slash > 0) {
|
|
439
|
+
const org = lower.slice(0, slash);
|
|
440
|
+
if (org !== fileOrg) return false; // another org's, or a DS file naming any org
|
|
441
|
+
return orgComponentNames.get(org)?.has(lower.slice(slash + 1)) ?? false;
|
|
442
|
+
}
|
|
443
|
+
if (atomNames.has(lower) || dsComponentNames.has(lower)) return true;
|
|
444
|
+
return fileOrg ? (orgComponentNames.get(fileOrg)?.has(lower) ?? false) : false;
|
|
445
|
+
};
|
|
412
446
|
|
|
413
447
|
/**
|
|
414
448
|
* THE KEY IS ONE STRING, CASE AND ALL.
|
|
@@ -60,8 +60,8 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
60
60
|
if (t === "Ref") {
|
|
61
61
|
if (typeof node.ref !== "string")
|
|
62
62
|
report("error", file, `Ref needs "ref": "<atom name>" (docs/design/03)`);
|
|
63
|
-
else if (atomsDirExists && !refResolves(node.ref))
|
|
64
|
-
report("error", file, `Ref "${node.ref}". No matching atom
|
|
63
|
+
else if (atomsDirExists && !refResolves(node.ref, file))
|
|
64
|
+
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
65
|
// RESOLVES IS NOT ENOUGH. Ref lookup ignores case; the marketplace fetches
|
|
66
66
|
// items/<kind>/<key>.json off a case-sensitive host. A Ref that differs only in case
|
|
67
67
|
// renders forever in rx/ and 404s on install (index.mjs, canonicalRef).
|
|
@@ -199,32 +199,17 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
|
|
|
199
199
|
// key anymore — a component writes only its own slice; templates react via select.where.
|
|
200
200
|
// (Legit panel/draft setTemplateValue writes a DIFFERENT key and is untouched.)
|
|
201
201
|
//
|
|
202
|
-
// The PLATFORM-DEFINED chrome keys a shared partial may write (see the exception below).
|
|
203
|
-
// Add to this only for state the SDK itself publishes and the design system must be able
|
|
204
|
-
// to answer — never for an app's own vocabulary, which is what template-local partials
|
|
205
|
-
// are for.
|
|
206
|
-
const SHARED_CHROME_KEYS = new Set(["latchDismissed"]);
|
|
207
202
|
const scanAction = (a) => {
|
|
208
203
|
if (!a || typeof a !== "object") return;
|
|
209
204
|
if (a.type === "setTemplateValue") {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// universal and belongs in the design system rather than copied into every app's
|
|
219
|
-
// folder: the composer's latch pill is one bar, shared, and its ✕ is the guest's only
|
|
220
|
-
// exit from the latch (docs/MCP_COMPLETE_GUIDE.md §The Component Latch). The list is
|
|
221
|
-
// an ALLOWLIST on purpose — it grants exactly the keys the platform defines and stays
|
|
222
|
-
// an error for everything else, so this cannot widen into "shared components may
|
|
223
|
-
// write template state".
|
|
224
|
-
if (!isTemplatePath(file) && !(Array.isArray(a.values) && a.values.length && a.values.every((v) => v && SHARED_CHROME_KEYS.has(v.key))))
|
|
225
|
-
report("error", file, `a component never writes template state: setTemplateValue is the deprecated bridge; change the view with setValue and let the template react via select.where (STATE_MODEL §5b)`);
|
|
226
|
-
else if (Array.isArray(a.values) && a.values.some((v) => v && v.key === "defaultState"))
|
|
227
|
-
report("warn", file, `setTemplateValue writing "defaultState" is the deprecated focus bridge. A component writes only its own slice; templates react via ComponentSlot.select.where (STATE_MODEL §5b)`);
|
|
205
|
+
// WHAT IS DEPRECATED IS THE FOCUS BRIDGE, not the action (2026-08-23, owner
|
|
206
|
+
// ruling). Writing `defaultState` to move the template is the bridge: a component
|
|
207
|
+
// reaching up to change which surface is shown, which the tree reacts to by name
|
|
208
|
+
// instead. Writing the template's own CHROME (openPanel, draft) is legitimate from
|
|
209
|
+
// anywhere — a `Ref`-embedded component has no slice of its own, so template state
|
|
210
|
+
// is the only state in scope, and the shared chat chrome depends on it.
|
|
211
|
+
if (Array.isArray(a.values) && a.values.some((v) => v && v.key === "defaultState"))
|
|
212
|
+
report(isTemplatePath(file) ? "warn" : "error", file, `setTemplateValue writing "defaultState" is the deprecated focus bridge. Change the view with setValue and let the template react via ComponentSlot.select.where (STATE_MODEL §5b)`);
|
|
228
213
|
}
|
|
229
214
|
if (a.then) scanAction(a.then);
|
|
230
215
|
};
|