@unoverse-platform/studio 0.1.90 → 0.1.91

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.
Files changed (130) hide show
  1. package/local/plugin.ts +20 -12
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +140 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +111 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +172 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +130 -0
  13. package/vendor/base/src/auth/auth.ts +278 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +128 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +643 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +260 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +199 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +198 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.mjs +64 -0
  53. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  54. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  55. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  56. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  57. package/vendor/base/src/logger.ts +33 -0
  58. package/vendor/base/src/manifests/compose.ts +539 -0
  59. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  60. package/vendor/base/src/manifests/executor/context.ts +107 -0
  61. package/vendor/base/src/manifests/executor/index.ts +47 -0
  62. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  63. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  64. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  65. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  66. package/vendor/base/src/manifests/executor/send.ts +65 -0
  67. package/vendor/base/src/manifests/executor/session.ts +46 -0
  68. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  69. package/vendor/base/src/manifests/index.ts +280 -0
  70. package/vendor/base/src/manifests/integrity.ts +93 -0
  71. package/vendor/base/src/manifests/rows.ts +33 -0
  72. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  73. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  74. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  75. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  76. package/vendor/base/src/manifests/runtime/channels/service.ts +125 -0
  77. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  78. package/vendor/base/src/manifests/runtime/context.ts +150 -0
  79. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  80. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  81. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  82. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  83. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  84. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  85. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  86. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  87. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  88. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  89. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  90. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  91. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  92. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  93. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  94. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  95. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  96. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  97. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  98. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  99. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  100. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +542 -0
  101. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  102. package/vendor/base/src/manifests/source.ts +187 -0
  103. package/vendor/base/src/manifests/watch.ts +67 -0
  104. package/vendor/base/src/markdown/document.ts +141 -0
  105. package/vendor/base/src/paths.ts +180 -0
  106. package/vendor/base/src/platform/audioManager.ts +100 -0
  107. package/vendor/base/src/platform/clientTransport.ts +65 -0
  108. package/vendor/base/src/platform/executionContext.ts +154 -0
  109. package/vendor/base/src/platform/index.ts +10 -0
  110. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  111. package/vendor/base/src/platform/redis.ts +54 -0
  112. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  113. package/vendor/base/src/pluginBase.ts +248 -0
  114. package/vendor/base/src/plugins/README.md +57 -0
  115. package/vendor/base/src/plugins/discovery.ts +87 -0
  116. package/vendor/base/src/plugins/index.ts +16 -0
  117. package/vendor/base/src/plugins/loader.ts +179 -0
  118. package/vendor/base/src/plugins/redis.ts +36 -0
  119. package/vendor/base/src/plugins/state.ts +53 -0
  120. package/vendor/base/src/registry.ts +95 -0
  121. package/vendor/base/src/security/permissions.ts +93 -0
  122. package/vendor/base/src/security/rules.ts +73 -0
  123. package/vendor/base/src/template/SafeExpression.ts +556 -0
  124. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  125. package/vendor/base/src/template/index.ts +22 -0
  126. package/vendor/base/src/types.ts +50 -0
  127. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  128. package/vendor/base/src/workflow/PlatformMemoryCore.ts +438 -0
  129. package/vendor/base/src/workflow/routing.ts +122 -0
  130. package/vendor/sdk/lib/core/store.ts +26 -6
@@ -0,0 +1,643 @@
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 } 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
+ export function listApps(org?: string): AppManifest[] {
367
+ const out: AppManifest[] = [];
368
+ // PER-ENTRY ISOLATION (same law as listDefinitions): this list is built INSIDE MCP
369
+ // server construction, so one unloadable app taking the walk down means EVERY
370
+ // initialize on every door answers -32603 (observed live: one ambiguous bare ref
371
+ // dropped the whole universe, 2026-08-20). One broken app degrades to a skipped
372
+ // entry with a warning, never a dead platform.
373
+ const push = (load: () => AppManifest | null, label: string) => {
374
+ try {
375
+ const m = load();
376
+ if (m) out.push(m);
377
+ } catch (err) {
378
+ console.warn(`[unoverse] skipping app '${label}' — failed to load: ${(err as Error)?.message ?? err}`);
379
+ }
380
+ };
381
+ // APP homes, not template homes: the 2026-08-28 rename split templateDirs (the new
382
+ // Template Model kind) from appDirs (the org apps/ folders) — this walk kept calling
383
+ // templateDirs and silently lost every manifest app (bpp-chat/bpp-voice gone, no
384
+ // makeMCP anywhere → every anonymous lane 401'd; observed live 2026-08-28).
385
+ for (const { org: o, dir } of appDirs(org ?? null)) {
386
+ if (!existsSync(dir)) continue;
387
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
388
+ if (!e.isDirectory()) continue;
389
+ push(() => loadAppManifest(`${o}/${e.name}`), `${o}/${e.name}`);
390
+ }
391
+ }
392
+ // DESIGN-SYSTEM component apps carry no org — include them whenever we're listing all
393
+ // apps (no org filter), so the MCP server registers them as native app tools.
394
+ if (org == null && existsSync(SHARED_DIR.component)) {
395
+ for (const e of readdirSync(SHARED_DIR.component, { withFileTypes: true })) {
396
+ if (!e.isDirectory()) continue;
397
+ push(() => loadComponentApp(e.name), e.name);
398
+ }
399
+ }
400
+ // ORG component apps (`rx/orgs/<org>/components/<id>` + manifest.json) — the client's
401
+ // own microapps ARE apps too; without this walk their /mcp tools are never registered
402
+ // and a discovered org component fails with "Tool <id> not found" (observed live:
403
+ // PerfectDay after the two-tier move). Tool id stays the bare component id (names are
404
+ // unique across tiers, lint-enforced); the org rides on the manifest for addressing.
405
+ const orgsToWalk = org != null ? [org] : listOrgs();
406
+ for (const o of orgsToWalk) {
407
+ const dir = join(projectDir(o), "components");
408
+ if (!existsSync(dir)) continue;
409
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
410
+ if (!e.isDirectory()) continue;
411
+ push(() => loadComponentApp(e.name, o), `${o}/${e.name}`);
412
+ }
413
+ }
414
+ // TEMPLATE MODEL templates (`design/<org>/templates/<id>`). ASKED FOR, not spelled out:
415
+ // `templateDirs` is the same enumerator the loader and the catalog walk, so a new org and
416
+ // a home that moves are covered here the moment they are covered there. Without this walk
417
+ // a discovered page is uncallable — the agent composes it and the door answers -32602,
418
+ // which is exactly what the org-component walk above was added to fix, one tier later.
419
+ for (const { org: o, dir } of templateDirs(org ?? null)) {
420
+ // The shelf (o === undefined) rides along only on an unfiltered list, exactly as the
421
+ // design-system component apps above do: an org-scoped door lists that org's own.
422
+ if (!existsSync(dir) || (!o && org != null)) continue;
423
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
424
+ if (!e.isDirectory()) continue;
425
+ push(() => loadTemplateApp(e.name, o), `${o}/${e.name}`);
426
+ }
427
+ }
428
+ return out;
429
+ }
430
+
431
+ /** A layer view enumerated from a definition's `states/` folder (UNOVERSE_LAYERS.md §7:
432
+ * the folder IS the registry). `when` = the state's root `visibleWhen` selector, so a
433
+ * viewer can both LIST and ACTIVATE it (set the discriminant that shows it). */
434
+ export interface StateEntry {
435
+ name: string;
436
+ when?: unknown;
437
+ /** The state root's `select.where` (a reaction-contract surface, STATE_MODEL §5b) —
438
+ * the viewer activates it by writing `{field: eq}` into a component's slice. */
439
+ where?: unknown;
440
+ }
441
+
442
+ /** The order states are referenced from the root `<name>` definition (its `$include:
443
+ * "states/…"` sequence — `Switch` cases / children order). The ROOT declares the order;
444
+ * reorder the includes and the list reorders. Unreferenced states fall to the end. */
445
+ function stateOrder(kind: "app" | "component", ref: string): string[] {
446
+ const lower = parseRef(ref).name.toLowerCase();
447
+ const hit = findFolder(kind, ref);
448
+ if (!hit) return [];
449
+ // An EXPLICIT authored order ranks FIRST (v2: the manifest `stateOrder` is the
450
+ // REACTION-state priority ladder — STATE_MODEL §5 rule 5 — and lists only those).
451
+ // States it does not name (the base arrangement's private substates, welcome/
452
+ // conversation) rank AFTER it, in the order the STRUCTURE includes them — the
453
+ // walk below — so the first included substate is the viewer's landing default.
454
+ const explicit: string[] = [];
455
+ const manifestPath = defPath(hit.folder, "manifest");
456
+ {
457
+ type TreeStates = Record<string, { states?: Record<string, unknown> } | null>;
458
+ const m = manifestPath ? readDefCached<{ stateOrder?: unknown; states?: TreeStates; layout?: string }>(manifestPath) : undefined;
459
+ // ONE FOLDER GRAMMAR (owner ruling 2026-08-29): the ENVELOPE (`<name>.yaml`)
460
+ // is the tree's home; the manifest's `states:` block is the legacy read.
461
+ const envPath = defPath(hit.folder, lower);
462
+ const envStates = envPath ? readDefCached<{ states?: TreeStates }>(envPath).states : undefined;
463
+ const states = envStates && typeof envStates === "object" && !Array.isArray(envStates) ? envStates : m?.states;
464
+ // THE TEMPLATE TREE ranks everything by declaration: reaction states (top-level
465
+ // minus the base) in order, then the base's substates in order — welcome first
466
+ // because it is declared first. Supersedes an authored stateOrder.
467
+ if (states && typeof states === "object" && !Array.isArray(states)) {
468
+ const names = Object.keys(states);
469
+ const base = defaultLayoutOf({ layout: m?.layout, states });
470
+ return [
471
+ ...names.filter((n) => n !== base),
472
+ ...Object.keys((states[base] as { states?: Record<string, unknown> } | null)?.states ?? {}),
473
+ ];
474
+ }
475
+ if (Array.isArray(m?.stateOrder)) explicit.push(...m.stateOrder.filter((n): n is string => typeof n === "string"));
476
+ }
477
+ // The structural walk starts at the envelope when one exists; a MANIFEST-ONLY
478
+ // template (no envelope) starts at its default layout — that is where its
479
+ // states/ are actually included from (via the shared chrome), so the include
480
+ // order still ranks the base's substates (welcome before conversation).
481
+ let rootPath = defPath(hit.folder, lower);
482
+ if (rootPath) {
483
+ const rootDef = readDefCached<{ stateOrder?: unknown }>(rootPath);
484
+ if (!explicit.length && Array.isArray(rootDef?.stateOrder))
485
+ explicit.push(...rootDef.stateOrder.filter((n): n is string => typeof n === "string"));
486
+ } else {
487
+ const m2 = manifestPath ? readDefCached<{ layout?: string; states?: Record<string, { layout?: unknown } | null> }>(manifestPath) : null;
488
+ const dflt = m2 ? defaultLayoutOf(m2) : "main";
489
+ // The base state's DECLARED path wins (2026-08-29 ruling); same-name survives
490
+ // only for the unswept installed tier.
491
+ const declared = m2?.states?.[dflt];
492
+ const declaredPath = declared && typeof declared === "object" && typeof declared.layout === "string" ? declared.layout : undefined;
493
+ rootPath = (declaredPath ? defPath(hit.folder, declaredPath) : undefined) ?? defPath(join(hit.folder, "layouts"), dflt);
494
+ }
495
+ if (!rootPath) return explicit;
496
+ const order: string[] = [...explicit];
497
+ const seen = new Set<string>();
498
+ const visited = new Set<string>([rootPath]);
499
+ const walk = (v: unknown): void => {
500
+ if (Array.isArray(v)) return v.forEach(walk);
501
+ if (v && typeof v === "object") {
502
+ const inc = (v as { $include?: unknown }).$include;
503
+ if (typeof inc === "string") {
504
+ const m = inc.match(/^states\/(.+)$/);
505
+ if (m && !seen.has(m[1])) (seen.add(m[1]), order.push(m[1]));
506
+ // Follow NON-state includes (e.g. a switcher root's layouts/full) so the
507
+ // authored state order survives the layouts/ restructure — the order must
508
+ // come from where the Switch actually references the states.
509
+ if (!m) {
510
+ const p = defPath(hit.folder, inc);
511
+ if (p && !visited.has(p)) {
512
+ visited.add(p);
513
+ walk(readDefCached(p));
514
+ }
515
+ }
516
+ }
517
+ Object.values(v).forEach(walk);
518
+ }
519
+ };
520
+ walk(readDefCached(rootPath));
521
+ return order;
522
+ }
523
+
524
+ /** Enumerate `<kind>/<name>/states/*` → the layer views (name + selector), ordered by
525
+ * the root's reference sequence (`stateOrder`); the folder IS the set (§7). Empty if none. */
526
+ export function listStates(kind: "app" | "component", ref: string): StateEntry[] {
527
+ const hit = findFolder(kind, ref);
528
+ if (!hit) return [];
529
+ const order = stateOrder(kind, ref);
530
+ const rank = (n: string) => { const i = order.indexOf(n); return i === -1 ? Infinity : i; };
531
+ const dir = join(hit.folder, "states");
532
+ const entries: StateEntry[] = !existsSync(dir)
533
+ ? []
534
+ : readdirSync(dir)
535
+ .filter(isDefFile)
536
+ .map((f) => {
537
+ const j = readDefCached<{ visibleWhen?: unknown; select?: { where?: unknown } }>(join(dir, f));
538
+ return { name: defName(f), when: j?.visibleWhen, where: j?.select?.where };
539
+ });
540
+ // TEMPLATE LAYOUTS (name-sync): each non-default layout of a multi-layout template
541
+ // is a first-class layer view, activated exactly like a reaction surface — put a
542
+ // component into the view of the layout's name. Served with that selector so the
543
+ // Studio's existing preview machinery seeds + activates it with zero special-casing.
544
+ if (kind === "app") {
545
+ const manifestPath = defPath(hit.folder, "manifest");
546
+ const manifest = manifestPath
547
+ ? readDefCached<{ layout?: string; states?: Record<string, { states?: Record<string, unknown> } | null> }>(manifestPath)
548
+ : undefined;
549
+ // ANATOMY UNIFIED (2026-08-22): the manifest TREE is the truth; a substate's
550
+ // drawing lives in layouts/ like every drawing (states/ read as legacy). Each
551
+ // declared substate is a layer view carrying its file's own selectors.
552
+ // A substate's drawing is OWNER-PREFIXED in layouts/ — `<owner>-<sub>` — the
553
+ // course-application `step-*` convention made law (owner ruling 2026-08-22);
554
+ // bare and legacy states/ spellings read as fallbacks until installed
555
+ // universes migrate.
556
+ const subOwners = new Map<string, string>();
557
+ // ONE FOLDER GRAMMAR (2026-08-29): the envelope is the tree's home; the
558
+ // manifest's block is the legacy read.
559
+ const envPath = defPath(hit.folder, parseRef(ref).name.toLowerCase());
560
+ const envStates = envPath ? readDefCached<{ states?: Record<string, { states?: Record<string, unknown> } | null> }>(envPath).states : undefined;
561
+ const mStates = (envStates && typeof envStates === "object" && !Array.isArray(envStates) ? envStates : undefined) ?? manifest?.states;
562
+ if (mStates && typeof mStates === "object" && !Array.isArray(mStates))
563
+ for (const [owner, st] of Object.entries(mStates))
564
+ if (st && typeof st === "object" && st.states && typeof st.states === "object")
565
+ for (const sub of Object.keys(st.states)) subOwners.set(sub, owner);
566
+ const subNames = new Set(subOwners.keys());
567
+ for (const [n, owner] of subOwners) {
568
+ if (entries.some((e) => e.name === n)) continue;
569
+ const p =
570
+ defPath(join(hit.folder, "layouts"), `${owner}-${n}`) ??
571
+ defPath(join(hit.folder, "layouts"), n) ??
572
+ defPath(join(hit.folder, "states"), n);
573
+ if (!p) continue;
574
+ const j = readDefCached<{ visibleWhen?: unknown; select?: { where?: unknown } }>(p);
575
+ entries.push({ name: n, when: j?.visibleWhen, where: j?.select?.where });
576
+ }
577
+ const ldir = join(hit.folder, "layouts");
578
+ if (existsSync(ldir)) {
579
+ const names = readdirSync(ldir).filter(isDefFile).map(defName);
580
+ if (names.length > 1) {
581
+ const dflt = defaultLayoutOf({ layout: manifest?.layout, states: mStates });
582
+ for (const n of names)
583
+ if (n !== dflt && !subNames.has(n) && !entries.some((e) => e.name === n)) entries.push({ name: n, where: { field: "defaultState", eq: n } });
584
+ }
585
+ }
586
+ }
587
+ // STATE MODEL v2 (UNOVERSE_STATE_MODEL §5 rule 2): a component with an authored
588
+ // `state.view` TREE serves its PUBLIC MENU as first-class states — each activated
589
+ // by the public axis (`view`), in tree order. The tree is the source of truth; this
590
+ // is its projection, exactly like the states/ folder before it.
591
+ if (kind === "component") {
592
+ const rootPath = defPath(hit.folder, parseRef(ref).name.toLowerCase());
593
+ const tree = rootPath
594
+ ? ((): { states?: Record<string, unknown> } | undefined => {
595
+ const raw = readDefCached<{ states?: Record<string, unknown>; state?: { view?: { states?: Record<string, unknown> } } }>(rootPath);
596
+ const top = raw?.states;
597
+ return top && typeof top === "object" && !Array.isArray(top) ? { states: top } : raw?.state?.view;
598
+ })()
599
+ : undefined;
600
+ if (tree && typeof tree === "object" && tree.states && typeof tree.states === "object") {
601
+ for (const n of Object.keys(tree.states))
602
+ if (!entries.some((e) => e.name === n)) entries.push({ name: n, where: { field: "state", eq: n } });
603
+ const treeOrder = Object.keys(tree.states);
604
+ const treeRank = (n: string) => { const i = treeOrder.indexOf(n); return i === -1 ? Infinity : i; };
605
+ return entries.sort((a, b) => treeRank(a.name) - treeRank(b.name) || rank(a.name) - rank(b.name));
606
+ }
607
+ }
608
+ return entries.sort((a, b) => rank(a.name) - rank(b.name) || a.name.localeCompare(b.name));
609
+ }
610
+
611
+ /**
612
+ * Templates for the workbench — each enriched with its app `binding` (and app name)
613
+ * when the folder carries a manifest. The workbench uses the binding to drive the live
614
+ * connection (workflow + trigger), so there's no manual entry: the app decides.
615
+ */
616
+ export function listAppsForWorkbench(org?: string): (UnoverseDefinition & {
617
+ binding?: AppManifest["binding"];
618
+ appName?: string;
619
+ autoTrigger?: boolean;
620
+ previewComponents?: string[];
621
+ fluidHeight?: boolean;
622
+ states?: StateEntry[];
623
+ /** The full MCP-app manifest when the template folder carries one — so the workbench
624
+ * can show the app's meta (description, category, version, binding, expose, schema). */
625
+ app?: AppManifest;
626
+ })[] {
627
+ return listDefinitions("app", org)
628
+ .map((def) => {
629
+ const ref = `${def.org}/${def.name}`; // folder id = lowercased template name, org-scoped
630
+ const app = loadAppManifest(ref);
631
+ const states = listStates("app", ref);
632
+ return app
633
+ ? { ...def, states, binding: app.binding, appName: app.name, autoTrigger: app.autoTrigger, previewComponents: app.previewComponents, fluidHeight: app.fluidHeight, app }
634
+ : { ...def, states };
635
+ })
636
+ // Templates view shows only apps that ARE surfaces (manifest defaultState
637
+ // "template", or a plain layout with no manifest). Component apps (focus /
638
+ // component / any non-template name) are wizards etc. whose UI is their
639
+ // COMPONENT — designed in the Components view; their app shell is vestigial
640
+ // under the reaction contract (STATE_MODEL §5b). They stay fully served for
641
+ // discovery/launch (resources/list, /registry/spatial) — just not listed here.
642
+ .filter((def) => !("app" in def) || def.app?.type === "template");
643
+ }