@unoverse-platform/studio 0.1.90 → 0.1.92
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/local/plugin.ts +21 -65
- package/package.json +13 -3
- package/vendor/base/src/agent-mcp/discovery.ts +203 -0
- package/vendor/base/src/agent-mcp/handoff.ts +56 -0
- package/vendor/base/src/agent-mcp/index.ts +44 -0
- package/vendor/base/src/agent-mcp/invoke.ts +153 -0
- package/vendor/base/src/agent-mcp/lean.ts +114 -0
- package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
- package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
- package/vendor/base/src/agent-mcp/rowComponents.ts +234 -0
- package/vendor/base/src/agent-mcp/tools.ts +134 -0
- package/vendor/base/src/agent-mcp/types.ts +138 -0
- package/vendor/base/src/auth/auth.ts +307 -0
- package/vendor/base/src/auth/authConfig.ts +73 -0
- package/vendor/base/src/boot.ts +245 -0
- package/vendor/base/src/definitions/definitions.ts +18 -0
- package/vendor/base/src/definitions/dsPackage.ts +45 -0
- package/vendor/base/src/definitions/fsCache.ts +155 -0
- package/vendor/base/src/definitions/index.ts +19 -0
- package/vendor/base/src/definitions/inputs.ts +30 -0
- package/vendor/base/src/definitions/load/compile/component.ts +80 -0
- package/vendor/base/src/definitions/load/compile/index.ts +12 -0
- package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
- package/vendor/base/src/definitions/load/compile/template.ts +153 -0
- package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
- package/vendor/base/src/definitions/load/expand/index.ts +17 -0
- package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
- package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
- package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
- package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
- package/vendor/base/src/definitions/load/load.ts +487 -0
- package/vendor/base/src/definitions/model.ts +238 -0
- package/vendor/base/src/definitions/registry/catalog.ts +659 -0
- package/vendor/base/src/definitions/registry/homes.ts +252 -0
- package/vendor/base/src/definitions/theme.ts +383 -0
- package/vendor/base/src/executor.ts +284 -0
- package/vendor/base/src/http/uwsFetchAdapter.ts +397 -0
- package/vendor/base/src/items/baseVersion.ts +96 -0
- package/vendor/base/src/items/collect.ts +246 -0
- package/vendor/base/src/items/fingerprint.ts +34 -0
- package/vendor/base/src/items/publish.ts +212 -0
- package/vendor/base/src/lint/context.mjs +56 -0
- package/vendor/base/src/lint/design/defs.mjs +46 -0
- package/vendor/base/src/lint/design/file.mjs +737 -0
- package/vendor/base/src/lint/design/index.d.mts +30 -0
- package/vendor/base/src/lint/design/index.mjs +996 -0
- package/vendor/base/src/lint/design/tokens.mjs +128 -0
- package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
- package/vendor/base/src/lint/design/walk.mjs +444 -0
- package/vendor/base/src/lint/index.mjs +28 -0
- package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
- package/vendor/base/src/lint/nodes/index.d.mts +28 -0
- package/vendor/base/src/lint/nodes/index.mjs +82 -0
- package/vendor/base/src/lint/nodes/load.mjs +126 -0
- package/vendor/base/src/lint/nodes/node.mjs +764 -0
- package/vendor/base/src/lint/nodes/package.mjs +112 -0
- package/vendor/base/src/lint/nodes/schema.mjs +75 -0
- package/vendor/base/src/logger.ts +33 -0
- package/vendor/base/src/manifests/compose.ts +539 -0
- package/vendor/base/src/manifests/executor/callback.ts +107 -0
- package/vendor/base/src/manifests/executor/context.ts +110 -0
- package/vendor/base/src/manifests/executor/index.ts +47 -0
- package/vendor/base/src/manifests/executor/lookup.ts +21 -0
- package/vendor/base/src/manifests/executor/memory.ts +62 -0
- package/vendor/base/src/manifests/executor/promise.ts +71 -0
- package/vendor/base/src/manifests/executor/refire.ts +34 -0
- package/vendor/base/src/manifests/executor/send.ts +65 -0
- package/vendor/base/src/manifests/executor/session.ts +46 -0
- package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
- package/vendor/base/src/manifests/index.ts +280 -0
- package/vendor/base/src/manifests/integrity.ts +93 -0
- package/vendor/base/src/manifests/rows.ts +33 -0
- package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
- package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
- package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
- package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
- package/vendor/base/src/manifests/runtime/channels/service.ts +138 -0
- package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
- package/vendor/base/src/manifests/runtime/context.ts +156 -0
- package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
- package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
- package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
- package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
- package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
- package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
- package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
- package/vendor/base/src/manifests/runtime/events.ts +223 -0
- package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
- package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
- package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
- package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
- package/vendor/base/src/manifests/runtime/index.ts +57 -0
- package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
- package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
- package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
- package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
- package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
- package/vendor/base/src/manifests/runtime/state.ts +171 -0
- package/vendor/base/src/manifests/runtime/templating.ts +110 -0
- package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
- package/vendor/base/src/manifests/runtime/tools/toolloop.ts +550 -0
- package/vendor/base/src/manifests/runtime/usage.ts +87 -0
- package/vendor/base/src/manifests/source.ts +196 -0
- package/vendor/base/src/manifests/watch.ts +67 -0
- package/vendor/base/src/markdown/document.ts +141 -0
- package/vendor/base/src/paths.ts +180 -0
- package/vendor/base/src/platform/audioManager.ts +100 -0
- package/vendor/base/src/platform/clientTransport.ts +65 -0
- package/vendor/base/src/platform/executionContext.ts +157 -0
- package/vendor/base/src/platform/index.ts +10 -0
- package/vendor/base/src/platform/pluginAPI.ts +146 -0
- package/vendor/base/src/platform/redis.ts +54 -0
- package/vendor/base/src/platform/serviceCalls.ts +222 -0
- package/vendor/base/src/pluginBase.ts +248 -0
- package/vendor/base/src/plugins/README.md +57 -0
- package/vendor/base/src/plugins/discovery.ts +87 -0
- package/vendor/base/src/plugins/index.ts +16 -0
- package/vendor/base/src/plugins/loader.ts +179 -0
- package/vendor/base/src/plugins/redis.ts +36 -0
- package/vendor/base/src/plugins/state.ts +53 -0
- package/vendor/base/src/registry.ts +95 -0
- package/vendor/base/src/security/permissions.ts +93 -0
- package/vendor/base/src/security/rules.ts +73 -0
- package/vendor/base/src/template/SafeExpression.ts +556 -0
- package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
- package/vendor/base/src/template/index.ts +22 -0
- package/vendor/base/src/types.ts +50 -0
- package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
- package/vendor/base/src/workflow/PlatformMemoryCore.ts +449 -0
- package/vendor/base/src/workflow/routing.ts +122 -0
- package/vendor/sdk/lib/core/store.ts +26 -6
- package/local/mcp.ts +0 -265
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CATALOG: listing every kind, app manifests, and the state-order/state-entry
|
|
3
|
+
* projections Studio and the workbench read. Split from definitions.ts 2026-08-29.
|
|
4
|
+
* Imports the load core lazily at call time — the definitions⇄catalog cycle is
|
|
5
|
+
* function-level only and safe under ESM live bindings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readdirSync, existsSync, statSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { readDefCached, defPath, isDefFile, defName, dirSignature, cachedBySignature } from "../fsCache.js";
|
|
11
|
+
import { SHARED_DIR, appDirs, templateDirs, componentDirs, resolveComponentDirs, parseRef, projectDir, listOrgs } from "./homes.js";
|
|
12
|
+
import type { UnoverseDefinition } from "../model.js";
|
|
13
|
+
import { normalizeDeclaredKind, defaultLayoutOf } from "../model.js";
|
|
14
|
+
import { loadDefinition } from "../load/load.js";
|
|
15
|
+
import { expandNode } from "../load/expand/index.js";
|
|
16
|
+
|
|
17
|
+
/** List definitions of a kind. Atoms come from the ONE shared home. Components come
|
|
18
|
+
* from the marketplace PLUS the org packs — every org's when `org` is omitted
|
|
19
|
+
* (each tagged), or just `org`'s own + the shared tier when given (org-private:
|
|
20
|
+
* another org's components are never listed into an org-scoped view). Templates
|
|
21
|
+
* come from `org`'s folder, or every org's (each tagged). */
|
|
22
|
+
export function listDefinitions(kind: "component" | "app" | "atom" | "template", org?: string): UnoverseDefinition[] {
|
|
23
|
+
const out: UnoverseDefinition[] = [];
|
|
24
|
+
const dirs =
|
|
25
|
+
kind === "app" ? appDirs(org ?? null)
|
|
26
|
+
: kind === "component" ? componentDirs(org ?? null)
|
|
27
|
+
: kind === "template" ? templateDirs(org ?? null)
|
|
28
|
+
: [{ org: undefined as string | undefined, dir: SHARED_DIR[kind] }];
|
|
29
|
+
for (const { org: o, dir } of dirs) {
|
|
30
|
+
if (!existsSync(dir)) continue;
|
|
31
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
32
|
+
// PER-ENTRY ISOLATION: a definition MID-EDIT (a saved layout referencing a
|
|
33
|
+
// states/ file not written yet, malformed JSON) is a normal state of a
|
|
34
|
+
// live-editing platform. One broken definition must degrade to a skipped
|
|
35
|
+
// entry with a warning — never fail the whole catalog request (observed
|
|
36
|
+
// live: an in-flight template edit 500'd /dev/apps for every org).
|
|
37
|
+
try {
|
|
38
|
+
if (e.isFile() && isDefFile(e.name)) {
|
|
39
|
+
/**
|
|
40
|
+
* A FLAT FILE IS LOADED, NOT READ (2026-08-31).
|
|
41
|
+
*
|
|
42
|
+
* This handed the RAW yaml straight to every caller while the folder branch below
|
|
43
|
+
* composed properly — so a flat definition reached the workbench with none of its
|
|
44
|
+
* compiled projections: no `layouts`, no `sections`, and its three words still
|
|
45
|
+
* sitting there as words. A flat BLOCK (`pair`, `feature`) therefore listed by
|
|
46
|
+
* name and drew as an empty stage, because the screen seeds its preview from
|
|
47
|
+
* `sections` and there were none. Everything that verified the block used
|
|
48
|
+
* `loadDefinition`; the screen used this, and the two disagreed.
|
|
49
|
+
*
|
|
50
|
+
* ATOMS keep the raw read. They are leaf vocabulary with no projections to
|
|
51
|
+
* compile, and the FILENAME has to travel: refs address a file atom by its
|
|
52
|
+
* basename, so the catalogue must name it the same way or an installed atom is
|
|
53
|
+
* unreachable.
|
|
54
|
+
*/
|
|
55
|
+
const name = defName(e.name);
|
|
56
|
+
const loaded = kind === "atom" ? null : loadDefinition(o ? `${o}/${name}` : name, kind);
|
|
57
|
+
if (loaded) {
|
|
58
|
+
out.push({ ...loaded, file: e.name } as UnoverseDefinition);
|
|
59
|
+
} else {
|
|
60
|
+
const raw = readDefCached<UnoverseDefinition>(join(dir, e.name));
|
|
61
|
+
const withFile = { ...raw, file: e.name } as UnoverseDefinition;
|
|
62
|
+
normalizeDeclaredKind(withFile);
|
|
63
|
+
out.push(o ? { ...withFile, org: o } : withFile);
|
|
64
|
+
}
|
|
65
|
+
} else if (e.isDirectory()) {
|
|
66
|
+
const d = loadDefinition(o ? `${o}/${e.name}` : e.name, kind); // composes the folder ($include + refs)
|
|
67
|
+
if (d) out.push(d);
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[unoverse] skipping ${kind} '${o ? `${o}/` : ""}${e.name}' — failed to load (mid-edit?): ${(err as Error)?.message ?? err}`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return out.filter((d) => d.kind === kind);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---- MCP apps: a template folder + a manifest.json (UNOVERSE_MCP_APP_PROTOCOL §4b) ----
|
|
80
|
+
//
|
|
81
|
+
// An MCP app is JUST a manifest: self-describing (name/description/category for discovery
|
|
82
|
+
// + selection) and self-running (it OWNS its workflow binding — the §6a flip). Loading an
|
|
83
|
+
// app = trigger the binding → the workflow streams component(s) back → the SDK renders
|
|
84
|
+
// them (no per-app shell; rendering the stream is generic). `template` is OPTIONAL, only
|
|
85
|
+
// for an app that wants fixed layout/chrome. The manifest is also the future
|
|
86
|
+
// spatial-ingestion record.
|
|
87
|
+
|
|
88
|
+
/** An MCP app manifest — `design/<org>/apps/<id>/manifest.yaml`. */
|
|
89
|
+
export interface AppManifest {
|
|
90
|
+
/** App id = the folder name (the app URI is `unoverse://apps/<org>/<id>`). */
|
|
91
|
+
id: string;
|
|
92
|
+
/** The org tree that serves this app (injected from the folder location, never
|
|
93
|
+
* hand-written). Channels use it to address the app's theme (`<org>/light`). */
|
|
94
|
+
org?: string;
|
|
95
|
+
name: string;
|
|
96
|
+
/** WHICH TIER THE DELIVERABLE LIVES IN — the home its definition is loaded from, and the
|
|
97
|
+
* home its address names (`componentUri`: "the address says WHAT it is"). Absent =
|
|
98
|
+
* component, the tier every app tool had until the Template Model shipped a second one.
|
|
99
|
+
*
|
|
100
|
+
* NOT `type`, which is the legacy full-surface/inline ROUTER word (`type: "template"`
|
|
101
|
+
* means a fluid-height app, and says nothing about where its definition lives). Two
|
|
102
|
+
* meanings on one field is how a template app would come to be loaded as a component and
|
|
103
|
+
* fail with a resolver error nobody could read. */
|
|
104
|
+
kind?: "component" | "template";
|
|
105
|
+
/** Human display name; falls back to `name`. */
|
|
106
|
+
title?: string;
|
|
107
|
+
description?: string;
|
|
108
|
+
/** Spatial-discovery selection text — see UnoverseDefinition.whenToUse. */
|
|
109
|
+
whenToUse?: string;
|
|
110
|
+
category?: string;
|
|
111
|
+
version?: string;
|
|
112
|
+
/** The app's named DEFAULT STATE on load — the single authored field. An OPEN name, not a
|
|
113
|
+
* closed enum: apps/channels recognize it by NAME (Switch/visibleWhen), so new states
|
|
114
|
+
* need zero protocol change (UNOVERSE_MCP_APP_PROTOCOL §4b). The known names today:
|
|
115
|
+
* - "app": fluid height — the app fills/swaps the whole surface (chat/voice shells)
|
|
116
|
+
* - "focus": fit-to-content AND opens in focus (large overlay) on load (wizards)
|
|
117
|
+
* - "component": renders INLINE in the conversation like any card — no focus, no takeover
|
|
118
|
+
* Any other name: fit-to-content; the org's apps define what the name renders as.
|
|
119
|
+
* `type` + `fluidHeight` below are LEGACY and DERIVED in `loadAppManifest` (full surface ⇒
|
|
120
|
+
* type:template + fluid · anything else ⇒ type:component + fit) so existing consumers keep
|
|
121
|
+
* reading them unchanged. Author `defaultState`; never set `type`/`fluidHeight` by hand.
|
|
122
|
+
* ("template" is the pre-rename spelling of "app", normalized on read; `mode` is the
|
|
123
|
+
* pre-rename alias of this whole field, also still read as a fallback.) */
|
|
124
|
+
defaultState?: string;
|
|
125
|
+
/** @deprecated pre-rename alias of `defaultState`; read as fallback, never author. */
|
|
126
|
+
mode?: string;
|
|
127
|
+
/** @deprecated derived from `defaultState`. The router branch (swap vs inline). */
|
|
128
|
+
type?: "template" | "component";
|
|
129
|
+
/** OPTIONAL layout shell template name. Default: none — the SDK renders the raw
|
|
130
|
+
* component stream. Set this only when the app wants fixed layout/chrome. */
|
|
131
|
+
template?: string;
|
|
132
|
+
/** The DEFAULT LAYOUT — the layouts/<name>.json used as the def's root when the
|
|
133
|
+
* envelope authors none (standard folder anatomy). Default: "main". */
|
|
134
|
+
layout?: string;
|
|
135
|
+
/** A native service the template binds (UNOVERSE_SPEC §2e-1) — e.g. "voice". The
|
|
136
|
+
* channel instantiates the matching service and feeds its state into the scope.
|
|
137
|
+
* Manifest-authored so a manifest-only template (no envelope) still declares it. */
|
|
138
|
+
service?: string;
|
|
139
|
+
/** Display icon URL (square; shown in catalogs/drawers — never embedded). */
|
|
140
|
+
icon?: string;
|
|
141
|
+
/** How to call it — JSON Schema for the input params. */
|
|
142
|
+
inputSchema?: Record<string, unknown>;
|
|
143
|
+
/** What to run — the app owns its workflow binding (app ≡ workflow). `autoTrigger`
|
|
144
|
+
* lives INSIDE it (owner ruling 2026-08-28: the load mode is part of the feed, so
|
|
145
|
+
* the binding block carries it); the loader lifts it to the top-level field every
|
|
146
|
+
* existing consumer reads. */
|
|
147
|
+
binding?: { workflow?: string; trigger?: string; autoTrigger?: boolean };
|
|
148
|
+
/** Load mode. true = on load, read the template AND fire the workflow (read resource +
|
|
149
|
+
* call tool). false/absent = just load the template (read resource), wait for the
|
|
150
|
+
* user. DERIVED from `binding.autoTrigger` on read; the top-level spelling remains
|
|
151
|
+
* legacy-readable. */
|
|
152
|
+
autoTrigger?: boolean;
|
|
153
|
+
/** Publish this app on the EXTERNAL MCP endpoint. OPT-IN: absent/false keeps the app off
|
|
154
|
+
* `/mcp` and `/mcp/{org}` entirely (no tool, no template listing) — only `makeMCP: true`
|
|
155
|
+
* reaches an outside client. Default-deny, so a new app is never externally callable by
|
|
156
|
+
* accident; publishing is always a deliberate line in the manifest.
|
|
157
|
+
* Visibility ONLY: orthogonal to auth, which is decided by the workflow's trigger toggle
|
|
158
|
+
* (`isPublicEntry`). The org URL picks WHICH SET a connector sees; this picks what is in
|
|
159
|
+
* that set at all. */
|
|
160
|
+
makeMCP?: boolean;
|
|
161
|
+
/** Asset origins this app's content loads (images, media) — e.g. the org's image CDN.
|
|
162
|
+
* Authored per app in manifest.yaml; the server UNIONS them across the connector's
|
|
163
|
+
* published apps into the MCP-app shell's CSP (`ui.csp.resourceDomains`). A sandboxed
|
|
164
|
+
* host builds its img-src from that list and blocks everything else, so an origin
|
|
165
|
+
* missing here renders as broken images in ChatGPT/Claude while working in our own
|
|
166
|
+
* hosts. Full origins with scheme, wildcards allowed: "https://*.datocms-assets.com". */
|
|
167
|
+
assetDomains?: string[];
|
|
168
|
+
/** Render lifetime (docs/design/04 §Two lifetimes). Default "turn": the instance returns
|
|
169
|
+
* to inline/retires on the next user turn (the universal reset). "conversation" = a
|
|
170
|
+
* DURABLE conversation-scoped surface (a cart, an itinerary, a composed page): keyed by
|
|
171
|
+
* the CONVERSATION (not the turn) so every re-call hydrates the same slice, and the
|
|
172
|
+
* client's new-turn reset skips it — it stays on screen until replaced or closed. */
|
|
173
|
+
lifetime?: "turn" | "conversation";
|
|
174
|
+
/* NO `latch` HERE (moved 2026-08-31, MCP guide §The Component Latch). It was a manifest
|
|
175
|
+
* field seeded onto the slice with every render, which said one thing about a component
|
|
176
|
+
* for the whole of its life; a page is addressable when it is THE PAGE and not when it
|
|
177
|
+
* is a summary in the flow, and the state is what knows that. It is now declared on the
|
|
178
|
+
* app's state and compiled to `def.latches` — a fact the client looks up, not one the
|
|
179
|
+
* server has to send. */
|
|
180
|
+
/** Workbench MOCK: what each STATE's preview seeds — `{ "<state>": ["course-card",
|
|
181
|
+
* "course-card"] }` (a repeated name = several instances, e.g. a card rail). The
|
|
182
|
+
* author decides per state; the seeded instances are written INTO that state's view
|
|
183
|
+
* (the same setValue the runtime performs). Not used in LIVE — the workflow decides. */
|
|
184
|
+
preview?: Record<string, string[]>;
|
|
185
|
+
/** LEGACY workbench mock hint (pre-`preview`): a flat component list seeded for any
|
|
186
|
+
* reaction state. Superseded by `preview` — kept as the fallback. */
|
|
187
|
+
previewComponents?: string[];
|
|
188
|
+
/** Layout height mode. Absent/true = FLUID: the template fills the available height
|
|
189
|
+
* (chat / assistant / voice layouts). false = size to CONTENT: the frame shrinks to the
|
|
190
|
+
* rendered component (focused widget apps like Bank Transfer). */
|
|
191
|
+
fluidHeight?: boolean;
|
|
192
|
+
/** The app's layer views — AUTO-DERIVED from its `states/` folder (UNOVERSE_LAYERS.md §7),
|
|
193
|
+
* NOT hand-written in manifest.json. Injected by `loadAppManifest` so the SERVED manifest
|
|
194
|
+
* makes external MCP callers aware of the states with zero drift (folder = source of truth). */
|
|
195
|
+
states?: StateEntry[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** THE FULL-SURFACE STATE. One name, no alias: `"app"`. The pre-rename spelling
|
|
199
|
+
* `"template"` is NOT read anywhere — a second accepted spelling is a second code path,
|
|
200
|
+
* and those drift into bugs that are miserable to find. A folder still saying "template"
|
|
201
|
+
* is a folder to migrate, not a case to handle. */
|
|
202
|
+
export const FULL_SURFACE_STATE = "app";
|
|
203
|
+
|
|
204
|
+
/** `defaultState` is the authored field (open NAME; `mode` = pre-rename alias); derive the
|
|
205
|
+
* legacy `type`/`fluidHeight` the rest of the system still reads (and back-derive
|
|
206
|
+
* `defaultState` for any manifest still on the old fields). */
|
|
207
|
+
function normalizeMode(m: AppManifest): AppManifest {
|
|
208
|
+
// `type: app` is the ARTIFACT DECLARATION (the authored word, 2026-08-28), not the
|
|
209
|
+
// legacy router field this function derives — strip it before the legacy read so a
|
|
210
|
+
// declaration can never impersonate a router value.
|
|
211
|
+
if ((m as { type?: string }).type === "app") {
|
|
212
|
+
const { type: _declaration, ...rest } = m as AppManifest & { type?: string };
|
|
213
|
+
m = rest as AppManifest;
|
|
214
|
+
}
|
|
215
|
+
const declared = m.defaultState ?? m.mode;
|
|
216
|
+
if (declared === FULL_SURFACE_STATE) return { ...m, defaultState: declared, type: "template", fluidHeight: true };
|
|
217
|
+
// Every other name (focus, component, anything future) is an inline/fit app at the router
|
|
218
|
+
// level — same branch (type:component). What the NAME renders as is the channel's and the
|
|
219
|
+
// org's apps' business (they branch on `defaultState` by name); e.g. the channel opens the
|
|
220
|
+
// focus overlay for "focus" and must NOT for "component".
|
|
221
|
+
if (declared) return { ...m, defaultState: declared, type: "component", fluidHeight: false };
|
|
222
|
+
// Legacy manifest (no authored field): infer, so new consumers read `defaultState` uniformly.
|
|
223
|
+
const inferred = m.type === "component" || m.fluidHeight === false ? "focus" : FULL_SURFACE_STATE;
|
|
224
|
+
// Derive the legacy fields here too: an unauthored manifest (the normal case now —
|
|
225
|
+
// `defaultState: app` is the default and never written) must still read as a
|
|
226
|
+
// full-surface app to every legacy `type`/`fluidHeight` consumer.
|
|
227
|
+
return inferred === FULL_SURFACE_STATE
|
|
228
|
+
? { ...m, defaultState: inferred, mode: inferred, type: "template", fluidHeight: true }
|
|
229
|
+
: { ...m, defaultState: inferred, mode: inferred, type: "component", fluidHeight: false };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** The definition folder for a ref: the shared home for components/atoms, the ref's
|
|
233
|
+
* org (or the compat scan) for templates. */
|
|
234
|
+
function findFolder(kind: "component" | "app" | "atom", ref: string): { folder: string; org?: string } | null {
|
|
235
|
+
const { org: refOrg, name } = parseRef(ref);
|
|
236
|
+
const lower = name.toLowerCase();
|
|
237
|
+
if (kind === "atom") {
|
|
238
|
+
const folder = join(SHARED_DIR.atom, lower);
|
|
239
|
+
return existsSync(folder) && statSync(folder).isDirectory() ? { folder } : null;
|
|
240
|
+
}
|
|
241
|
+
const dirs =
|
|
242
|
+
kind === "app" ? appDirs(refOrg) : resolveComponentDirs(componentDirs(refOrg), refOrg, lower);
|
|
243
|
+
for (const { org, dir } of dirs) {
|
|
244
|
+
const folder = join(dir, lower);
|
|
245
|
+
if (existsSync(folder) && statSync(folder).isDirectory()) return { folder, ...(org ? { org } : {}) };
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Load one app manifest by `<org>/<id>` (bare id = legacy compat scan). The served
|
|
251
|
+
* manifest carries its `org` (a projection from the folder location, like `states` —
|
|
252
|
+
* never hand-written); the app's URI is `unoverse://apps/<org>/<id>`. */
|
|
253
|
+
export function loadAppManifest(ref: string): AppManifest | null {
|
|
254
|
+
const hit = findFolder("app", ref);
|
|
255
|
+
if (!hit) return null;
|
|
256
|
+
const path = defPath(hit.folder, "manifest");
|
|
257
|
+
if (!path) return null;
|
|
258
|
+
const m = readDefCached<AppManifest>(path);
|
|
259
|
+
const id = parseRef(ref).name.toLowerCase();
|
|
260
|
+
// THE CONTAINER BRIDGE (UNOVERSE_INTERFACE_MODEL §4): binding, inputSchema and
|
|
261
|
+
// autoTrigger belong to a CONTAINER STATE now (the chat state that runs the advisor
|
|
262
|
+
// owns its own schema). Until per-container send routing lands, the app-level readers
|
|
263
|
+
// (the MCP app tool, live Studio) keep working by LIFTING the first workflow-bearing
|
|
264
|
+
// container state's contract when the app authored no top-level copy.
|
|
265
|
+
const containers = (m as { templates?: Record<string, { binding?: { workflow?: string; trigger?: string; autoTrigger?: boolean; inputSchema?: unknown }; workflow?: string; trigger?: string; autoTrigger?: boolean; inputSchema?: unknown; states?: Record<string, { workflow?: string; trigger?: string; autoTrigger?: boolean; inputSchema?: unknown }> }> }).templates;
|
|
266
|
+
let lifted: { binding?: { workflow?: string; trigger?: string }; inputSchema?: Record<string, unknown>; autoTrigger?: boolean } = {};
|
|
267
|
+
if (containers && !(m as { binding?: unknown }).binding) {
|
|
268
|
+
for (const c of Object.values(containers)) {
|
|
269
|
+
const carriers = [...(c.binding ? [c.binding] : []), c, ...Object.values(c.states ?? {})];
|
|
270
|
+
const w = carriers.find((x) => typeof x.workflow === "string" && x.workflow);
|
|
271
|
+
if (w) {
|
|
272
|
+
lifted = {
|
|
273
|
+
binding: { workflow: w.workflow, ...(w.trigger ? { trigger: w.trigger } : {}) },
|
|
274
|
+
...(w.inputSchema ? { inputSchema: w.inputSchema as Record<string, unknown> } : {}),
|
|
275
|
+
...(typeof w.autoTrigger === "boolean" ? { autoTrigger: w.autoTrigger } : {}),
|
|
276
|
+
};
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
// `states` is AUTO-DERIVED from the states/ folder — never hand-written in manifest.json —
|
|
282
|
+
// so the SERVED manifest (MCP `resources/read unoverse://apps/{org}/{id}`, discovery) carries
|
|
283
|
+
// the app's layer views with zero drift. The folder stays the source of truth (§7).
|
|
284
|
+
// `autoTrigger` is authored INSIDE `binding:`; lift it to the top-level field the
|
|
285
|
+
// consumers read (an explicit top-level line, the legacy spelling, still wins).
|
|
286
|
+
const bindingAuto =
|
|
287
|
+
m.autoTrigger === undefined && typeof m.binding?.autoTrigger === "boolean"
|
|
288
|
+
? { autoTrigger: m.binding.autoTrigger }
|
|
289
|
+
: {};
|
|
290
|
+
return normalizeMode({ ...lifted, ...bindingAuto, ...m, id, org: hit.org, states: listStates("app", `${hit.org}/${id}`) });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Load a COMPONENT as a native MCP app (UNOVERSE_MCP_APP_PROTOCOL §0.1 path B): a
|
|
294
|
+
* universal component folder (`rx/components/<id>`) with a `manifest.json` IS a native MCP
|
|
295
|
+
* app whose deliverable is the component itself — the server registers it right alongside
|
|
296
|
+
* template apps. No `binding` by default (calling it just renders the component + runs the
|
|
297
|
+
* `outputs` elicitation); a `binding` MAY be authored to ALSO fire a workflow (optional,
|
|
298
|
+
* binding-agnostic handler). `previewComponents` defaults to the component's own id so the
|
|
299
|
+
* app-tool handler resolves ITS `outputs` (the elicitation schema) with no handler change. */
|
|
300
|
+
export function loadComponentApp(id: string, org?: string): AppManifest | null {
|
|
301
|
+
const lower = id.toLowerCase();
|
|
302
|
+
// TWO TIERS: marketplace (`rx/components/<id>`) or, with an org, the client's own
|
|
303
|
+
// microapp (`rx/orgs/<org>/components/<id>`) — org in the address, always.
|
|
304
|
+
const home = org ? join(projectDir(org), "components") : SHARED_DIR.component;
|
|
305
|
+
const path = defPath(join(home, lower), "manifest");
|
|
306
|
+
if (!path) return null;
|
|
307
|
+
const m = readDefCached<AppManifest>(path);
|
|
308
|
+
return normalizeMode({
|
|
309
|
+
...m,
|
|
310
|
+
id: lower,
|
|
311
|
+
// Component manifests carry `title`, not `name` — default it so every consumer
|
|
312
|
+
// (tool registration, referee/mirror text "call <name> again") has a display name.
|
|
313
|
+
name: m.name ?? m.title ?? lower,
|
|
314
|
+
...(org ? { org } : {}),
|
|
315
|
+
// Default load mode = focus (interactive widgets); author `defaultState` to override.
|
|
316
|
+
defaultState: m.defaultState ?? m.mode ?? "focus",
|
|
317
|
+
// The deliverable IS this component — point the handler's outputs/elicitation lookup
|
|
318
|
+
// at it, by the QUALIFIED ref for an org component: two orgs may share the bare name,
|
|
319
|
+
// and the bare form here took every /mcp initialize down with the resolver's
|
|
320
|
+
// ambiguity error (observed live, bppunoverse 2026-08-20).
|
|
321
|
+
previewComponents: m.previewComponents?.length ? m.previewComponents : [org ? `${org}/${lower}` : lower],
|
|
322
|
+
states: listStates("component", org ? `${org}/${lower}` : lower),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Load a TEMPLATE MODEL template as a native MCP app — the third path, beside the manifest
|
|
327
|
+
* app and the component app. A template folder (`design/<org>/templates/<id>`) with a
|
|
328
|
+
* `manifest.yaml` IS an app whose deliverable is the page: the agent fills its parts in ONE
|
|
329
|
+
* call and the server renders it.
|
|
330
|
+
*
|
|
331
|
+
* IT WAS NOT ONE UNTIL 2026-08-31, and nothing said so. Templates are discovered (spatial
|
|
332
|
+
* carries their rows), read (`unoverse://templates/…` serves the definition) and SCHEMA'd
|
|
333
|
+
* (`…/schema` compiles the page's parts) — every surface but the one that answers the call.
|
|
334
|
+
* So an agent could find a page, be handed its contract, compose every part of it, and then
|
|
335
|
+
* be told the tool does not exist. Observed live: a fully composed Perfect Day, -32602 at the
|
|
336
|
+
* door. The kind travels on the manifest as `kind` so the render path loads the definition
|
|
337
|
+
* from the right tier; `type` is the legacy full-surface/inline router word and says nothing
|
|
338
|
+
* about which home a definition lives in. */
|
|
339
|
+
export function loadTemplateApp(id: string, org?: string): AppManifest | null {
|
|
340
|
+
const lower = id.toLowerCase();
|
|
341
|
+
// TWO TIERS, like components: the marketplace shelf (`design/marketplace/templates`) or
|
|
342
|
+
// an org's own (`design/<org>/templates`). A shelf template carries no org, and its tool
|
|
343
|
+
// id is the bare name.
|
|
344
|
+
const home = org ? join(projectDir(org), "templates") : SHARED_DIR.template;
|
|
345
|
+
const path = defPath(join(home, lower), "manifest");
|
|
346
|
+
if (!path) return null;
|
|
347
|
+
const m = readDefCached<AppManifest>(path);
|
|
348
|
+
return normalizeMode({
|
|
349
|
+
...m,
|
|
350
|
+
id: lower,
|
|
351
|
+
...(org ? { org } : {}),
|
|
352
|
+
// Template manifests carry `title`, never `name` (the folder is the name).
|
|
353
|
+
name: m.name ?? m.title ?? lower,
|
|
354
|
+
kind: "template",
|
|
355
|
+
// A DURABLE SURFACE by nature: a composed page is refined across turns, and every
|
|
356
|
+
// refinement must hydrate the same instance rather than stack a new page per message.
|
|
357
|
+
lifetime: m.lifetime ?? "conversation",
|
|
358
|
+
// The deliverable IS this template — the render path resolves the definition from this
|
|
359
|
+
// ref, in the tier `kind` names.
|
|
360
|
+
previewComponents: m.previewComponents?.length ? m.previewComponents : [org ? `${org}/${lower}` : lower],
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** List MCP apps — every TEMPLATE app (one org's, or all orgs') PLUS every universal
|
|
365
|
+
* COMPONENT app (a component with a manifest.json). Both are native MCP app tools.
|
|
366
|
+
*
|
|
367
|
+
* MEMOIZED ON THE TREE'S SIGNATURE, like every other catalogue read. This walk (every
|
|
368
|
+
* org's apps, components and templates, a manifest load per entry) ran on every MCP
|
|
369
|
+
* initialize, twice per anonymous MCP message, on every app open and every sitetool
|
|
370
|
+
* read, and nothing in it changes between requests except when a file is edited. The
|
|
371
|
+
* returned array is SHARED: callers read it (find/filter/map), never mutate it. */
|
|
372
|
+
export function listApps(org?: string): AppManifest[] {
|
|
373
|
+
const orgsToWalk = org != null ? [org] : listOrgs();
|
|
374
|
+
const dirs = [
|
|
375
|
+
...appDirs(org ?? null).map((d) => d.dir),
|
|
376
|
+
...(org == null ? [SHARED_DIR.component] : []),
|
|
377
|
+
...orgsToWalk.map((o) => join(projectDir(o), "components")),
|
|
378
|
+
...templateDirs(org ?? null).map((d) => d.dir),
|
|
379
|
+
];
|
|
380
|
+
return cachedBySignature(`apps:${org ?? "*"}`, dirSignature(dirs), () => walkApps(org, orgsToWalk));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function walkApps(org: string | undefined, orgsToWalk: string[]): AppManifest[] {
|
|
384
|
+
const out: AppManifest[] = [];
|
|
385
|
+
// PER-ENTRY ISOLATION (same law as listDefinitions): this list is built INSIDE MCP
|
|
386
|
+
// server construction, so one unloadable app taking the walk down means EVERY
|
|
387
|
+
// initialize on every door answers -32603 (observed live: one ambiguous bare ref
|
|
388
|
+
// dropped the whole universe, 2026-08-20). One broken app degrades to a skipped
|
|
389
|
+
// entry with a warning, never a dead platform.
|
|
390
|
+
const push = (load: () => AppManifest | null, label: string) => {
|
|
391
|
+
try {
|
|
392
|
+
const m = load();
|
|
393
|
+
if (m) out.push(m);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
console.warn(`[unoverse] skipping app '${label}' — failed to load: ${(err as Error)?.message ?? err}`);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
// APP homes, not template homes: the 2026-08-28 rename split templateDirs (the new
|
|
399
|
+
// Template Model kind) from appDirs (the org apps/ folders) — this walk kept calling
|
|
400
|
+
// templateDirs and silently lost every manifest app (bpp-chat/bpp-voice gone, no
|
|
401
|
+
// makeMCP anywhere → every anonymous lane 401'd; observed live 2026-08-28).
|
|
402
|
+
for (const { org: o, dir } of appDirs(org ?? null)) {
|
|
403
|
+
if (!existsSync(dir)) continue;
|
|
404
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
405
|
+
if (!e.isDirectory()) continue;
|
|
406
|
+
push(() => loadAppManifest(`${o}/${e.name}`), `${o}/${e.name}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// DESIGN-SYSTEM component apps carry no org — include them whenever we're listing all
|
|
410
|
+
// apps (no org filter), so the MCP server registers them as native app tools.
|
|
411
|
+
if (org == null && existsSync(SHARED_DIR.component)) {
|
|
412
|
+
for (const e of readdirSync(SHARED_DIR.component, { withFileTypes: true })) {
|
|
413
|
+
if (!e.isDirectory()) continue;
|
|
414
|
+
push(() => loadComponentApp(e.name), e.name);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// ORG component apps (`rx/orgs/<org>/components/<id>` + manifest.json) — the client's
|
|
418
|
+
// own microapps ARE apps too; without this walk their /mcp tools are never registered
|
|
419
|
+
// and a discovered org component fails with "Tool <id> not found" (observed live:
|
|
420
|
+
// PerfectDay after the two-tier move). Tool id stays the bare component id (names are
|
|
421
|
+
// unique across tiers, lint-enforced); the org rides on the manifest for addressing.
|
|
422
|
+
for (const o of orgsToWalk) {
|
|
423
|
+
const dir = join(projectDir(o), "components");
|
|
424
|
+
if (!existsSync(dir)) continue;
|
|
425
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
426
|
+
if (!e.isDirectory()) continue;
|
|
427
|
+
push(() => loadComponentApp(e.name, o), `${o}/${e.name}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// TEMPLATE MODEL templates (`design/<org>/templates/<id>`). ASKED FOR, not spelled out:
|
|
431
|
+
// `templateDirs` is the same enumerator the loader and the catalog walk, so a new org and
|
|
432
|
+
// a home that moves are covered here the moment they are covered there. Without this walk
|
|
433
|
+
// a discovered page is uncallable — the agent composes it and the door answers -32602,
|
|
434
|
+
// which is exactly what the org-component walk above was added to fix, one tier later.
|
|
435
|
+
for (const { org: o, dir } of templateDirs(org ?? null)) {
|
|
436
|
+
// The shelf (o === undefined) rides along only on an unfiltered list, exactly as the
|
|
437
|
+
// design-system component apps above do: an org-scoped door lists that org's own.
|
|
438
|
+
if (!existsSync(dir) || (!o && org != null)) continue;
|
|
439
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
440
|
+
if (!e.isDirectory()) continue;
|
|
441
|
+
push(() => loadTemplateApp(e.name, o), `${o}/${e.name}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** A layer view enumerated from a definition's `states/` folder (UNOVERSE_LAYERS.md §7:
|
|
448
|
+
* the folder IS the registry). `when` = the state's root `visibleWhen` selector, so a
|
|
449
|
+
* viewer can both LIST and ACTIVATE it (set the discriminant that shows it). */
|
|
450
|
+
export interface StateEntry {
|
|
451
|
+
name: string;
|
|
452
|
+
when?: unknown;
|
|
453
|
+
/** The state root's `select.where` (a reaction-contract surface, STATE_MODEL §5b) —
|
|
454
|
+
* the viewer activates it by writing `{field: eq}` into a component's slice. */
|
|
455
|
+
where?: unknown;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** The order states are referenced from the root `<name>` definition (its `$include:
|
|
459
|
+
* "states/…"` sequence — `Switch` cases / children order). The ROOT declares the order;
|
|
460
|
+
* reorder the includes and the list reorders. Unreferenced states fall to the end. */
|
|
461
|
+
function stateOrder(kind: "app" | "component", ref: string): string[] {
|
|
462
|
+
const lower = parseRef(ref).name.toLowerCase();
|
|
463
|
+
const hit = findFolder(kind, ref);
|
|
464
|
+
if (!hit) return [];
|
|
465
|
+
// An EXPLICIT authored order ranks FIRST (v2: the manifest `stateOrder` is the
|
|
466
|
+
// REACTION-state priority ladder — STATE_MODEL §5 rule 5 — and lists only those).
|
|
467
|
+
// States it does not name (the base arrangement's private substates, welcome/
|
|
468
|
+
// conversation) rank AFTER it, in the order the STRUCTURE includes them — the
|
|
469
|
+
// walk below — so the first included substate is the viewer's landing default.
|
|
470
|
+
const explicit: string[] = [];
|
|
471
|
+
const manifestPath = defPath(hit.folder, "manifest");
|
|
472
|
+
{
|
|
473
|
+
type TreeStates = Record<string, { states?: Record<string, unknown> } | null>;
|
|
474
|
+
const m = manifestPath ? readDefCached<{ stateOrder?: unknown; states?: TreeStates; layout?: string }>(manifestPath) : undefined;
|
|
475
|
+
// ONE FOLDER GRAMMAR (owner ruling 2026-08-29): the ENVELOPE (`<name>.yaml`)
|
|
476
|
+
// is the tree's home; the manifest's `states:` block is the legacy read.
|
|
477
|
+
const envPath = defPath(hit.folder, lower);
|
|
478
|
+
const envStates = envPath ? readDefCached<{ states?: TreeStates }>(envPath).states : undefined;
|
|
479
|
+
const states = envStates && typeof envStates === "object" && !Array.isArray(envStates) ? envStates : m?.states;
|
|
480
|
+
// THE TEMPLATE TREE ranks everything by declaration: reaction states (top-level
|
|
481
|
+
// minus the base) in order, then the base's substates in order — welcome first
|
|
482
|
+
// because it is declared first. Supersedes an authored stateOrder.
|
|
483
|
+
if (states && typeof states === "object" && !Array.isArray(states)) {
|
|
484
|
+
const names = Object.keys(states);
|
|
485
|
+
const base = defaultLayoutOf({ layout: m?.layout, states });
|
|
486
|
+
return [
|
|
487
|
+
...names.filter((n) => n !== base),
|
|
488
|
+
...Object.keys((states[base] as { states?: Record<string, unknown> } | null)?.states ?? {}),
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
if (Array.isArray(m?.stateOrder)) explicit.push(...m.stateOrder.filter((n): n is string => typeof n === "string"));
|
|
492
|
+
}
|
|
493
|
+
// The structural walk starts at the envelope when one exists; a MANIFEST-ONLY
|
|
494
|
+
// template (no envelope) starts at its default layout — that is where its
|
|
495
|
+
// states/ are actually included from (via the shared chrome), so the include
|
|
496
|
+
// order still ranks the base's substates (welcome before conversation).
|
|
497
|
+
let rootPath = defPath(hit.folder, lower);
|
|
498
|
+
if (rootPath) {
|
|
499
|
+
const rootDef = readDefCached<{ stateOrder?: unknown }>(rootPath);
|
|
500
|
+
if (!explicit.length && Array.isArray(rootDef?.stateOrder))
|
|
501
|
+
explicit.push(...rootDef.stateOrder.filter((n): n is string => typeof n === "string"));
|
|
502
|
+
} else {
|
|
503
|
+
const m2 = manifestPath ? readDefCached<{ layout?: string; states?: Record<string, { layout?: unknown } | null> }>(manifestPath) : null;
|
|
504
|
+
const dflt = m2 ? defaultLayoutOf(m2) : "main";
|
|
505
|
+
// The base state's DECLARED path wins (2026-08-29 ruling); same-name survives
|
|
506
|
+
// only for the unswept installed tier.
|
|
507
|
+
const declared = m2?.states?.[dflt];
|
|
508
|
+
const declaredPath = declared && typeof declared === "object" && typeof declared.layout === "string" ? declared.layout : undefined;
|
|
509
|
+
rootPath = (declaredPath ? defPath(hit.folder, declaredPath) : undefined) ?? defPath(join(hit.folder, "layouts"), dflt);
|
|
510
|
+
}
|
|
511
|
+
if (!rootPath) return explicit;
|
|
512
|
+
const order: string[] = [...explicit];
|
|
513
|
+
const seen = new Set<string>();
|
|
514
|
+
const visited = new Set<string>([rootPath]);
|
|
515
|
+
const walk = (v: unknown): void => {
|
|
516
|
+
if (Array.isArray(v)) return v.forEach(walk);
|
|
517
|
+
if (v && typeof v === "object") {
|
|
518
|
+
const inc = (v as { $include?: unknown }).$include;
|
|
519
|
+
if (typeof inc === "string") {
|
|
520
|
+
const m = inc.match(/^states\/(.+)$/);
|
|
521
|
+
if (m && !seen.has(m[1])) (seen.add(m[1]), order.push(m[1]));
|
|
522
|
+
// Follow NON-state includes (e.g. a switcher root's layouts/full) so the
|
|
523
|
+
// authored state order survives the layouts/ restructure — the order must
|
|
524
|
+
// come from where the Switch actually references the states.
|
|
525
|
+
if (!m) {
|
|
526
|
+
const p = defPath(hit.folder, inc);
|
|
527
|
+
if (p && !visited.has(p)) {
|
|
528
|
+
visited.add(p);
|
|
529
|
+
walk(readDefCached(p));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
Object.values(v).forEach(walk);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
walk(readDefCached(rootPath));
|
|
537
|
+
return order;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Enumerate `<kind>/<name>/states/*` → the layer views (name + selector), ordered by
|
|
541
|
+
* the root's reference sequence (`stateOrder`); the folder IS the set (§7). Empty if none. */
|
|
542
|
+
export function listStates(kind: "app" | "component", ref: string): StateEntry[] {
|
|
543
|
+
const hit = findFolder(kind, ref);
|
|
544
|
+
if (!hit) return [];
|
|
545
|
+
const order = stateOrder(kind, ref);
|
|
546
|
+
const rank = (n: string) => { const i = order.indexOf(n); return i === -1 ? Infinity : i; };
|
|
547
|
+
const dir = join(hit.folder, "states");
|
|
548
|
+
const entries: StateEntry[] = !existsSync(dir)
|
|
549
|
+
? []
|
|
550
|
+
: readdirSync(dir)
|
|
551
|
+
.filter(isDefFile)
|
|
552
|
+
.map((f) => {
|
|
553
|
+
const j = readDefCached<{ visibleWhen?: unknown; select?: { where?: unknown } }>(join(dir, f));
|
|
554
|
+
return { name: defName(f), when: j?.visibleWhen, where: j?.select?.where };
|
|
555
|
+
});
|
|
556
|
+
// TEMPLATE LAYOUTS (name-sync): each non-default layout of a multi-layout template
|
|
557
|
+
// is a first-class layer view, activated exactly like a reaction surface — put a
|
|
558
|
+
// component into the view of the layout's name. Served with that selector so the
|
|
559
|
+
// Studio's existing preview machinery seeds + activates it with zero special-casing.
|
|
560
|
+
if (kind === "app") {
|
|
561
|
+
const manifestPath = defPath(hit.folder, "manifest");
|
|
562
|
+
const manifest = manifestPath
|
|
563
|
+
? readDefCached<{ layout?: string; states?: Record<string, { states?: Record<string, unknown> } | null> }>(manifestPath)
|
|
564
|
+
: undefined;
|
|
565
|
+
// ANATOMY UNIFIED (2026-08-22): the manifest TREE is the truth; a substate's
|
|
566
|
+
// drawing lives in layouts/ like every drawing (states/ read as legacy). Each
|
|
567
|
+
// declared substate is a layer view carrying its file's own selectors.
|
|
568
|
+
// A substate's drawing is OWNER-PREFIXED in layouts/ — `<owner>-<sub>` — the
|
|
569
|
+
// course-application `step-*` convention made law (owner ruling 2026-08-22);
|
|
570
|
+
// bare and legacy states/ spellings read as fallbacks until installed
|
|
571
|
+
// universes migrate.
|
|
572
|
+
const subOwners = new Map<string, string>();
|
|
573
|
+
// ONE FOLDER GRAMMAR (2026-08-29): the envelope is the tree's home; the
|
|
574
|
+
// manifest's block is the legacy read.
|
|
575
|
+
const envPath = defPath(hit.folder, parseRef(ref).name.toLowerCase());
|
|
576
|
+
const envStates = envPath ? readDefCached<{ states?: Record<string, { states?: Record<string, unknown> } | null> }>(envPath).states : undefined;
|
|
577
|
+
const mStates = (envStates && typeof envStates === "object" && !Array.isArray(envStates) ? envStates : undefined) ?? manifest?.states;
|
|
578
|
+
if (mStates && typeof mStates === "object" && !Array.isArray(mStates))
|
|
579
|
+
for (const [owner, st] of Object.entries(mStates))
|
|
580
|
+
if (st && typeof st === "object" && st.states && typeof st.states === "object")
|
|
581
|
+
for (const sub of Object.keys(st.states)) subOwners.set(sub, owner);
|
|
582
|
+
const subNames = new Set(subOwners.keys());
|
|
583
|
+
for (const [n, owner] of subOwners) {
|
|
584
|
+
if (entries.some((e) => e.name === n)) continue;
|
|
585
|
+
const p =
|
|
586
|
+
defPath(join(hit.folder, "layouts"), `${owner}-${n}`) ??
|
|
587
|
+
defPath(join(hit.folder, "layouts"), n) ??
|
|
588
|
+
defPath(join(hit.folder, "states"), n);
|
|
589
|
+
if (!p) continue;
|
|
590
|
+
const j = readDefCached<{ visibleWhen?: unknown; select?: { where?: unknown } }>(p);
|
|
591
|
+
entries.push({ name: n, when: j?.visibleWhen, where: j?.select?.where });
|
|
592
|
+
}
|
|
593
|
+
const ldir = join(hit.folder, "layouts");
|
|
594
|
+
if (existsSync(ldir)) {
|
|
595
|
+
const names = readdirSync(ldir).filter(isDefFile).map(defName);
|
|
596
|
+
if (names.length > 1) {
|
|
597
|
+
const dflt = defaultLayoutOf({ layout: manifest?.layout, states: mStates });
|
|
598
|
+
for (const n of names)
|
|
599
|
+
if (n !== dflt && !subNames.has(n) && !entries.some((e) => e.name === n)) entries.push({ name: n, where: { field: "defaultState", eq: n } });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// STATE MODEL v2 (UNOVERSE_STATE_MODEL §5 rule 2): a component with an authored
|
|
604
|
+
// `state.view` TREE serves its PUBLIC MENU as first-class states — each activated
|
|
605
|
+
// by the public axis (`view`), in tree order. The tree is the source of truth; this
|
|
606
|
+
// is its projection, exactly like the states/ folder before it.
|
|
607
|
+
if (kind === "component") {
|
|
608
|
+
const rootPath = defPath(hit.folder, parseRef(ref).name.toLowerCase());
|
|
609
|
+
const tree = rootPath
|
|
610
|
+
? ((): { states?: Record<string, unknown> } | undefined => {
|
|
611
|
+
const raw = readDefCached<{ states?: Record<string, unknown>; state?: { view?: { states?: Record<string, unknown> } } }>(rootPath);
|
|
612
|
+
const top = raw?.states;
|
|
613
|
+
return top && typeof top === "object" && !Array.isArray(top) ? { states: top } : raw?.state?.view;
|
|
614
|
+
})()
|
|
615
|
+
: undefined;
|
|
616
|
+
if (tree && typeof tree === "object" && tree.states && typeof tree.states === "object") {
|
|
617
|
+
for (const n of Object.keys(tree.states))
|
|
618
|
+
if (!entries.some((e) => e.name === n)) entries.push({ name: n, where: { field: "state", eq: n } });
|
|
619
|
+
const treeOrder = Object.keys(tree.states);
|
|
620
|
+
const treeRank = (n: string) => { const i = treeOrder.indexOf(n); return i === -1 ? Infinity : i; };
|
|
621
|
+
return entries.sort((a, b) => treeRank(a.name) - treeRank(b.name) || rank(a.name) - rank(b.name));
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return entries.sort((a, b) => rank(a.name) - rank(b.name) || a.name.localeCompare(b.name));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Templates for the workbench — each enriched with its app `binding` (and app name)
|
|
629
|
+
* when the folder carries a manifest. The workbench uses the binding to drive the live
|
|
630
|
+
* connection (workflow + trigger), so there's no manual entry: the app decides.
|
|
631
|
+
*/
|
|
632
|
+
export function listAppsForWorkbench(org?: string): (UnoverseDefinition & {
|
|
633
|
+
binding?: AppManifest["binding"];
|
|
634
|
+
appName?: string;
|
|
635
|
+
autoTrigger?: boolean;
|
|
636
|
+
previewComponents?: string[];
|
|
637
|
+
fluidHeight?: boolean;
|
|
638
|
+
states?: StateEntry[];
|
|
639
|
+
/** The full MCP-app manifest when the template folder carries one — so the workbench
|
|
640
|
+
* can show the app's meta (description, category, version, binding, expose, schema). */
|
|
641
|
+
app?: AppManifest;
|
|
642
|
+
})[] {
|
|
643
|
+
return listDefinitions("app", org)
|
|
644
|
+
.map((def) => {
|
|
645
|
+
const ref = `${def.org}/${def.name}`; // folder id = lowercased template name, org-scoped
|
|
646
|
+
const app = loadAppManifest(ref);
|
|
647
|
+
const states = listStates("app", ref);
|
|
648
|
+
return app
|
|
649
|
+
? { ...def, states, binding: app.binding, appName: app.name, autoTrigger: app.autoTrigger, previewComponents: app.previewComponents, fluidHeight: app.fluidHeight, app }
|
|
650
|
+
: { ...def, states };
|
|
651
|
+
})
|
|
652
|
+
// Templates view shows only apps that ARE surfaces (manifest defaultState
|
|
653
|
+
// "template", or a plain layout with no manifest). Component apps (focus /
|
|
654
|
+
// component / any non-template name) are wizards etc. whose UI is their
|
|
655
|
+
// COMPONENT — designed in the Components view; their app shell is vestigial
|
|
656
|
+
// under the reaction contract (STATE_MODEL §5b). They stay fully served for
|
|
657
|
+
// discovery/launch (resources/list, /registry/spatial) — just not listed here.
|
|
658
|
+
.filter((def) => !("app" in def) || def.app?.type === "template");
|
|
659
|
+
}
|