unoverse 0.1.191 → 0.1.192

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.191",
3
+ "version": "0.1.192",
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",
@@ -454,16 +454,31 @@ for (const orgDir of orgDirs) {
454
454
  // resolver has always done this (componentDirs' fallback); the lint did not, so a part
455
455
  // composing a sibling part read as an unresolvable Ref while it rendered perfectly.
456
456
  const templatePartNames = new Map(); // template folder name -> Set(lower part names)
457
+ // BLOCKS (§6b-0, 2026-09-02): a flat def file directly under a templates home is a small
458
+ // reusable template — Ref'able bare from its own org (the loader's third fallback).
459
+ const orgTemplateNames = new Map(); // org (or DS basename) -> Set(lower block names)
457
460
  for (const root of [DS, ...orgDirs]) {
458
461
  const home = join(root, "templates");
459
462
  if (!existsSync(home)) continue;
463
+ const blocks = orgTemplateNames.get(basename(root)) ?? new Set();
460
464
  for (const t of readdirSync(home)) {
465
+ if (isDefFile(t)) {
466
+ blocks.add(defName(t).toLowerCase());
467
+ continue;
468
+ }
461
469
  const parts = join(home, t, "components");
462
470
  if (!existsSync(parts)) continue;
463
471
  const set = templatePartNames.get(t.toLowerCase()) ?? new Set();
464
- for (const e of readdirSync(parts)) if (isDefFile(e)) set.add(defName(e).toLowerCase());
472
+ for (const e of readdirSync(parts)) {
473
+ if (isDefFile(e)) set.add(defName(e).toLowerCase());
474
+ // FOLDER-FORM PARTS (moment/, chapter/): the part is <name>/<name>.yaml. The flat
475
+ // scan above missed them, so a part in earned structure read as unresolvable while
476
+ // it rendered perfectly.
477
+ else if (existsSync(join(parts, e, e + ".yaml"))) set.add(e.toLowerCase());
478
+ }
465
479
  templatePartNames.set(t.toLowerCase(), set);
466
480
  }
481
+ orgTemplateNames.set(basename(root), blocks);
467
482
  }
468
483
  const orgOfFile = (file) => {
469
484
  const home = orgDirs.find((d) => file.startsWith(d + sep));
@@ -485,7 +500,10 @@ const refResolves = (ref, file) => {
485
500
  return orgComponentNames.get(qualifier)?.has(name) ?? false;
486
501
  }
487
502
  if (atomNames.has(lower) || dsComponentNames.has(lower)) return true;
488
- return fileOrg ? (orgComponentNames.get(fileOrg)?.has(lower) ?? false) : false;
503
+ if (fileOrg && orgComponentNames.get(fileOrg)?.has(lower)) return true;
504
+ // A BLOCK — a flat template, own org first, then the design system's (loader order).
505
+ if (fileOrg && orgTemplateNames.get(fileOrg)?.has(lower)) return true;
506
+ return orgTemplateNames.get(basename(DS))?.has(lower) ?? false;
489
507
  };
490
508
 
491
509
  /**
@@ -18,10 +18,10 @@ import { PRIMITIVES, CONDITION_KEYS, STYLE_KEYS, RAW_VALUE, CHILD_NODE_KEYS, PAR
18
18
  import { isDefFile, defName, defPath, readDef } from "./defs.mjs";
19
19
 
20
20
  // THE FOUR WORDS and the two closed blocks (UNOVERSE_TEMPLATE_MODEL.md §9b + §9e).
21
- const SECTION_WORDS = ["static", "copywriter", "director", "query"];
22
- const DIRECTOR_KEYS = ["state", "limit", "rules", "preview"];
21
+ const SECTION_WORDS = ["static", "copywriter", "director"];
22
+ const DELIVERY_KEYS = ["state", "pick", "rules", "preview"];
23
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"];
24
+ const SEARCH_KEYS = ["component", "type", "include", "pick", "items"];
25
25
  // The `object_type`s a place may admit. Apps and skills are absent by construction: the
26
26
  // page's own searches are SILENT (no app unlocked, no tool minted, no card lane, no
27
27
  // director woken), or every place would rain cards onto a page still assembling itself.
@@ -53,16 +53,82 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
53
53
  if (word && node.type === undefined) {
54
54
  if (!isTemplatePath(file))
55
55
  report("error", file, `"${word}:" is template grammar — only a template layout's parts say how they are filled. Apps and components author ComponentSlot directly (UNOVERSE_TEMPLATE_MODEL §9b)`);
56
+ if (node.query !== undefined) {
57
+ report("error", file, `"query:" is retired vocabulary (§9e rule 10b REVERSED 2026-09-02) — spell the place director: with the same component/items/type/include/pick. One word, one judge`);
58
+ }
56
59
  if (word === "director") {
60
+ /**
61
+ * ONE WORD, TWO MODES (§9e, 2026-09-02): a director place WITH `component` is a
62
+ * SEARCH place (the page fetches its own content); WITHOUT one it is a DELIVERY
63
+ * band (the conversation fills it). The key sets are mutually exclusive, so a
64
+ * place is always unambiguously one or the other — behaviour never forks on a
65
+ * stray key. The QUESTION is never authored (rule 10d): it is derived from the
66
+ * writing, the page's pool, the component's contract, or the guest's ask.
67
+ */
57
68
  const d = node.director;
58
- if (!d || typeof d !== "object" || Array.isArray(d))
59
- report("error", file, `"director:" is a block, not a path — no component is ever named there: it is the one part whose content nobody knows at design time (§9b rule 1)`);
60
- else {
69
+ if (!d || typeof d !== "object" || Array.isArray(d)) {
70
+ report("error", file, `"director:" is a block, not a path (§9b rule 1)`);
71
+ } else if (d.limit !== undefined) {
72
+ report("error", file, `director.limit is retired — the one cap word is "pick": how many the director keeps (§9e rule 9, renamed 2026-09-02)`);
73
+ } else if (d.ask !== undefined || d.query !== undefined) {
74
+ report("error", file, `a director place never states its question — it is DERIVED (§9e rule 10d): the writer's headings (items), the page's pool (a gallery), the component's own contract, or the guest's ask`);
75
+ } else if (typeof d.component === "string" && d.component) {
76
+ // A SEARCH PLACE. The checks here are the ones that fail SILENTLY at runtime: a
77
+ // place missing include or pick is skipped, and a kind nobody stores draws
78
+ // nothing — both read as "the design is broken" with no log saying why.
79
+ //
80
+ // THE SEARCH WORDS SIT ON THE THING THAT SEARCHES (owner ruling 2026-09-02): on
81
+ // an items place each ITEM searches, so type/include/pick nest INSIDE items:
82
+ // beside min/max, and pick reads unambiguously as per-item. Spelled flat, the
83
+ // same word meant two scopes depending on whether items: was present.
84
+ const itemed = d.items && typeof d.items === "object" && !Array.isArray(d.items);
85
+ if (d.items !== undefined && !itemed)
86
+ report("error", file, `director.items is a block — items: { min, max, type, include, pick } (§9e rule 10c)`);
87
+ if (itemed && (d.type !== undefined || d.include !== undefined || d.pick !== undefined))
88
+ report("error", file, `an items place nests its search words INSIDE items: { min, max, type, include, pick } — each ITEM searches, so pick reads per item (§9e rule 10c, 2026-09-02)`);
89
+ const TOP_KEYS = itemed ? ["component", "items"] : SEARCH_KEYS;
61
90
  for (const k of Object.keys(d))
62
- if (!DIRECTOR_KEYS.includes(k))
63
- report("error", file, `director has no "${k}". It reads ${DIRECTOR_KEYS.map((x) => `"${x}"`).join(", ")} (§9b)`);
64
- if (d.limit !== undefined && typeof d.limit !== "number")
65
- report("error", file, `director.limit is a NUMBER a hard cap the machine enforces, never trusted to the model 9b)`);
91
+ if (!TOP_KEYS.includes(k))
92
+ report("error", file, `a search place has no "${k}". It reads ${TOP_KEYS.map((x) => `"${x}"`).join(", ")} (§9e; "state"/"rules" belong to a delivery band — a place with a component is governed by the page's brief)`);
93
+ if (!defPath(root, d.component) && !existsSync(join(root, d.component)))
94
+ report("error", file, `director.component "${d.component}" does not resolve under ${relative(process.cwd(), root)}/9e)`);
95
+ const ITEM_KEYS = ["min", "max", "type", "include", "pick"];
96
+ if (itemed) {
97
+ for (const k of Object.keys(d.items))
98
+ if (!ITEM_KEYS.includes(k))
99
+ report("error", file, `director.items has no "${k}". It reads ${ITEM_KEYS.map((x) => `"${x}"`).join(", ")} (§9e rule 10c)`);
100
+ const { min, max } = d.items;
101
+ if (typeof min !== "number" || typeof max !== "number" || min < 1 || max < min)
102
+ report("error", file, `director.items carries { min, max } — how many items the writer may shape, 1 <= min <= max (§9e rule 10c)`);
103
+ }
104
+ // The search words, read where they live: the place itself, or each item.
105
+ const s = itemed ? d.items : d;
106
+ const at = itemed ? "director.items" : "director";
107
+ // REQUIRED, both (2026-09-01, carried over). The engine supplies NO defaults: a
108
+ // default include or pick would be one template's answer written into the platform.
109
+ if (s.include === undefined)
110
+ report("error", file, `${at}.include is REQUIRED on a search place — what KIND may answer; the engine has no default kind (§9e rule 6)`);
111
+ else if (!Array.isArray(s.include) || s.include.some((t) => typeof t !== "string"))
112
+ report("error", file, `${at}.include is an array of result types — ${QUERY_TYPES.map((t) => `"${t}"`).join(", ")} (§9e rule 6)`);
113
+ else
114
+ for (const t of s.include)
115
+ if (!QUERY_TYPES.includes(String(t)))
116
+ report("error", file, `${at}.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)`);
117
+ if (s.pick === undefined)
118
+ report("error", file, `${at}.pick is REQUIRED — how many the director keeps${itemed ? " PER ITEM" : ""}; the engine has no default count (§9e rule 9)`);
119
+ else if (typeof s.pick !== "number" || s.pick < 1)
120
+ report("error", file, `${at}.pick is a NUMBER — how many the director keeps${itemed ? " per item" : ""}, a cap the machine enforces (§9e rule 9)`);
121
+ // `type` is OPTIONAL since the question derives (rule 10d): authored it stands;
122
+ // absent, a contract subject searches as intent and the guest's ask as discovery.
123
+ if (s.type !== undefined && s.type !== "discovery" && s.type !== "intent")
124
+ report("error", file, `${at}.type is "discovery" (what lives around a topic) or "intent" (a named need) — or omitted, and the derivation decides (§9e rule 10d)`);
125
+ } else {
126
+ // A DELIVERY BAND.
127
+ for (const k of Object.keys(d))
128
+ if (!DELIVERY_KEYS.includes(k))
129
+ report("error", file, `a delivery band has no "${k}". It reads ${DELIVERY_KEYS.map((x) => `"${x}"`).join(", ")} (§9b; a place that names a component is a search place)`);
130
+ if (d.pick !== undefined && (typeof d.pick !== "number" || d.pick < 1))
131
+ report("error", file, `director.pick is a NUMBER — how many the director keeps, a cap the machine enforces (§9e rule 9)`);
66
132
  if (d.rules !== undefined && typeof d.rules !== "string")
67
133
  report("error", file, `director.rules is written like a brief: one string of English (§9b)`);
68
134
  if (d.preview !== undefined && (!Array.isArray(d.preview) || d.preview.some((c) => typeof c !== "string")))
@@ -73,86 +139,14 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
73
139
  if (comps && !comps.has(String(c).toLowerCase()))
74
140
  report("error", file, `director.preview names unknown component "${c}". No match in the design system or this org's components (lookup is case-insensitive)`);
75
141
  }
76
- }
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.
87
- const seen = directorsSeen.get(file) ?? [];
88
- const face = typeof d?.state === "string" ? d.state : null;
89
- if (face && seen.includes(face))
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)`);
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)`);
142
+ // A PLACE NEEDS NO NAME (§9d): the compiler addresses delivery bands by
143
+ // position; an AUTHORED state: still stands (the flat-email pin), so two bands
144
+ // pinned to the SAME face remains the one genuine ambiguity.
145
+ const seen = directorsSeen.get(file) ?? [];
146
+ const face = typeof d?.state === "string" ? d.state : null;
147
+ if (face && seen.includes(face))
148
+ 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)`);
149
+ directorsSeen.set(file, [...seen, face]);
156
150
  }
157
151
  } else if (typeof node[word] !== "string") {
158
152
  report("error", file, `"${word}:" links a component by PATH (${word}: components/<part>) — that is what makes it a known part (§9b rule 1)`);