unoverse 0.1.170 → 0.1.172

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.170",
3
+ "version": "0.1.172",
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",
@@ -28,7 +28,7 @@ function checkStateOrder(order, rootFolder, file, includeLayouts = false) {
28
28
  ? readdirSync(d).filter(isDefFile).map(defName)
29
29
  : [];
30
30
  };
31
- // TEMPLATES: stateOrder lists LOCAL states + LAYOUTS in picker order (docs/design/05) —
31
+ // TEMPLATES: stateOrder lists LOCAL states + LAYOUTS in picker order (docs.unoverse.ai/design/apps) —
32
32
  // a layout name (the view a component enters) is a valid entry. COMPONENTS: states only.
33
33
  const stateNames = dirNames("states");
34
34
  const onDisk = new Set([...stateNames, ...(includeLayouts ? dirNames("layouts") : [])]);
@@ -39,7 +39,7 @@ function checkStateOrder(order, rootFolder, file, includeLayouts = false) {
39
39
  // legitimately omitted, so never warn on layouts.
40
40
  for (const name of stateNames)
41
41
  if (!order.includes(name))
42
- report("warn", file, `states/${name}.json is not in stateOrder. It falls to the end of the picker; add it to lock the order (docs/design/07)`);
42
+ report("warn", file, `states/${name}.json is not in stateOrder. It falls to the end of the picker; add it to lock the order (docs.unoverse.ai/design/studio)`);
43
43
  }
44
44
 
45
45
  // ── lint one file ──
@@ -49,7 +49,7 @@ function lintFile(file) {
49
49
  const src = readText(file);
50
50
 
51
51
  // LAW 1 — tokens only (skip manifest + fixture; styles/ is never in a def home).
52
- // Exempt: `appWidth` — the HOST-facing outer width (state-owned sizing, docs/design/05).
52
+ // Exempt: `appWidth` — the HOST-facing outer width (state-owned sizing, docs.unoverse.ai/design/apps).
53
53
  // It is raw CSS the embed host applies to the app panel ("min(50vw, 760px)", "360px"),
54
54
  // never a style the SDK resolves — token law governs the inside, not the envelope.
55
55
  if (!isFixture(file) && !isHook(file) && !isManifest(file))
@@ -74,7 +74,7 @@ function lintFile(file) {
74
74
  }
75
75
  }
76
76
  if (RAW_VALUE.test(code) && !/^\s*"?appWidth"?\s*:/.test(code))
77
- report("error", file, `raw value. Token names only; add/scale a token in the org styles instead (LAW 1, docs/design/06): ${code.trim()}`, i + 1);
77
+ report("error", file, `raw value. Token names only; add/scale a token in the org styles instead (LAW 1, docs.unoverse.ai/design/styles-and-tokens): ${code.trim()}`, i + 1);
78
78
  });
79
79
 
80
80
  let json;
@@ -93,9 +93,9 @@ function lintFile(file) {
93
93
  if (isAppPath(file)) {
94
94
  // TEMPLATE manifest = the envelope. Requires binding + a resolvable root.
95
95
  for (const req of ["name", "whenToUse"])
96
- if (!json[req]) report("warn", file, `template manifest missing "${req}": ${req === "whenToUse" ? "the AI selects the app by it" : "the display name"} (docs/design/05)`);
96
+ if (!json[req]) report("warn", file, `template manifest missing "${req}": ${req === "whenToUse" ? "the AI selects the app by it" : "the display name"} (docs.unoverse.ai/design/apps)`);
97
97
  if (!(json.binding && json.binding.workflow))
98
- report("warn", file, `template manifest has no binding.workflow. The app owns its workflow binding (docs/design/05)`);
98
+ report("warn", file, `template manifest has no binding.workflow. The app owns its workflow binding (docs.unoverse.ai/design/apps)`);
99
99
  // Two valid roots (definitions.ts:229): the STANDARD manifest-only form (root =
100
100
  // layouts/<layout>), OR a `<name>` envelope OVERRIDE (its own root). Only the
101
101
  // manifest-only form must resolve a layout; an envelope-form template supplies its own.
@@ -103,7 +103,7 @@ function lintFile(file) {
103
103
  if (!hasEnvelope) {
104
104
  const layoutName = json.layout ?? "main";
105
105
  if (!defPath(join(root, "layouts"), layoutName))
106
- report("error", file, `manifest.layout "${layoutName}" → layouts/${layoutName} does not exist (and no <name> envelope) (docs/design/05)`);
106
+ report("error", file, `manifest.layout "${layoutName}" → layouts/${layoutName} does not exist (and no <name> envelope) (docs.unoverse.ai/design/apps)`);
107
107
  }
108
108
  // THE TEMPLATE TREE (STATE_MODEL v2, checkpoint 2026-08-08): a manifest
109
109
  // `states:` block declares the whole machine — validate the DECLARATION:
@@ -171,7 +171,7 @@ function lintFile(file) {
171
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)`);
172
172
  }
173
173
  } else checkStateOrder(json.stateOrder, root, file, /* includeLayouts */ true);
174
- // ONE STATE AT A TIME (docs/design/04): the active state is derived from the
174
+ // ONE STATE AT A TIME (docs.unoverse.ai/design/state): the active state is derived from the
175
175
  // latest surfaced VIEW, so no two surfaces in one template may claim the same
176
176
  // view — the active surface would be ambiguous.
177
177
  {
@@ -182,7 +182,7 @@ function lintFile(file) {
182
182
  const w = n.type === "ComponentSlot" ? n.select?.where : null;
183
183
  if ((w?.field === "view" || w?.field === "defaultState") && typeof w.eq === "string") {
184
184
  if (claims.has(w.eq))
185
- report("error", file, `two reaction surfaces claim the view "${w.eq}" (${claims.get(w.eq)} and ${from}). A template is in ONE state at a time; each view has exactly one surface (docs/design/04)`);
185
+ report("error", file, `two reaction surfaces claim the view "${w.eq}" (${claims.get(w.eq)} and ${from}). A template is in ONE state at a time; each view has exactly one surface (docs.unoverse.ai/design/state)`);
186
186
  else claims.set(w.eq, from);
187
187
  }
188
188
  for (const v of Object.values(n)) if (v && typeof v === "object") collectClaims(v, from);
@@ -200,16 +200,16 @@ function lintFile(file) {
200
200
  }
201
201
  }
202
202
  if (json.mode !== undefined && json.defaultState === undefined)
203
- report("warn", file, `"mode" was renamed to "defaultState". Still read as a fallback, but rename it (docs/design/04)`);
203
+ report("warn", file, `"mode" was renamed to "defaultState". Still read as a fallback, but rename it (docs.unoverse.ai/design/state)`);
204
204
  // `preview` — the per-state MOCK map ({ "<state>": ["course-card", …] }): each key
205
205
  // must be a states/ file, each name a real component. A repeated name seeds
206
206
  // several instances (a card rail).
207
207
  if (json.preview !== undefined) {
208
208
  if (!json.preview || typeof json.preview !== "object" || Array.isArray(json.preview))
209
- report("error", file, `"preview" must be an object mapping state names to component-name arrays (docs/design/07)`);
209
+ report("error", file, `"preview" must be an object mapping state names to component-name arrays (docs.unoverse.ai/design/studio)`);
210
210
  else {
211
211
  // preview keys are per-LAYOUT (the component view Studio mocks) or a local state
212
- // (docs/design/05) — resolve against states/ ∪ layouts/, same as stateOrder.
212
+ // (docs.unoverse.ai/design/apps) — resolve against states/ ∪ layouts/, same as stateOrder.
213
213
  const viewsIn = (sub) => {
214
214
  const d = join(root, sub);
215
215
  return existsSync(d)
@@ -228,7 +228,7 @@ function lintFile(file) {
228
228
  const comps = componentNamesForFile(file);
229
229
  for (const [state, list] of Object.entries(json.preview)) {
230
230
  if (!states.has(state))
231
- report("error", file, `preview."${state}". No states/${state}.json or layouts/${state}.json in this template (docs/design/07)`);
231
+ report("error", file, `preview."${state}". No states/${state}.json or layouts/${state}.json in this template (docs.unoverse.ai/design/studio)`);
232
232
  // An OBJECT entry is authored TEMPLATE-STATE mock data (what the workflow
233
233
  // would have echoed — comments, a discriminant, an anchor), merged verbatim
234
234
  // when the state's pill is picked. Only the two shapes; anything else errors.
@@ -243,33 +243,33 @@ function lintFile(file) {
243
243
  }
244
244
  }
245
245
  }
246
- // Sizing is STATE-OWNED (docs/design/05): the layout root's `appWidth` is the core
246
+ // Sizing is STATE-OWNED (docs.unoverse.ai/design/apps): the layout root's `appWidth` is the core
247
247
  // surface's constant width; a panel slot's `appWidth` slides out on top. Manifest
248
248
  // width/focusWidth are DEAD — nothing reads them; there is no fallback.
249
249
  for (const dep of ["width", "focusWidth"])
250
250
  if (json[dep] !== undefined)
251
- report("error", file, `manifest "${dep}" is dead. Nothing reads it. Sizing is state-owned: \`appWidth\` on the layout root (constant core width) or on a panel (slide-out width) (docs/design/05)`);
251
+ report("error", file, `manifest "${dep}" is dead. Nothing reads it. Sizing is state-owned: \`appWidth\` on the layout root (constant core width) or on a panel (slide-out width) (docs.unoverse.ai/design/apps)`);
252
252
  } else {
253
253
  // COMPONENT manifest = OPTIONAL spatial discovery. No binding. Mirrors the
254
254
  // discovery-meta assertions in server/src/runtime/microapp-structure.test.ts.
255
255
  const desc = typeof json.description === "string" ? json.description.trim() : "";
256
256
  if (desc.length < 20)
257
- report("error", file, `discovery manifest.description missing/too short. One line (≥20 chars) saying what the component IS (docs/design/03a)`);
257
+ report("error", file, `discovery manifest.description missing/too short. One line (≥20 chars) saying what the component IS (docs.unoverse.ai/design/components)`);
258
258
  else if (desc.length > 120)
259
- report("error", file, `discovery manifest.description is ${desc.length} chars: it's the listing subtitle (≤120); move detail into whenToUse (docs/design/03a)`);
259
+ report("error", file, `discovery manifest.description is ${desc.length} chars: it's the listing subtitle (≤120); move detail into whenToUse (docs.unoverse.ai/design/components)`);
260
260
  const wtu = typeof json.whenToUse === "string" ? json.whenToUse.trim() : "";
261
261
  if (wtu.length < 20)
262
- report("error", file, `discovery manifest.whenToUse missing/too short. The utterance-shaped selection text findIntent ranks on (docs/design/03a)`);
262
+ report("error", file, `discovery manifest.whenToUse missing/too short. The utterance-shaped selection text findIntent ranks on (docs.unoverse.ai/design/components)`);
263
263
  else if (/\b(pick when|use (this|when)|when the user|the user (asks|wants|needs)|select (this|when))\b/i.test(wtu))
264
- report("error", file, `discovery manifest.whenToUse is selector-shaped. Write the words the USER would say, not instructions about the user (docs/design/03a)`);
264
+ report("error", file, `discovery manifest.whenToUse is selector-shaped. Write the words the USER would say, not instructions about the user (docs.unoverse.ai/design/components)`);
265
265
  if (json.binding)
266
- report("warn", file, `a component discovery manifest has no workflow. Drop "binding" (a component is streamed or node-hydrated) (docs/design/03a)`);
267
- // `lifetime` — OPTIONAL render lifetime (docs/design/04 §Two lifetimes). Closed set:
266
+ report("warn", file, `a component discovery manifest has no workflow. Drop "binding" (a component is streamed or node-hydrated) (docs.unoverse.ai/design/components)`);
267
+ // `lifetime` — OPTIONAL render lifetime (docs.unoverse.ai/design/state §Two lifetimes). Closed set:
268
268
  // "turn" (default — the universal new-turn reset) | "conversation" (durable
269
269
  // conversation-scoped surface: conversation-keyed instance, exempt from the
270
270
  // new-turn reset, retired only by replacement, self-close, or a template swap).
271
271
  if (json.lifetime !== undefined && json.lifetime !== "turn" && json.lifetime !== "conversation")
272
- report("error", file, `manifest "lifetime" must be "turn" (default) or "conversation". Got ${JSON.stringify(json.lifetime)} (docs/design/04 §Two lifetimes)`);
272
+ report("error", file, `manifest "lifetime" must be "turn" (default) or "conversation". Got ${JSON.stringify(json.lifetime)} (docs.unoverse.ai/design/state §Two lifetimes)`);
273
273
  }
274
274
  return;
275
275
  }
@@ -280,11 +280,11 @@ function lintFile(file) {
280
280
  if (isEnvelope) {
281
281
  // COMPONENT envelope (templates have no envelope — their manifest is it).
282
282
  for (const req of ["kind", "name", "root"])
283
- if (json[req] === undefined) report("error", file, `envelope missing "${req}" (docs/design/02)`);
283
+ if (json[req] === undefined) report("error", file, `envelope missing "${req}" (docs.unoverse.ai/design/sdui-and-mcp-apps)`);
284
284
  if (json.kind && !["component", "template", "atom"].includes(json.kind))
285
285
  report("error", file, `unknown kind "${json.kind}"`);
286
286
  if (json.kind === "component" && !json.category)
287
- report("warn", file, `component has no "category". Used to group it in the palette (docs/design/02)`);
287
+ report("warn", file, `component has no "category". Used to group it in the palette (docs.unoverse.ai/design/sdui-and-mcp-apps)`);
288
288
  if (json.root) walkNode(json.root, file, root);
289
289
 
290
290
  // ── the contained-microapp discipline (mirrors microapp-structure.test.ts) ──
@@ -300,7 +300,7 @@ function lintFile(file) {
300
300
  if (defPath(root, "manifest"))
301
301
  for (const k of ["description", "whenToUse"])
302
302
  if (json[k] !== undefined)
303
- report("error", file, `envelope duplicates manifest meta "${k}". The discovery manifest is the single home (docs/design/03a)`);
303
+ report("error", file, `envelope duplicates manifest meta "${k}". The discovery manifest is the single home (docs.unoverse.ai/design/components)`);
304
304
 
305
305
  // deprecated bridge: a top-level `defaultState` triggers the component node APP_DATA emit
306
306
  if (json.defaultState !== undefined)
@@ -317,7 +317,7 @@ function lintFile(file) {
317
317
  .filter(([, v]) => !(v && typeof v === "object" && typeof v.input === "boolean"))
318
318
  .map(([k]) => k);
319
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)`);
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.unoverse.ai/design/components)`);
321
321
 
322
322
  // STATE MODEL v2 (UNOVERSE_STATE_MODEL §5): an authored `state.view` TREE is
323
323
  // the component's state machine — the ONE object the scalar rule admits.
@@ -344,9 +344,9 @@ function lintFile(file) {
344
344
  for (const [k, v] of Object.entries(json.state)) {
345
345
  if (k === "view" && viewTree) continue;
346
346
  if (Array.isArray(v) || (v && typeof v === "object"))
347
- report("error", file, `state.${k} is an ${Array.isArray(v) ? "array" : "object"}. The state block is SCALAR view-state only (the one object allowed is a well-formed v2 \`state.view\` tree); workflow-fed data → props (input:true), static content → hardcode in the layout (docs/design/03)`);
347
+ report("error", file, `state.${k} is an ${Array.isArray(v) ? "array" : "object"}. The state block is SCALAR view-state only (the one object allowed is a well-formed v2 \`state.view\` tree); workflow-fed data → props (input:true), static content → hardcode in the layout (docs.unoverse.ai/design/components)`);
348
348
  else if (typeof v === "string" && /^https?:\/\//.test(v))
349
- report("error", file, `state.${k} is a URL. Content, not view-state; hardcode it in the layout (or props input:true if workflow-fed) (docs/design/03)`);
349
+ report("error", file, `state.${k} is a URL. Content, not view-state; hardcode it in the layout (or props input:true if workflow-fed) (docs.unoverse.ai/design/components)`);
350
350
  }
351
351
 
352
352
  if (hasLayouts) {
@@ -356,7 +356,7 @@ function lintFile(file) {
356
356
  if (!raw.includes('"on":"view"') || !/"\$include":"layouts\//.test(raw))
357
357
  report("error", file, `a v2 component (state.view tree) must root-Switch on "view" → $include layouts/<layout> (each state owns its layout; same-name by convention) (STATE_MODEL §5)`);
358
358
  } else if (!raw.includes('"on":"defaultState"') || !/"\$include":"layouts\//.test(raw))
359
- report("error", file, `a faced component's root must Switch on defaultState → $include layouts/<state> (legacy; or declare a v2 state.view tree and Switch on "view") (docs/design/03)`);
359
+ report("error", file, `a faced component's root must Switch on defaultState → $include layouts/<state> (legacy; or declare a v2 state.view tree and Switch on "view") (docs.unoverse.ai/design/components)`);
360
360
 
361
361
  // ── face set ⇄ layouts/ cross-check (OPEN name set — inline/focused/<any>) ──
362
362
  // The FACES are the root Switch's cases; Studio's face toggle and the render
@@ -375,7 +375,7 @@ function lintFile(file) {
375
375
  const caseNames = Object.keys(cases).filter((k) => k !== "default");
376
376
  // inline is the UNIVERSAL default face: an unknown/absent defaultState must
377
377
  // render SOMETHING — require an `inline` case or an explicit `default`.
378
- // EXCEPTION — a SURFACE-ONLY component (docs/design/03): its manifest
378
+ // EXCEPTION — a SURFACE-ONLY component (docs.unoverse.ai/design/components): its manifest
379
379
  // declares a SURFACED arrival (defaultState naming one of its cases, not
380
380
  // inline), and it deliberately renders NOTHING while unsurfaced (e.g. a
381
381
  // rail card retired by a new turn). Then omitting inline/default is the
@@ -409,7 +409,7 @@ function lintFile(file) {
409
409
  if (name !== "default" && layoutName !== expected)
410
410
  report("error", file, viewTree
411
411
  ? `state "${name}" declares layout "${expected}" but its case includes layouts/${layoutName} — the tree's declaration and the case must agree (STATE_MODEL §5 rule 1)`
412
- : `face case "${name}" includes layouts/${layoutName}. The layout FILENAME is the state name; rename one so they match (docs/design/03)`);
412
+ : `face case "${name}" includes layouts/${layoutName}. The layout FILENAME is the state name; rename one so they match (docs.unoverse.ai/design/components)`);
413
413
  }
414
414
  // v2: tree-declared layouts (including nested substates' and variants') are
415
415
  // reachable by declaration — seed them so the orphan check knows them.
@@ -457,7 +457,7 @@ function lintFile(file) {
457
457
  for (const lf of readdirSync(layoutsDir).filter(isDefFile)) {
458
458
  const lname = defName(lf);
459
459
  if (!usedLayouts.has(lname))
460
- report("warn", file, `layouts/${lf} is not referenced by any Switch case. An orphan face is unreachable (add a case "${lname}" or delete the file) (docs/design/03)`);
460
+ report("warn", file, `layouts/${lf} is not referenced by any Switch case. An orphan face is unreachable (add a case "${lname}" or delete the file) (docs.unoverse.ai/design/components)`);
461
461
  }
462
462
  }
463
463
  // v2 STRAGGLER NUDGE: inside a component that declares a tree, every
@@ -486,14 +486,14 @@ function lintFile(file) {
486
486
  ? (viewTree.initial ?? Object.keys(viewTree.states)[0])
487
487
  : (mDefault ?? (hasStateBlock ? json.state.defaultState : undefined));
488
488
  if (typeof arrival !== "string")
489
- report("error", file, `a faced component must declare its base state — a v2 state.view tree \`initial\`, or (legacy) manifest.defaultState / state.defaultState (docs/design/03)`);
489
+ report("error", file, `a faced component must declare its base state — a v2 state.view tree \`initial\`, or (legacy) manifest.defaultState / state.defaultState (docs.unoverse.ai/design/components)`);
490
490
  }
491
491
  if (stateFiles.length) {
492
492
  const order = Array.isArray(json.stateOrder) ? [...json.stateOrder].sort() : null;
493
493
  if (!order || !order.length)
494
- report("error", file, `has states/ but no stateOrder in the envelope (docs/design/03)`);
494
+ report("error", file, `has states/ but no stateOrder in the envelope (docs.unoverse.ai/design/components)`);
495
495
  else if (JSON.stringify(order) !== JSON.stringify(stateFiles))
496
- report("error", file, `stateOrder and states/*.json must name the same set (docs/design/03)`);
496
+ report("error", file, `stateOrder and states/*.json must name the same set (docs.unoverse.ai/design/components)`);
497
497
  }
498
498
  }
499
499
  } else {
@@ -502,7 +502,7 @@ function lintFile(file) {
502
502
  } else {
503
503
  // bare partial (layouts/ states/ components/ blocks/, or an atom). A template
504
504
  // layout's TOP-LEVEL node is the app's layout root — the one non-slot home for
505
- // `appWidth` (state-owned sizing, docs/design/05).
505
+ // `appWidth` (state-owned sizing, docs.unoverse.ai/design/apps).
506
506
  walkNode(json, file, root, null, /[\\/]layouts[\\/][^\\/]+\.(json|yaml)$/.test(file) && isAppPath(file));
507
507
  }
508
508
  }
@@ -290,7 +290,7 @@ const checkDimension = (file, where, key, v) => {
290
290
  // or the scale drifts straight back to two vocabularies for the same number — which is
291
291
  // the state the t-shirt aliases left it in, and the reason they were retired.
292
292
  if (PAGE_WIDTH_KEYS.has(key) && layoutAlias.has(val))
293
- report("error", file, `${where}.${key}: "${val}" has a name — use "${layoutAlias.get(val)}". A page-level width reads as what it IS; a bare step here puts two spellings on one value (docs/design/06)`);
293
+ report("error", file, `${where}.${key}: "${val}" has a name — use "${layoutAlias.get(val)}". A page-level width reads as what it IS; a bare step here puts two spellings on one value (docs.unoverse.ai/design/styles-and-tokens)`);
294
294
  // A shorthand ("auto auto 0 0" on `inset`) is a list of dimensions; each word is one.
295
295
  for (const word of val.trim().split(/\s+/)) {
296
296
  if (spaceSteps.has(word) || DIMENSION_LITERALS.has(word)) continue;
@@ -298,7 +298,7 @@ const checkDimension = (file, where, key, v) => {
298
298
  // percentage value; LAW 1 owns those, and `calc()`/`%` are legitimate escape hatches.
299
299
  if (/^\d/.test(word) && !/^\d+(\.\d+)?$/.test(word)) continue;
300
300
  if (/[()%]/.test(word)) continue; // calc(), min(), clamp(), 50%
301
- report("error", file, `${where}.${key}: "${word}" is not a step on the space scale. Invalid values fall through as broken CSS (auto sizing). Real steps: ${stepList()} (docs/design/06)`);
301
+ report("error", file, `${where}.${key}: "${word}" is not a step on the space scale. Invalid values fall through as broken CSS (auto sizing). Real steps: ${stepList()} (docs.unoverse.ai/design/styles-and-tokens)`);
302
302
  }
303
303
  }
304
304
  };
@@ -329,7 +329,7 @@ const checkToken = (file, where, key, v) => {
329
329
  report(
330
330
  "error",
331
331
  file,
332
- `${where}.${k}: "${val}" is not a ${bucket} token. Unknown names are handed to CSS verbatim and dropped — the style simply does not apply, with no error anywhere${extra}. Known: ${[...T[bucket]].sort().join(", ") || "none"} (docs/design/06)`,
332
+ `${where}.${k}: "${val}" is not a ${bucket} token. Unknown names are handed to CSS verbatim and dropped — the style simply does not apply, with no error anywhere${extra}. Known: ${[...T[bucket]].sort().join(", ") || "none"} (docs.unoverse.ai/design/styles-and-tokens)`,
333
333
  );
334
334
 
335
335
  // The straightforward one-bucket keys (background/color/shadow/radius*/font/lineHeight).
@@ -354,9 +354,9 @@ const checkToken = (file, where, key, v) => {
354
354
  const parts = v.trim().split(/\s+/);
355
355
  const [w, c] = parts.length > 1 ? parts : [null, parts[0]];
356
356
  if (w !== null && !T.borderWidth.has(w))
357
- report("error", file, `${where}.${key}: "${w}" is not a border-width token (the leading word of "<width> <colour>"). Known: ${[...T.borderWidth].sort().join(", ") || "none"} (docs/design/06)`);
357
+ report("error", file, `${where}.${key}: "${w}" is not a border-width token (the leading word of "<width> <colour>"). Known: ${[...T.borderWidth].sort().join(", ") || "none"} (docs.unoverse.ai/design/styles-and-tokens)`);
358
358
  if (!LITERAL_VALUES.has(c) && !T.color.has(`border.${c}`) && !T.color.has(c))
359
- report("error", file, `${where}.${key}: "${c}" resolves to no colour token (tried border.${c}, then ${c}). The border renders with no colour and nothing reports it (docs/design/06)`);
359
+ report("error", file, `${where}.${key}: "${c}" resolves to no colour token (tried border.${c}, then ${c}). The border renders with no colour and nothing reports it (docs.unoverse.ai/design/styles-and-tokens)`);
360
360
  return;
361
361
  }
362
362
 
@@ -407,7 +407,7 @@ 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
- // The ORG tier's component names, per org (docs/unoverse/UNOVERSE_COMPONENT_ORGS.md):
410
+ // The ORG tier's component names, per org (docs.unoverse.ai/design/components):
411
411
  // a Ref may also resolve an org component — bare from inside that org's own tree (the
412
412
  // resolver's context rule), or org-qualified as `<org>/<name>` when two orgs share the
413
413
  // name. Org-privacy holds: a file only ever reaches its OWN org's components, and a
@@ -576,7 +576,7 @@ for (const orgDir of orgDirs) {
576
576
 
577
577
  // ── component-name uniqueness (per tier, no marketplace shadowing) ──
578
578
  // Names are unique WITHIN a home: two orgs may ship the same name (each addressed
579
- // `unoverse://components/<org>/<name>` — docs/unoverse/UNOVERSE_COMPONENT_ORGS.md),
579
+ // `unoverse://components/<org>/<name>` — docs.unoverse.ai/design/components),
580
580
  // but an org may never SHADOW a marketplace name, so a bare ref stays unambiguous:
581
581
  // bare = the marketplace tier, or the single org that carries the name.
582
582
  {
@@ -627,7 +627,7 @@ for (const orgDir of orgDirs) {
627
627
  report(
628
628
  "error",
629
629
  mf,
630
- `org "${org}" has ${defaults.length} apps with "default": true. An org has exactly ONE default app (its /mcp/${org} front door). Keep it on one manifest, remove it from the others (docs/unoverse/UNOVERSE_MCP_APP_PROTOCOL.md §4b)`,
630
+ `org "${org}" has ${defaults.length} apps with "default": true. An org has exactly ONE default app (its /mcp/${org} front door). Keep it on one manifest, remove it from the others (docs.unoverse.ai/design/apps §4b)`,
631
631
  );
632
632
  }
633
633
 
@@ -690,7 +690,7 @@ for (const orgDir of [DS, ...orgDirs]) {
690
690
 
691
691
  for (const e of entries) {
692
692
  if (!KNOWN_LIFECYCLES.has(e.phase))
693
- report("error", mf, `lifecycle "${e.phase}" is not a phase the platform fires, so it would never run. Known: ${[...KNOWN_LIFECYCLES].join(", ")} (docs/unoverse/UNOVERSE_AUTHORING.md §3c)`);
693
+ report("error", mf, `lifecycle "${e.phase}" is not a phase the platform fires, so it would never run. Known: ${[...KNOWN_LIFECYCLES].join(", ")} (docs.unoverse.ai/unoverse/UNOVERSE_AUTHORING §3c)`);
694
694
  if (e.layouts !== undefined && !PHASES_WITH_LAYOUTS.has(e.phase))
695
695
  report("error", mf, `lifecycle "${e.phase}" declares layouts, but only ${[...PHASES_WITH_LAYOUTS].join(", ")} fires per view — the scope would be ignored`);
696
696
  // WHAT RUNS is named by `handler`; the phase only says WHEN. A custom hook is a
@@ -759,7 +759,7 @@ for (const orgDir of [DS, ...orgDirs]) {
759
759
  if (T) {
760
760
  for (const k of ["background", "color"])
761
761
  if (typeof latch[k] === "string" && latch[k].trim() && !T.color.has(latch[k]))
762
- report("error", mf, `latch.${k}: "${latch[k]}" is not a colour token. Unknown names reach CSS verbatim and are dropped, so the pill renders unstyled with no error anywhere. Known: ${[...T.color].sort().join(", ") || "none"} (docs/design/06)`);
762
+ report("error", mf, `latch.${k}: "${latch[k]}" is not a colour token. Unknown names reach CSS verbatim and are dropped, so the pill renders unstyled with no error anywhere. Known: ${[...T.color].sort().join(", ") || "none"} (docs.unoverse.ai/design/styles-and-tokens)`);
763
763
  }
764
764
 
765
765
  /**
@@ -791,13 +791,13 @@ function checkCondition(vw, file, where) {
791
791
  if (typeof vw === "string") return; // bare truthy field
792
792
  if (vw && typeof vw === "object" && !Array.isArray(vw)) {
793
793
  if (typeof vw.field !== "string")
794
- report("error", file, `${where}: condition needs a "field" (docs/design/04)`);
794
+ report("error", file, `${where}: condition needs a "field" (docs.unoverse.ai/design/state)`);
795
795
  const extra = Object.keys(vw).filter((k) => !CONDITION_KEYS.has(k));
796
796
  if (extra.length)
797
- report("error", file, `${where}: illegal condition key(s) ${extra.join(", ")}. Only eq/ne/in/truthy exist; no and/or/arithmetic (derive in the node) (docs/design/03)`);
797
+ report("error", file, `${where}: illegal condition key(s) ${extra.join(", ")}. Only eq/ne/in/truthy exist; no and/or/arithmetic (derive in the node) (docs.unoverse.ai/design/components)`);
798
798
  return;
799
799
  }
800
- report("error", file, `${where}: visibleWhen must be a field name or { field, eq|ne|in } (docs/design/04)`);
800
+ report("error", file, `${where}: visibleWhen must be a field name or { field, eq|ne|in } (docs.unoverse.ai/design/state)`);
801
801
  }
802
802
 
803
803
  // ── per-node structural walk ──
@@ -29,25 +29,25 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
29
29
  const a = defPath(root, node.$include);
30
30
  const b = join(root, node.$include);
31
31
  if (!a && !existsSync(b))
32
- report("error", file, `$include "${node.$include}" does not resolve under ${relative(process.cwd(), root)}/ (docs/design/03)`);
32
+ report("error", file, `$include "${node.$include}" does not resolve under ${relative(process.cwd(), root)}/ (docs.unoverse.ai/design/components)`);
33
33
  return; // the included file is linted on its own
34
34
  }
35
35
 
36
36
  const t = node.type;
37
37
  if (typeof t !== "string")
38
- report("error", file, `node without "type" (and no $include). Every node names a primitive (docs/design/02)`);
38
+ report("error", file, `node without "type" (and no $include). Every node names a primitive (docs.unoverse.ai/design/sdui-and-mcp-apps)`);
39
39
  else if (!PRIMITIVES.has(t))
40
- report("error", file, `unknown primitive "${t}". The set is closed; compose, don't invent (docs/design/02)`);
40
+ report("error", file, `unknown primitive "${t}". The set is closed; compose, don't invent (docs.unoverse.ai/design/sdui-and-mcp-apps)`);
41
41
 
42
42
  if (t === "Switch") {
43
43
  if (typeof node.on !== "string" || !node.cases || typeof node.cases !== "object")
44
- report("error", file, `Switch needs "on" (the discriminant field) + "cases" (docs/design/04)`);
44
+ report("error", file, `Switch needs "on" (the discriminant field) + "cases" (docs.unoverse.ai/design/state)`);
45
45
  else
46
46
  for (const [caseKey, branch] of Object.entries(node.cases)) {
47
47
  const vw = branch && typeof branch === "object" ? branch.visibleWhen : undefined;
48
48
  const guarded = typeof vw === "string" ? vw : vw && typeof vw === "object" ? vw.field : null;
49
49
  if (guarded === node.on)
50
- report("error", file, `Switch on "${node.on}" → case "${caseKey}" re-guards its own discriminant. A layer never guards itself; delete the visibleWhen (docs/design/03)`);
50
+ 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)`);
51
51
  }
52
52
  }
53
53
  // Each: a `template` + a list — EITHER a literal `items:[]` (hardcoded content,
@@ -55,11 +55,11 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
55
55
  if (t === "Each") {
56
56
  const hasList = Array.isArray(node.items) || (node.bind && typeof node.bind === "object" && node.bind.items);
57
57
  if (!node.template || !hasList)
58
- report("error", file, `Each needs "template" + a list. Literal "items": [...] or "bind": { "items": "<field>" } (docs/design/03)`);
58
+ report("error", file, `Each needs "template" + a list. Literal "items": [...] or "bind": { "items": "<field>" } (docs.unoverse.ai/design/components)`);
59
59
  }
60
60
  if (t === "Ref") {
61
61
  if (typeof node.ref !== "string")
62
- report("error", file, `Ref needs "ref": "<atom name>" (docs/design/03)`);
62
+ report("error", file, `Ref needs "ref": "<atom name>" (docs.unoverse.ai/design/components)`);
63
63
  else if (atomsDirExists && !refResolves(node.ref, file))
64
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
@@ -96,7 +96,7 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
96
96
  file,
97
97
  `Ref "${node.ref}" ${key === "props" ? "remaps" : "passes"} "${name}", which the atom does not declare. ` +
98
98
  `Both \`props\` and \`with\` are matched BY NAME against the atom's own props, and an unknown key is ` +
99
- `silently ignored: the element renders and does nothing. ${node.ref} declares: ${known} (docs/design/03)`,
99
+ `silently ignored: the element renders and does nothing. ${node.ref} declares: ${known} (docs.unoverse.ai/design/components)`,
100
100
  );
101
101
  }
102
102
  }
@@ -109,9 +109,9 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
109
109
  checkToken(file, t, "icon", node.icon);
110
110
  if (t === "ComponentSlot") {
111
111
  if (!node.select || typeof node.select !== "object")
112
- report("error", file, `ComponentSlot needs "select" ({} for the conversation flow) (docs/design/05)`);
112
+ report("error", file, `ComponentSlot needs "select" ({} for the conversation flow) (docs.unoverse.ai/design/apps)`);
113
113
  else if (node.select.from === "all" && !node.select.type && !node.select.where)
114
- report("warn", file, `global ComponentSlot (from:"all") with no "type" and no "where". Selects OLDEST-first; a trap in a multi-turn surface. Filter by "where" (the reaction contract, §5b) or pin "type", unless the shell is deliberately catch-all (docs/design/05)`);
114
+ report("warn", file, `global ComponentSlot (from:"all") with no "type" and no "where". Selects OLDEST-first; a trap in a multi-turn surface. Filter by "where" (the reaction contract, §5b) or pin "type", unless the shell is deliberately catch-all (docs.unoverse.ai/design/apps)`);
115
115
  // STATE-SELECTED UI (STATE_MODEL §5): a reaction surface reacts to the component's
116
116
  // PUBLIC state — the `view` axis (`defaultState` = legacy alias) — never to a
117
117
  // component's internal state (step/phase/…) which is private to the component.
@@ -122,16 +122,16 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
122
122
  // claim exactly ONE view by `eq` — `ne`/`in`/bare selects make "which state is
123
123
  // the template in?" ambiguous.
124
124
  else if (node.select.where && (node.select.where.field === "view" || node.select.where.field === "defaultState") && typeof node.select.where.eq !== "string")
125
- report("error", file, `a reaction surface claims exactly ONE view: select.where needs "eq": "<view>": ne/in/bare make the template's active state ambiguous (docs/design/04)`);
125
+ report("error", file, `a reaction surface claims exactly ONE view: select.where needs "eq": "<view>": ne/in/bare make the template's active state ambiguous (docs.unoverse.ai/design/state)`);
126
126
  }
127
127
 
128
- // STATE-OWNED SIZING (docs/design/05): every PANEL states its width once via
128
+ // STATE-OWNED SIZING (docs.unoverse.ai/design/apps): every PANEL states its width once via
129
129
  // `appWidth`; the app is the sum of the open ones. A plain node = always open (the
130
130
  // core chat column); a reaction-surface ComponentSlot = open while occupied; a
131
131
  // visibleWhen pane = open while its condition matches.
132
132
  if (node.appWidth !== undefined) {
133
133
  if (typeof node.appWidth !== "string" || node.appWidth.trim() === "")
134
- report("error", file, `"appWidth" must be a CSS width string ("360px", "min(50vw, 760px)") or a named app size ("chat", "rail", "panel") (docs/design/05)`);
134
+ report("error", file, `"appWidth" must be a CSS width string ("360px", "min(50vw, 760px)") or a named app size ("chat", "rail", "panel") (docs.unoverse.ai/design/apps)`);
135
135
  // `flex` is contract vocabulary, not a token: the surface takes the REMAINING
136
136
  // host space (the SDK reports a full-width app while it is active).
137
137
  // Any other bare name is a STANDARD SIZE — it must exist in the org's
@@ -140,17 +140,17 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
140
140
  else if (node.appWidth !== "flex" && /^[a-z][a-z0-9-]*$/i.test(node.appWidth)) {
141
141
  const sizes = appSizesForFile(file);
142
142
  if (sizes && !(node.appWidth in sizes))
143
- report("error", file, `"appWidth": "${node.appWidth}" names no app size. Use "flex", a CSS width, or a name from styles/semantic/app-sizes (known: ${Object.keys(sizes).join(", ") || "none"}) (docs/design/05)`);
143
+ report("error", file, `"appWidth": "${node.appWidth}" names no app size. Use "flex", a CSS width, or a name from styles/semantic/app-sizes (known: ${Object.keys(sizes).join(", ") || "none"}) (docs.unoverse.ai/design/apps)`);
144
144
  }
145
145
  if (t === "ComponentSlot" && !(node.select && node.select.where))
146
- report("error", file, `"appWidth" on a ComponentSlot without select.where. Only a reaction surface can slide out; the flow slot never sizes the app (docs/design/05)`);
146
+ report("error", file, `"appWidth" on a ComponentSlot without select.where. Only a reaction surface can slide out; the flow slot never sizes the app (docs.unoverse.ai/design/apps)`);
147
147
  // ONE declaration per panel: the SDK sizes the frame FROM appWidth (width +
148
148
  // flex: 0 0 auto) — a frame width/flex alongside it is dead duplication that
149
149
  // can silently disagree.
150
150
  if (t === "ComponentSlot" && node.frame && node.frame.style) {
151
151
  for (const dup of ["width", "flex"])
152
152
  if (node.frame.style[dup] !== undefined)
153
- report("error", file, `panel frame declares style.${dup} alongside appWidth. The panel states its width ONCE; the SDK sizes the frame from appWidth. Remove the frame ${dup} (docs/design/05)`);
153
+ report("error", file, `panel frame declares style.${dup} alongside appWidth. The panel states its width ONCE; the SDK sizes the frame from appWidth. Remove the frame ${dup} (docs.unoverse.ai/design/apps)`);
154
154
  }
155
155
  }
156
156
 
@@ -161,7 +161,7 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
161
161
  if (node.brief !== undefined) {
162
162
  const b = node.brief;
163
163
  if (typeof b !== "string" && (typeof b !== "object" || b === null || Array.isArray(b)))
164
- report("error", file, `"brief" must be a string (the description) or { description, maxLength | minItems/maxItems } (docs/design/03)`);
164
+ report("error", file, `"brief" must be a string (the description) or { description, maxLength | minItems/maxItems } (docs.unoverse.ai/design/components)`);
165
165
  else if (typeof b === "object") {
166
166
  // description/maxLength/minItems/maxItems COMPILE into the tool inputSchema. `hydrate` is a
167
167
  // NON-schema brief annotation (a hydration hook naming what to hydrate) — a valid brief key
@@ -172,24 +172,24 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
172
172
  const BRIEF_KEYS = new Set(["description", "maxLength", "minItems", "maxItems", "hydrate", "optional"]);
173
173
  for (const k of Object.keys(b))
174
174
  if (!BRIEF_KEYS.has(k))
175
- report("error", file, `brief.${k} is not part of the brief contract. Only description / maxLength / minItems / maxItems / optional (schema) or hydrate (hydration hook) are allowed (docs/design/03)`);
175
+ report("error", file, `brief.${k} is not part of the brief contract. Only description / maxLength / minItems / maxItems / optional (schema) or hydrate (hydration hook) are allowed (docs.unoverse.ai/design/components)`);
176
176
  if (b.description !== undefined && typeof b.description !== "string")
177
- report("error", file, `brief.description must be a string. It IS the schema field's description (docs/design/03)`);
177
+ report("error", file, `brief.description must be a string. It IS the schema field's description (docs.unoverse.ai/design/components)`);
178
178
  if (b.optional !== undefined && typeof b.optional !== "boolean")
179
- report("error", file, `brief.optional must be a boolean (docs/design/03)`);
179
+ report("error", file, `brief.optional must be a boolean (docs.unoverse.ai/design/components)`);
180
180
  if (b.hydrate !== undefined && typeof b.hydrate !== "string")
181
- report("error", file, `brief.hydrate must be a string (names the field/source to hydrate) (docs/design/03)`);
181
+ report("error", file, `brief.hydrate must be a string (names the field/source to hydrate) (docs.unoverse.ai/design/components)`);
182
182
  for (const nk of ["maxLength", "minItems", "maxItems"])
183
183
  if (b[nk] !== undefined && (typeof b[nk] !== "number" || b[nk] < 0 || !Number.isInteger(b[nk])))
184
- report("error", file, `brief.${nk} must be a non-negative integer. It compiles to the native JSON Schema keyword (docs/design/03)`);
184
+ report("error", file, `brief.${nk} must be a non-negative integer. It compiles to the native JSON Schema keyword (docs.unoverse.ai/design/components)`);
185
185
  if (typeof b.minItems === "number" && typeof b.maxItems === "number" && b.minItems > b.maxItems)
186
- report("error", file, `brief.minItems (${b.minItems}) > maxItems (${b.maxItems}). No composition can satisfy this schema (docs/design/03)`);
186
+ report("error", file, `brief.minItems (${b.minItems}) > maxItems (${b.maxItems}). No composition can satisfy this schema (docs.unoverse.ai/design/components)`);
187
187
  const bound = node.bind && (node.bind.value || node.bind.src);
188
188
  const isEach = t === "Each" && node.bind && node.bind.items;
189
189
  if (b.maxLength !== undefined && !bound)
190
- report("warn", file, `brief.maxLength on a node with no bind. A length cap only compiles when the brief sits next to the bound field it governs (docs/design/03)`);
190
+ report("warn", file, `brief.maxLength on a node with no bind. A length cap only compiles when the brief sits next to the bound field it governs (docs.unoverse.ai/design/components)`);
191
191
  if ((b.minItems !== undefined || b.maxItems !== undefined) && !isEach)
192
- report("warn", file, `brief.minItems/maxItems on a non-Each node. Item counts only compile on the Each that binds the array (docs/design/03)`);
192
+ report("warn", file, `brief.minItems/maxItems on a non-Each node. Item counts only compile on the Each that binds the array (docs.unoverse.ai/design/components)`);
193
193
  }
194
194
  }
195
195
 
@@ -225,12 +225,12 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
225
225
  widthCap = widthCap == null ? Number(mw) : Math.min(widthCap, Number(mw));
226
226
  const hb = node.style.hideBelow;
227
227
  if (typeof hb === "string" && /^\d+$/.test(hb) && widthCap != null && Number(hb) >= widthCap)
228
- report("warn", file, `hideBelow "${hb}" ≥ an ancestor maxWidth "${widthCap}". The query can only be satisfied by the surrounding surface, so visibility depends on the host, not the card; lower the threshold below the card's own max width (docs/design/06)`);
228
+ report("warn", file, `hideBelow "${hb}" ≥ an ancestor maxWidth "${widthCap}". The query can only be satisfied by the surrounding surface, so visibility depends on the host, not the card; lower the threshold below the card's own max width (docs.unoverse.ai/design/styles-and-tokens)`);
229
229
  const checkKeys = (obj, where) => {
230
230
  for (const k of Object.keys(obj)) {
231
231
  if (k === "when") continue; // validated below
232
232
  if (!STYLE_KEYS.has(k))
233
- report("error", file, `${where}: unknown style key "${k}". The style vocabulary is closed (the cross-platform contract). Typo, or a web-ism that won't port (docs/design/06)`);
233
+ report("error", file, `${where}: unknown style key "${k}". The style vocabulary is closed (the cross-platform contract). Typo, or a web-ism that won't port (docs.unoverse.ai/design/styles-and-tokens)`);
234
234
  else if ((k === "hover" || k === "active") && obj[k] && typeof obj[k] === "object")
235
235
  checkKeys(obj[k], `${where}.${k}`);
236
236
  else if (DIMENSION_KEYS.has(k)) checkDimension(file, where, k, obj[k]);
@@ -244,15 +244,15 @@ function walkNode(node, file, root, widthCap = null, isLayoutRoot = false) {
244
244
  if (node.style.when !== undefined) {
245
245
  const w = node.style.when;
246
246
  if (!Array.isArray(w))
247
- report("error", file, `${t}.style.when must be an array of { field, eq|ne|in, apply } entries (docs/design/04)`);
247
+ report("error", file, `${t}.style.when must be an array of { field, eq|ne|in, apply } entries (docs.unoverse.ai/design/state)`);
248
248
  else
249
249
  for (const e of w) {
250
250
  if (!e || typeof e !== "object" || typeof e.field !== "string" || !e.apply)
251
- report("error", file, `${t}.style.when entry needs "field" + "apply" (docs/design/04)`);
251
+ report("error", file, `${t}.style.when entry needs "field" + "apply" (docs.unoverse.ai/design/state)`);
252
252
  else {
253
253
  const extra = Object.keys(e).filter((k) => !CONDITION_KEYS.has(k) && k !== "apply");
254
254
  if (extra.length)
255
- report("error", file, `${t}.style.when: illegal key(s) ${extra.join(", ")}. Conditions are eq/ne/in/truthy only (docs/design/04)`);
255
+ report("error", file, `${t}.style.when: illegal key(s) ${extra.join(", ")}. Conditions are eq/ne/in/truthy only (docs.unoverse.ai/design/state)`);
256
256
  if (typeof e.apply === "object") checkKeys(e.apply, `${t}.style.when.apply`);
257
257
  }
258
258
  }