unoverse 0.1.188 → 0.1.190

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.188",
3
+ "version": "0.1.190",
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",
@@ -399,8 +399,16 @@ function lintFile(file) {
399
399
  // ONE REQUIREMENT SHAPE (2026-08-29): a kind declares its tree (`states:`) OR
400
400
  // its drawing (`root` — flat components/atoms). A tree component's root is
401
401
  // SYNTHESIZED by the compiler, never required.
402
+ // A BLOCK is a template that declares a DRAWING instead of a tree (2026-08-31): a flat
403
+ // file, no manifest, one `root:` — `pair`, `feature`, `single`, `rail`, `stack`. It has
404
+ // no states because it is not a screen: it is a shape a page imports by `Ref`, inlined
405
+ // before the page's sections compile. Requiring `states:` of it would be demanding a
406
+ // state machine from a fragment, which is the same mistake as demanding a manifest:
407
+ // both are what a PAGE has, and a block is not a page.
408
+ const isBlock = declared === "template" && json.states === undefined && json.root !== undefined;
402
409
  const envelopeRequired =
403
- declared === "app" || declared === "template" ? ["name", "states"]
410
+ isBlock ? ["name", "root"]
411
+ : declared === "app" || declared === "template" ? ["name", "states"]
404
412
  : declared === "component" ? ["name", ...(json.states !== undefined || json.state?.view !== undefined ? [] : ["root"])]
405
413
  : ["name", "root"];
406
414
  if (declared === "template" && json.states === undefined && (json.parts !== undefined || json.shapes !== undefined)) envelopeRequired.pop();
@@ -45,7 +45,7 @@ export const UNIVERSAL_PROPS = new Set([
45
45
  */
46
46
  export const PROP_KEYS = [
47
47
  "type", "input", "description", "maxLength", "minItems", "maxItems",
48
- "hydrate", "optional", "enum", "preview", "default", "items",
48
+ "hydrate", "override", "optional", "enum", "preview", "default", "items",
49
49
  ];
50
50
 
51
51
  /**
@@ -86,7 +86,7 @@ export const PRIMITIVE_PROPS = {
86
86
  Input: ["placeholder", "inputType", "disabledWhen", "maxLength", "multiline", "rows",
87
87
  "inputMode", "autoComplete", "advanceOnFill"],
88
88
  Select: ["placeholder", "options", "disabledWhen"],
89
- Each: ["template", "app", "items"],
89
+ Each: ["item", "app", "items"],
90
90
  Switch: ["on", "cases", "fallback"],
91
91
  Ref: ["ref", "props", "with", "children", "name"],
92
92
  ComponentSlot: ["select", "frame", "fallback", "pinned", "appWidth"],
@@ -121,7 +121,7 @@ export const STYLE_KEYS = new Set([
121
121
  // exact regex from server/src/runtime/definition-tokens.test.ts
122
122
  export const RAW_VALUE = /#[0-9a-fA-F]{3,8}\b|\b\d*\.?\d+(px|rem|em)\b/;
123
123
  // nodes reachable through these keys (data keys like `items`/`cases` handled explicitly)
124
- export const CHILD_NODE_KEYS = ["children", "template", "fallback", "user", "assistant"];
124
+ export const CHILD_NODE_KEYS = ["children", "item", "fallback", "user", "assistant"];
125
125
  // folders that hold BARE PARTIALS one level under the definition root
126
126
  export const PARTIAL_DIRS = new Set(["layouts", "states", "components", "blocks"]);
127
127
 
@@ -17,9 +17,15 @@ import { join, dirname, basename, relative, sep } from "node:path";
17
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
- // THE THREE WORDS and the director's closed block (UNOVERSE_TEMPLATE_MODEL.md §9b).
21
- const SECTION_WORDS = ["static", "copywriter", "director"];
20
+ // THE FOUR WORDS and the two closed blocks (UNOVERSE_TEMPLATE_MODEL.md §9b + §9e).
21
+ const SECTION_WORDS = ["static", "copywriter", "director", "query"];
22
22
  const DIRECTOR_KEYS = ["state", "limit", "rules", "preview"];
23
+ // §9e: a queried place says WHAT draws it, WHAT it asks, WHAT KIND may answer, HOW MANY.
24
+ const QUERY_KEYS = ["component", "ask", "type", "include", "pick"];
25
+ // The `object_type`s a place may admit. Apps and skills are absent by construction: the
26
+ // page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
27
+ // director woken), or every place would rain cards onto a page still assembling itself.
28
+ const QUERY_TYPES = ["services", "images", "needs", "actions"];
23
29
 
24
30
  export function makeWalkNode(ctx) {
25
31
  const { report, checkCondition, checkDimension, checkToken, appSizesForFile, componentNamesForFile, refResolves, canonicalRef, declaredProps, atomsDirExists, stepList, spaceSteps, isAppPath, isTemplatePath, defRoot } = ctx;
@@ -68,18 +74,86 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
68
74
  report("error", file, `director.preview names unknown component "${c}". No match in the design system or this org's components (lookup is case-insensitive)`);
69
75
  }
70
76
  }
71
- // ONE MIND, MANY BANDS9b rule 3, amended 2026-08-30): a layout may hold several
72
- // directed bands — one verdict, arranged — but each must claim a DISTINCT face, or
73
- // the surface is ambiguous (the reaction contract's one-view-one-surface rule). So
74
- // on a multi-band layout every director declares its `state:`; two bands claiming
75
- // the same face is the error this rule now exists for.
77
+ // A PLACE NEEDS NO NAME 9d, ruled 2026-08-31). A layout may hold several director
78
+ // places — one mind, arranged — and each needs a DISTINCT address or the surface is
79
+ // ambiguous (the reaction contract's one-view-one-surface rule). But the ADDRESS is
80
+ // the compiler's to hand out, by position, and an author writes places and a brief,
81
+ // never a face name. This rule used to demand `state:` on every place after the
82
+ // first; that was config in the layout, saying nothing the one mind did not already
83
+ // know from the page's brief.
84
+ //
85
+ // An AUTHORED `state:` still stands (the flat-email pin, §9b rule 4), so two places
86
+ // pinned to the SAME face remains an error — that one is genuinely ambiguous.
76
87
  const seen = directorsSeen.get(file) ?? [];
77
88
  const face = typeof d?.state === "string" ? d.state : null;
78
- if (seen.length && !face)
79
- report("error", file, `a second "director:" in this layout declares no "state:". Several bands are allowed — one mind, arranged — but each must claim a DISTINCT face, so every one of them names it (§9b rule 3)`);
80
- else if (face && seen.includes(face))
89
+ if (face && seen.includes(face))
81
90
  report("error", file, `two directed bands claim the face "${face}". A face has exactly one surface, or the active one is ambiguous (§9b rule 3, the reaction contract)`);
82
91
  directorsSeen.set(file, [...seen, face]);
92
+ } else if (word === "query") {
93
+ /**
94
+ * A QUERIED PLACE (§9e, RULED 2026-09-01) — lane 2 of §5b, spelled at last. It links a
95
+ * component like `static:`/`copywriter:` do, but as a BLOCK, because it also carries
96
+ * the question it puts to the search.
97
+ *
98
+ * The checks here are the ones that fail SILENTLY at runtime: a place with no ask
99
+ * simply never fills, and a place asking for a type nobody stores draws nothing. Both
100
+ * read as "the design is broken" on screen, with nothing in any log saying why.
101
+ */
102
+ const q = node.query;
103
+ if (!q || typeof q !== "object" || Array.isArray(q)) {
104
+ report("error", file, `"query:" is a block, not a path — it carries the component AND the question it asks (§9e)`);
105
+ } else {
106
+ for (const k of Object.keys(q))
107
+ if (!QUERY_KEYS.includes(k))
108
+ report("error", file, `query has no "${k}". It reads ${QUERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9e, the four words: the ask, which search answers it, what kind may answer, how many the director picks)`);
109
+ if (typeof q.component !== "string" || !q.component)
110
+ report("error", file, `query.component links the part by PATH (component: components/<part>) — that is what draws the rows it finds (§9e)`);
111
+ else if (!defPath(root, q.component) && !existsSync(join(root, q.component)))
112
+ report("error", file, `query.component "${q.component}" does not resolve under ${relative(process.cwd(), root)}/ (§9e)`);
113
+ if (typeof q.ask !== "string" || !q.ask.trim())
114
+ report("error", file, `query.ask is the QUESTION put to the search — without one the place never fills, and nothing says so (§9e)`);
115
+ // REQUIRED, both of them (2026-09-01). The engine supplies NO defaults: it has to
116
+ // fill a course page and a deal page as readily as a day, and a default `include`
117
+ // or `limit` would be one template's answer written into the platform. A place that
118
+ // does not say what it wants and how many is skipped at runtime, so the slip has to
119
+ // be caught here or it is silent.
120
+ if (q.include === undefined)
121
+ report("error", file, `query.include is REQUIRED — the engine has no default kind, because a default would be one template's answer written into the platform (§9e rule 6)`);
122
+ if (q.pick === undefined)
123
+ report("error", file, `query.pick is REQUIRED — how many the director puts on the page; the engine has no default count (§9e rule 9)`);
124
+ // A searching place says WHICH search answers it; a gallery place ([images] alone)
125
+ // never searches and omits it. Skipped silently at runtime otherwise (§9e).
126
+ const gallery = Array.isArray(q.include) && q.include.length > 0 && q.include.every((w) => String(w).toLowerCase() === "images");
127
+ if (!gallery && q.type !== "discovery" && q.type !== "intent")
128
+ report("error", file, `query.type is REQUIRED on a searching place — "discovery" (what lives around a topic) or "intent" (a named need). The author knows which their question is (§9e)`);
129
+ {
130
+ // NOT CHECKED HERE: "one place, one kind of moment, one plain question" (§9e).
131
+ // It is a real rule and it is not machine-readable. An "and" check was written and
132
+ // removed the same hour: it fired on "theme parks and attractions"
133
+ // and "bars and evening entertainment", which name ONE kind of thing twice for
134
+ // recall — indistinguishable by regex from "dinner and after-dark", which is two
135
+ // moments. A warning that fires on correct authoring teaches authors to ignore
136
+ // warnings, so the rule stays in the doc and in review rather than half-enforced.
137
+
138
+ // The template writes the half true of EVERY guest; who the guest is reaches the
139
+ // ranking through task affinity, not through the words. Measured 2026-09-01:
140
+ // a plain "restaurants" ask returned three matches, and the same question with
141
+ // ", for a family with teenagers" appended returned NONE — the clause pulls the
142
+ // embedding off the thing being asked for, under the precision floor.
143
+ if (/\b(for a|for my|with kids|with children|teenager|family of)\b/i.test(q.ask))
144
+ report("error", file, `query.ask "${q.ask}" names WHO it is for. An ask says what KIND of thing; who the guest is rides task affinity in the ranking, and appending it to the question drops every match under the precision floor (§9e rule 4)`);
145
+ }
146
+ if (q.include !== undefined) {
147
+ if (!Array.isArray(q.include) || q.include.some((t) => typeof t !== "string"))
148
+ report("error", file, `query.include is an array of result types — ${QUERY_TYPES.map((t) => `"${t}"`).join(", ")} (§9e rule 6)`);
149
+ else
150
+ for (const t of q.include)
151
+ if (!QUERY_TYPES.includes(String(t)))
152
+ report("error", file, `query.include has no "${t}". A place may admit ${QUERY_TYPES.map((x) => `"${x}"`).join(", ")}; apps and skills are never included, because the page's own searches are SILENT (§9e rule 6)`);
153
+ }
154
+ if (q.pick !== undefined && (typeof q.pick !== "number" || q.pick < 1))
155
+ report("error", file, `query.pick is a NUMBER — how many the director puts on the page, a cap the machine enforces (§9e rule 9)`);
156
+ }
83
157
  } else if (typeof node[word] !== "string") {
84
158
  report("error", file, `"${word}:" links a component by PATH (${word}: components/<part>) — that is what makes it a known part (§9b rule 1)`);
85
159
  } else {
@@ -94,7 +168,7 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
94
168
  // exists to abolish.
95
169
  if (node.select !== undefined)
96
170
  report("error", file, `a template part never authors "select" — the three words compile to the wire primitives at serve time (§9b rule 9)`);
97
- const { static: _s, copywriter: _c, director: _d, select: _sel, ...rest } = node;
171
+ const { static: _s, copywriter: _c, director: _d, query: _q, select: _sel, ...rest } = node;
98
172
  // An unpinned director claims the OWNING STATE's name (arrivals keep their own state
99
173
  // and the template's states react by name — the free grid↔page dance), so the face
100
174
  // comes from whichever state in the envelope declares this layout.
@@ -108,7 +182,12 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
108
182
  return typeof l === "string" && basename(l).toLowerCase() === here.toLowerCase();
109
183
  });
110
184
  };
111
- const face = word === "director" ? (node.director?.state ?? owningState()) : String(node[word]).split("/").pop();
185
+ // A QUERIED place links its component under `component:`, and claims that part's own
186
+ // face at limit 1 exactly as a linked part does — its `limit:` counts ROWS inside the
187
+ // part, not instances of it, so it never reaches the slot (§9e).
188
+ const linkedPath = word === "query" ? node.query?.component : node[word];
189
+ const face =
190
+ word === "director" ? (node.director?.state ?? owningState()) : typeof linkedPath === "string" ? linkedPath.split("/").pop() : undefined;
112
191
  const limit = word === "director" ? node.director?.limit : 1;
113
192
  const select = { from: "all", ...(face ? { where: { field: "state", eq: face } } : {}), ...(typeof limit === "number" ? { limit } : {}) };
114
193
  return walkNode({ type: "ComponentSlot", select, ...rest }, file, root, widthCap);
@@ -151,12 +230,12 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
151
230
  report("error", file, `Switch on "${node.on}" → case "${caseKey}" re-guards its own discriminant. A layer never guards itself; delete the visibleWhen (docs.unoverse.ai/design/components)`);
152
231
  }
153
232
  }
154
- // Each: a `template` + a list — EITHER a literal `items:[]` (hardcoded content,
233
+ // Each: an `item` + a list — EITHER a literal `items:[]` (hardcoded content,
155
234
  // the microapp default) OR `bind.items` (a workflow-fed array).
156
235
  if (t === "Each") {
157
236
  const hasList = Array.isArray(node.items) || (node.bind && typeof node.bind === "object" && node.bind.items);
158
- if (!node.template || !hasList)
159
- report("error", file, `Each needs "template" + a list. Literal "items": [...] or "bind": { "items": "<field>" } (docs.unoverse.ai/design/components)`);
237
+ if (!node.item || !hasList)
238
+ report("error", file, `Each needs "item" + a list. Literal "items": [...] or "bind": { "items": "<field>" } (docs.unoverse.ai/design/components)`);
160
239
  }
161
240
  if (t === "Ref") {
162
241
  if (typeof node.ref !== "string")