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
|
@@ -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))
|
|
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
|
-
|
|
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"
|
|
22
|
-
const
|
|
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
|
|
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
|
|
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 (!
|
|
63
|
-
report("error", file, `
|
|
64
|
-
if (d.
|
|
65
|
-
report("error", file, `director.
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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)`);
|