unoverse 0.1.189 → 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.189",
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",
@@ -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;
@@ -83,6 +89,71 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
83
89
  if (face && seen.includes(face))
84
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)`);
85
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
+ }
86
157
  } else if (typeof node[word] !== "string") {
87
158
  report("error", file, `"${word}:" links a component by PATH (${word}: components/<part>) — that is what makes it a known part (§9b rule 1)`);
88
159
  } else {
@@ -97,7 +168,7 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
97
168
  // exists to abolish.
98
169
  if (node.select !== undefined)
99
170
  report("error", file, `a template part never authors "select" — the three words compile to the wire primitives at serve time (§9b rule 9)`);
100
- 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;
101
172
  // An unpinned director claims the OWNING STATE's name (arrivals keep their own state
102
173
  // and the template's states react by name — the free grid↔page dance), so the face
103
174
  // comes from whichever state in the envelope declares this layout.
@@ -111,7 +182,12 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
111
182
  return typeof l === "string" && basename(l).toLowerCase() === here.toLowerCase();
112
183
  });
113
184
  };
114
- 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;
115
191
  const limit = word === "director" ? node.director?.limit : 1;
116
192
  const select = { from: "all", ...(face ? { where: { field: "state", eq: face } } : {}), ...(typeof limit === "number" ? { limit } : {}) };
117
193
  return walkNode({ type: "ComponentSlot", select, ...rest }, file, root, widthCap);