@olonjs/cli 3.0.96 → 3.0.98

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.
@@ -1632,6 +1632,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1632
1632
  "scripts": {
1633
1633
  "dev": "vite",
1634
1634
  "dev:clean": "vite --force",
1635
+ "verify:webmcp": "node scripts/webmcp-feature-check.mjs",
1635
1636
  "prebuild": "node scripts/sync-pages-to-public.mjs",
1636
1637
  "build": "tsc && vite build",
1637
1638
  "dist": "bash ./src2Code.sh --template alpha src .cursor vercel.json index.html tsconfig.json tsconfig.node.json vite.config.ts scripts specs package.json",
@@ -1645,7 +1646,7 @@ cat << 'END_OF_FILE_CONTENT' > "package.json"
1645
1646
  "@tiptap/extension-link": "^2.11.5",
1646
1647
  "@tiptap/react": "^2.11.5",
1647
1648
  "@tiptap/starter-kit": "^2.11.5",
1648
- "@olonjs/core": "^1.0.84",
1649
+ "@olonjs/core": "^1.0.86",
1649
1650
  "class-variance-authority": "^0.7.1",
1650
1651
  "clsx": "^2.1.1",
1651
1652
  "lucide-react": "^0.474.0",
@@ -1959,10 +1960,34 @@ import { build } from 'vite';
1959
1960
  import path from 'path';
1960
1961
  import { fileURLToPath, pathToFileURL } from 'url';
1961
1962
  import fs from 'fs/promises';
1963
+ import {
1964
+ buildPageContract,
1965
+ buildPageManifest,
1966
+ buildPageManifestHref,
1967
+ buildSiteManifest,
1968
+ } from '../../../packages/core/src/lib/webmcp-contracts.mjs';
1962
1969
 
1963
1970
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
1964
1971
  const root = path.resolve(__dirname, '..');
1965
1972
  const pagesDir = path.resolve(root, 'src/data/pages');
1973
+ const publicDir = path.resolve(root, 'public');
1974
+ const distDir = path.resolve(root, 'dist');
1975
+
1976
+ async function writeJsonTargets(relativePath, value) {
1977
+ const targets = [
1978
+ path.resolve(publicDir, relativePath),
1979
+ path.resolve(distDir, relativePath),
1980
+ ];
1981
+
1982
+ for (const targetPath of targets) {
1983
+ await fs.mkdir(path.dirname(targetPath), { recursive: true });
1984
+ await fs.writeFile(targetPath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
1985
+ }
1986
+ }
1987
+
1988
+ function escapeHtmlAttribute(value) {
1989
+ return String(value).replace(/&/g, '&amp;').replace(/"/g, '&quot;');
1990
+ }
1966
1991
 
1967
1992
  function toCanonicalSlug(relativeJsonPath) {
1968
1993
  const normalized = relativeJsonPath.replace(/\\/g, '/');
@@ -2032,7 +2057,7 @@ if (targets.length === 0) {
2032
2057
  console.log(`[bake] Targets: ${targets.map((t) => t.slug).join(', ')}`);
2033
2058
 
2034
2059
  const ssrEntryUrl = pathToFileURL(path.resolve(root, 'dist-ssr/entry-ssg.js')).href;
2035
- const { render, getCss, getPageMeta } = await import(ssrEntryUrl);
2060
+ const { render, getCss, getPageMeta, getWebMcpBuildState } = await import(ssrEntryUrl);
2036
2061
 
2037
2062
  const template = await fs.readFile(path.resolve(root, 'dist/index.html'), 'utf-8');
2038
2063
  const hasCommentMarker = template.includes('<!--app-html-->');
@@ -2043,6 +2068,33 @@ if (!hasCommentMarker && !hasRootDivMarker) {
2043
2068
 
2044
2069
  const inlinedCss = getCss();
2045
2070
  const styleTag = `<style data-bake="inline">${inlinedCss}</style>`;
2071
+ const webMcpBuildState = getWebMcpBuildState();
2072
+
2073
+ for (const { slug } of targets) {
2074
+ const pageConfig = webMcpBuildState.pages[slug];
2075
+ if (!pageConfig) continue;
2076
+ const contract = buildPageContract({
2077
+ slug,
2078
+ pageConfig,
2079
+ schemas: webMcpBuildState.schemas,
2080
+ siteConfig: webMcpBuildState.siteConfig,
2081
+ });
2082
+ await writeJsonTargets(`schemas/${slug}.schema.json`, contract);
2083
+ const pageManifest = buildPageManifest({
2084
+ slug,
2085
+ pageConfig,
2086
+ schemas: webMcpBuildState.schemas,
2087
+ siteConfig: webMcpBuildState.siteConfig,
2088
+ });
2089
+ await writeJsonTargets(buildPageManifestHref(slug).replace(/^\//, ''), pageManifest);
2090
+ }
2091
+
2092
+ const mcpManifest = buildSiteManifest({
2093
+ pages: webMcpBuildState.pages,
2094
+ schemas: webMcpBuildState.schemas,
2095
+ siteConfig: webMcpBuildState.siteConfig,
2096
+ });
2097
+ await writeJsonTargets('mcp-manifest.json', mcpManifest);
2046
2098
 
2047
2099
  for (const { slug, out, depth } of targets) {
2048
2100
  console.log(`\n[bake] Rendering /${slug === 'home' ? '' : slug}...`);
@@ -2056,12 +2108,26 @@ for (const { slug, out, depth } of targets) {
2056
2108
  `<meta property="og:title" content="${safeTitle}">`,
2057
2109
  `<meta property="og:description" content="${safeDescription}">`,
2058
2110
  ].join('\n ');
2111
+ const jsonLd = JSON.stringify({
2112
+ '@context': 'https://schema.org',
2113
+ '@type': 'WebPage',
2114
+ name: title,
2115
+ description,
2116
+ url: slug === 'home' ? '/' : `/${slug}`,
2117
+ });
2059
2118
 
2060
2119
  const prefix = depth > 0 ? '../'.repeat(depth) : './';
2061
2120
  const fixedTemplate = depth > 0 ? template.replace(/(['"])\.\//g, `$1${prefix}`) : template;
2121
+ const mcpManifestHref = `${prefix}${buildPageManifestHref(slug).replace(/^\//, '')}`;
2122
+ const contractHref = `${prefix}schemas/${slug}.schema.json`;
2123
+ const contractLinks = [
2124
+ `<link rel="mcp-manifest" href="${escapeHtmlAttribute(mcpManifestHref)}">`,
2125
+ `<link rel="olon-contract" href="${escapeHtmlAttribute(contractHref)}">`,
2126
+ `<script type="application/ld+json">${jsonLd}</script>`,
2127
+ ].join('\n ');
2062
2128
 
2063
2129
  let bakedHtml = fixedTemplate
2064
- .replace('</head>', ` ${styleTag}\n</head>`)
2130
+ .replace('</head>', ` ${styleTag}\n ${contractLinks}\n</head>`)
2065
2131
  .replace(/<title>.*?<\/title>/, `<title>${safeTitle}</title>\n ${metaTags}`);
2066
2132
 
2067
2133
  if (hasCommentMarker) {
@@ -2103,1604 +2169,920 @@ fs.cpSync(sourceDir, targetDir, { recursive: true });
2103
2169
  console.log('[sync-pages-to-public] Synced pages to public/pages');
2104
2170
 
2105
2171
  END_OF_FILE_CONTENT
2106
- mkdir -p "specs"
2107
- echo "Creating specs/olonjsSpecs_V.1.3.md..."
2108
- cat << 'END_OF_FILE_CONTENT' > "specs/olonjsSpecs_V.1.3.md"
2109
- # 📐 OlonJS Architecture Specifications v1.3
2110
-
2111
- **Status:** Mandatory Standard
2112
- **Version:** 1.3.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing)
2113
- **Target:** Senior Architects / AI Agents / Enterprise Governance
2114
-
2115
- **Scope v1.3:** This edition preserves the complete v1.2 architecture (MTRP, JSP, TBP, CIP, ECIP, JAP + Studio/ICE UX contract: IDAC, TOCC, BSDS, ASC, JEB + Tenant Type & Code-Generation Annex) as a **faithful superset**, and adds strict path-based/nested-array behavior for Studio selection and Inspector expansion.
2116
- **Scope note (breaking):** In strict v1.3 Studio semantics, the legacy flat protocol (`itemField` / `itemId`) is removed in favor of `itemPath` (root-to-leaf path segments).
2117
-
2118
- ---
2119
-
2120
- ## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2
2121
-
2122
- **Objective:** Establish a strictly typed, open-ended protocol for extending content data structures where the **Core Engine** is the orchestrator and the **Tenant** is the provider.
2123
-
2124
- ### 1.1 The Sovereign Dependency Inversion
2125
- The **Core** defines the empty `SectionDataRegistry`. The **Tenant** "injects" its specific definitions using **Module Augmentation**. This allows the Core to be distributed as a compiled NPM package while remaining aware of Tenant-specific types at compile-time.
2126
-
2127
- ### 1.2 Technical Implementation (`@olonjs/core/kernel`)
2128
- ```typescript
2129
- export interface SectionDataRegistry {} // Augmented by Tenant
2130
- export interface SectionSettingsRegistry {} // Augmented by Tenant
2131
-
2132
- export interface BaseSection<K extends keyof SectionDataRegistry> {
2133
- id: string;
2134
- type: K;
2135
- data: SectionDataRegistry[K];
2136
- settings?: K extends keyof SectionSettingsRegistry
2137
- ? SectionSettingsRegistry[K]
2138
- : BaseSectionSettings;
2139
- }
2140
-
2141
- export type Section = {
2142
- [K in keyof SectionDataRegistry]: BaseSection<K>
2143
- }[keyof SectionDataRegistry];
2144
- ```
2145
-
2146
- **SectionType:** Core exports (or Tenant infers) **`SectionType`** as **`keyof SectionDataRegistry`**. After Tenant module augmentation, this is the union of all section type keys (e.g. `'header' | 'footer' | 'hero' | ...`). The Tenant uses this type for the ComponentRegistry and SECTION_SCHEMAS keys.
2147
-
2148
- **Perché servono:** Il Core deve poter renderizzare section senza conoscere i tipi concreti a compile-time; il Tenant deve poter aggiungere nuovi tipi senza modificare il Core. I registry vuoti + module augmentation permettono di distribuire Core come pacchetto NPM e mantenere type-safety end-to-end (Section, registry, config). Senza MTRP, ogni nuovo tipo richiederebbe cambi nel Core o tipi deboli (`any`).
2149
-
2150
- ---
2151
-
2152
- ## 2. 📐 JsonPages Site Protocol (JSP) v1.8
2153
-
2154
- **Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).
2155
-
2156
- ### 2.1 The File System Ontology (The Silo Contract)
2157
- Every site must reside in an isolated directory. Global Governance is physically separated from Local Content.
2158
- * **`/config/site.json`** — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.
2159
- * **`/config/menu.json`** — Navigation Tree (SSOT for System Header). See Appendix A.
2160
- * **`/config/theme.json`** — Theme tokens (optional but recommended). See Appendix A.
2161
- * **`/pages/[slug].json`** — Local Body Content per page. See Appendix A (PageConfig).
2162
-
2163
- **Application path convention:** The runtime app typically imports these via an alias (e.g. **`@/data/config/`** and **`@/data/pages/`**). The physical silo may be `src/data/config/` and `src/data/pages/` so that `site.json`, `menu.json`, `theme.json` live under `src/data/config/`, and page JSONs under `src/data/pages/`. The CLI or projection script may use `/config/` and `/pages/` at repo root; the **contract** is that the app receives **siteConfig**, **menuConfig**, **themeConfig**, and **pages** as defined in JEB (§10) and Appendix A.
2164
-
2165
- ### 2.2 Deterministic Projection (CLI Workflow)
2166
- The CLI (`@olonjs/cli`) creates new tenants by:
2167
- 1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).
2168
- 2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).
2169
- 3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.
2170
-
2171
- **Perché servono:** Una struttura file deterministica (config vs pages) separa governance globale (site, menu, theme) dal contenuto per pagina; il CLI può rigenerare tenant e tooling può trovare dati e schemi sempre negli stessi path. Senza JSP, ogni tenant sarebbe una struttura ad hoc e ingestione/export/Bake sarebbero fragili.
2172
-
2173
- ---
2174
-
2175
- ## 3. 🧱 Tenant Block Protocol (TBP) v1.0
2176
-
2177
- **Objective:** Standardize the "Capsule" structure for components to enable automated ingestion (Pull) by the SaaS.
2178
-
2179
- ### 3.1 The Atomic Capsule Structure
2180
- Components are self-contained directories under **`src/components/<sectionType>/`**:
2181
- * **`View.tsx`** — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).
2182
- * **`schema.ts`** — Zod schema(s) for the **data** contract (and optionally **settings**). Exports at least one schema (e.g. `HeroSchema`) used as the **data** schema for that type. Must extend BaseSectionData (§8) for data; array items must extend BaseArrayItem (§8).
2183
- * **`types.ts`** — TypeScript interfaces inferred from the schema (e.g. `HeroData`, `HeroSettings`). Export types with names **`<SectionType>Data`** and **`<SectionType>Settings`** (or equivalent) so the Tenant can aggregate them in a single types module.
2184
- * **`index.ts`** — Public API: re-exports View, schema(s), and types.
2185
-
2186
- ### 3.2 Reserved System Types
2187
- * **`type: 'header'`** — Reserved for `site.json`. Receives **`menu: MenuItem[]`** in addition to `data` and `settings`. Menu is sourced from `menu.json` (see Appendix A). The Tenant **must** type `SectionComponentPropsMap['header']` as `{ data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] }`.
2188
- * **`type: 'footer'`** — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).
2189
- * **`type: 'sectionHeader'`** — A standard local block. Must define its own `links` array in its local schema if used.
2190
-
2191
- **Perché servono:** La capsula (View + schema + types + index) è l’unità di estensione: il Core e il Form Factory possono scoprire tipi e contratti per tipo senza convenzioni ad hoc. Header/footer riservati evitano conflitti tra globale e locale. Senza TBP, aggregazione di SECTION_SCHEMAS e registry sarebbe incoerente e l’ingestion da SaaS non sarebbe automatizzabile.
2192
-
2193
- ---
2194
-
2195
- ## 4. 🧱 Component Implementation Protocol (CIP) v1.5
2196
-
2197
- **Objective:** Ensure system-wide stability and Admin UI integrity.
2198
-
2199
- 1. **The "Sovereign View" Law:** Components receive `data` and `settings` (and `menu` for header only) and return JSX. They are metadata-blind (never import Zod schemas).
2200
- 2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.
2201
- 3. **Agnostic Asset Protocol:** Use `resolveAssetUrl(path, tenantId)` for all media. Resolved URLs are under **`/assets/...`** with no tenantId segment in the path (e.g. relative `img/hero.jpg` → `/assets/img/hero.jpg`).
2202
-
2203
- ### 4.4 Local Design Tokens (v1.2)
2204
- Section Views that control their own background, text, borders, or radii **shall** define a **local scope** via an inline `style` object on the section root: e.g. `--local-bg`, `--local-text`, `--local-text-muted`, `--local-surface`, `--local-border`, `--local-radius-lg`, `--local-accent`, mapped to theme variables. All Tailwind classes that affect color or radius in that section **must** use these variables (e.g. `bg-[var(--local-bg)]`, `text-[var(--local-text)]`). No naked utilities (e.g. `bg-blue-500`). An optional **`label`** in section data may be rendered with class **`jp-section-label`** for overlay type labels.
2205
-
2206
- ### 4.5 Z-Index & Overlay Governance (v1.2)
2207
- Section content root **must** stay at **`z-index` ≤ 1** (prefer `z-0`) so the Sovereign Overlay can sit above with high z-index in Tenant CSS (§7). Header/footer may use a higher z-index (e.g. 50) only as a documented exception for global chrome.
2208
-
2209
- **Perché servono (CIP):** View “dumb” (solo data/settings) e senza import di Zod evita accoppiamento e permette al Form Factory di essere l’unica fonte di verità sugli schemi. Z-index basso evita che il contenuto copra l’overlay di selezione in Studio. Asset via `resolveAssetUrl`: i path relativi vengono risolti in `/assets/...` (senza segmento tenantId nel path). Token locali (`--local-*`) rendono le section temabili e coerenti con overlay e tema; senza, stili “nudi” creano drift visivo e conflitti con l’UI di editing.
2210
-
2211
- ---
2212
-
2213
- ## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5
2214
-
2215
- **Objective:** Standardize the Polymorphic ICE engine.
2216
-
2217
- 1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.
2218
- 2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.
2219
- 3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.
2220
-
2221
- ### 5.4 UI Metadata Vocabulary (v1.2)
2222
- Standard keys for the Form Factory:
2223
-
2224
- | Key | Use case |
2225
- |-----|----------|
2226
- | `ui:text` | Single-line text input. |
2227
- | `ui:textarea` | Multi-line text. |
2228
- | `ui:select` | Enum / single choice. |
2229
- | `ui:number` | Numeric input. |
2230
- | `ui:list` | Array of items; list editor (add/remove/reorder). |
2231
- | `ui:icon-picker` | Icon selection. |
2232
-
2233
- Unknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.
2234
-
2235
- ### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)
2236
- In strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.
2237
-
2238
- ```typescript
2239
- export type SelectionPathSegment = { fieldKey: string; itemId?: string };
2240
- export type SelectionPath = SelectionPathSegment[];
2241
- ```
2242
-
2243
- Rules:
2244
- * Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.
2245
- * Matching by `fieldKey` alone is non-compliant for nested structures.
2246
- * Legacy flat payload fields **`itemField`** and **`itemId`** are removed from strict v1.3 selection protocol.
2247
-
2248
- **Perché servono (ECIP):** Il Form Factory deve sapere quale widget usare (text, textarea, select, list, …) senza hardcodare per tipo; `.describe('ui:...')` è il contratto. BaseArrayItem con `id` su ogni item di array garantisce chiavi stabili in React e reorder/delete corretti nell’Inspector. In v1.3 la selezione/espansione path-only elimina ambiguità su array annidati: senza path completo root→leaf, la sidebar può aprire il ramo sbagliato o non aprire il target.
2249
-
2250
- ---
2251
-
2252
- ## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1
2253
-
2254
- **Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.
2255
-
2256
- ### 6.1 Section-Level Markup (Core-Provided)
2257
- **SectionRenderer** (Core) wraps each section root with:
2258
- * **`data-section-id`** — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.
2259
- * Sibling overlay element **`data-jp-section-overlay`** — Selection ring and type label. **Tenant does not add this;** Core injects it.
2260
-
2261
- Tenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.
2262
-
2263
- ### 6.2 Field-Level Binding (Tenant-Provided)
2264
- For every **editable scalar field** the View **must** attach **`data-jp-field="<fieldKey>"`** (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).
2265
-
2266
- ### 6.3 Array-Item Binding (Tenant-Provided)
2267
- For every **editable array item** the View **must** attach:
2268
- * **`data-jp-item-id="<stableId>"`** — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.
2269
- * **`data-jp-item-field="<arrayKey>"`** — e.g. `cards`, `layers`, `products`, `paragraphs`.
2270
-
2271
- ### 6.4 Compliance
2272
- **Reserved types** (`header`, `footer`): ICE attributes optional unless Studio edits them. **All other section types** in the Stage and in `SECTION_SCHEMAS` **must** implement §6.2 and §6.3 for every editable field and array item.
2273
-
2274
- ### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)
2275
- For nested array targets, the Core/Inspector contract is path-based:
2276
- * The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).
2277
- * Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.
2278
- * In strict mode, index-based identity fallback is non-compliant for editable object arrays.
2279
-
2280
- **Perché servono (IDAC):** Lo Stage è in un iframe e l’Inspector deve sapere **quale campo o item** corrisponde al click (o alla selezione) senza conoscere la struttura DOM del Tenant. **`data-jp-field`** associa un nodo DOM al path dello schema (es. `title`, `description`): così il Core può evidenziare la riga giusta nella sidebar, applicare opacità attivo/inattivo e aprire il form sul campo corretto. **`data-jp-item-id`** e **`data-jp-item-field`** fanno lo stesso per gli item di array (liste, reorder, delete). In v1.3, `itemPath` rende deterministico anche il caso nested (array dentro array), eliminando mismatch tra selezione canvas e ramo aperto in sidebar.
2281
-
2282
- ---
2283
-
2284
- ## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0
2285
-
2286
- **Objective:** The Stage iframe loads only Tenant HTML/CSS. Core injects overlay **markup** but does **not** ship overlay styles. The Tenant **must** supply CSS so overlay is visible.
2287
-
2288
- ### 7.1 Required Selectors (Tenant global CSS)
2289
- 1. **`[data-jp-section-overlay]`** — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.
2290
- 2. **`[data-section-id]:hover [data-jp-section-overlay]`** — Hover: e.g. dashed border, subtle tint.
2291
- 3. **`[data-section-id][data-jp-selected] [data-jp-section-overlay]`** — Selected: solid border, optional tint.
2292
- 4. **`[data-jp-section-overlay] > div`** (type label) — Position and visibility (e.g. visible on hover/selected).
2293
-
2294
- ### 7.2 Z-Index
2295
- Overlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).
2296
-
2297
- ### 7.3 Responsibility
2298
- **Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.
2299
-
2300
- **Perché servono (TOCC):** L’iframe dello Stage carica solo HTML/CSS del Tenant; il Core inietta il markup dell’overlay ma non gli stili. Senza CSS Tenant per i selettori TOCC, bordo hover/selected e type label non sarebbero visibili: l’autore non vedrebbe quale section è selezionata né il label del tipo. TOCC chiarisce la responsabilità (Core = markup, Tenant = aspetto) e garantisce UX uniforme tra tenant.
2301
-
2302
- ---
2303
-
2304
- ## 8. 📦 Base Section Data & Settings (BSDS) v1.0
2305
-
2306
- **Objective:** Standardize base schema fragments for anchors, array items, and section settings.
2307
-
2308
- ### 8.1 BaseSectionData
2309
- Every section data schema **must** extend a base with at least **`anchorId`** (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):
2310
-
2311
- ```typescript
2312
- export const BaseSectionData = z.object({
2313
- anchorId: z.string().optional().describe('ui:text'),
2314
- });
2315
- ```
2316
-
2317
- ### 8.2 BaseArrayItem
2318
- Every array item schema editable in the Inspector **must** include **`id`** (optional string minimum). Canonical Zod:
2319
-
2320
- ```typescript
2321
- export const BaseArrayItem = z.object({
2322
- id: z.string().optional(),
2323
- });
2324
- ```
2325
-
2326
- Recommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.
2327
-
2328
- ### 8.3 BaseSectionSettings (Optional)
2329
- Common section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):
2330
-
2331
- ```typescript
2332
- export const BaseSectionSettingsSchema = z.object({
2333
- paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2334
- paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2335
- theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
2336
- container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
2337
- });
2338
- ```
2339
-
2340
- Capsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.
2341
-
2342
- **Perché servono (BSDS):** anchorId permette deep-link e navigazione in-page; id sugli array item è necessario per `data-jp-item-id`, reorder e React reconciliation. BaseSectionSettings comuni (padding, theme, container) evitano ripetizione e allineano il Form Factory tra capsule. Senza base condivisi, ogni capsule inventa convenzioni e validazione/add-section diventano fragili.
2343
-
2344
- ---
2345
-
2346
- ## 9. 📌 AddSectionConfig (ASC) v1.0
2347
-
2348
- **Objective:** Formalize the "Add Section" contract used by the Studio.
2349
-
2350
- **Type (Core exports `AddSectionConfig`):**
2351
- ```typescript
2352
- interface AddSectionConfig {
2353
- addableSectionTypes: readonly string[];
2354
- sectionTypeLabels: Record<string, string>;
2355
- getDefaultSectionData(sectionType: string): Record<string, unknown>;
2356
- }
2357
- ```
2358
-
2359
- **Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:
2360
- * **`addableSectionTypes`** — Readonly array of section type keys. Only these types appear in the Add Section Library. Must be a subset of (or equal to) the keys in SectionDataRegistry.
2361
- * **`sectionTypeLabels`** — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).
2362
- * **`getDefaultSectionData(sectionType: string): Record<string, unknown>`** — Returns default `data` for a new section. Must conform to the capsule’s data schema so the new section validates.
2363
-
2364
- Core creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.
2365
-
2366
- **Perché servono (ASC):** Lo Studio deve mostrare una libreria “Aggiungi sezione” con nomi leggibili e, alla scelta, creare una section con dati iniziali validi. addableSectionTypes, sectionTypeLabels e getDefaultSectionData sono il contratto: il Tenant è l’unica fonte di verità su quali tipi sono addabili e con quali default. Senza ASC, il Core non saprebbe cosa mostrare in modal né come popolare i dati della nuova section.
2367
-
2368
- ---
2369
-
2370
- ## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1
2371
-
2372
- **Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.
2373
-
2374
- ### 10.1 JsonPagesConfig (required fields)
2375
- The Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:
2376
-
2377
- | Field | Type | Description |
2378
- |-------|------|-------------|
2379
- | **tenantId** | string | Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are **`/assets/...`** with no tenantId segment in the path. |
2380
- | **registry** | `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` | Component registry. Must match MTRP keys. See Appendix A. |
2381
- | **schemas** | `Record<SectionType, ZodType>` or equivalent | SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. |
2382
- | **pages** | `Record<string, PageConfig>` | Slug → page config. See Appendix A. |
2383
- | **siteConfig** | SiteConfig | Global site (identity, header/footer blocks). See Appendix A. |
2384
- | **themeConfig** | ThemeConfig | Theme tokens. See Appendix A. |
2385
- | **menuConfig** | MenuConfig | Navigation tree (SSOT for header menu). See Appendix A. |
2386
- | **themeCss** | `{ tenant: string }` | At least **tenant**: string (inline CSS or URL) for Stage iframe injection. |
2387
- | **addSection** | AddSectionConfig | Add-section config (§9). |
2388
-
2389
- Core may define optional fields. The Tenant must not omit required fields.
2390
-
2391
- ### 10.2 JsonPagesEngine
2392
- Root component: **`<JsonPagesEngine config={config} />`**. Responsibilities: route → page, SectionRenderer per section; in Studio mode Sovereign Shell (Inspector, Control Bar, postMessage); section wrappers and overlay per IDAC and JAP. Tenant does not implement the Shell.
2393
-
2394
- ### 10.3 Studio Selection Event Contract (v1.3, breaking)
2395
- In strict v1.3 Studio, section selection payload for nested targets is path-based:
2396
-
2397
- ```typescript
2398
- type SectionSelectMessage = {
2399
- type: 'SECTION_SELECT';
2400
- section: { id: string; type: string; scope: 'global' | 'local' };
2401
- itemPath?: SelectionPath; // root -> leaf
2402
- };
2403
- ```
2404
-
2405
- Removed from strict protocol:
2406
- * `itemField`
2407
- * `itemId`
2408
-
2409
- **Perché servono (JEB):** Un unico punto di bootstrap (config + Engine) evita che il Tenant replichi logica di routing, Shell e overlay. I campi obbligatori in JsonPagesConfig (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection) sono il minimo per far funzionare rendering, Studio e Form Factory; omissioni causano errori a runtime. In v1.3, il payload `itemPath` sincronizza in modo non ambiguo Stage e Inspector su nested arrays.
2410
-
2411
- ---
2412
-
2413
- # 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2
2414
-
2415
- **Status:** Mandatory Standard
2416
- **Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)
2417
- **Objective:** Deterministic orchestration of the "Studio" environment (ICE Level 1).
2418
-
2419
- ---
2420
-
2421
- ## 1. The Sovereign Shell Topology
2422
- The Admin interface is a **Sovereign Shell** from `@olonjs/core`.
2423
- 1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).
2424
- 2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.
2425
- 3. **The Control Bar:** Save, Export, Add Section.
2426
-
2427
- ## 2. State Orchestration & Persistence
2428
- * **Working Draft:** Reactive local state for unsaved changes.
2429
- * **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.
2430
- * **Bake Protocol:** "Bake HTML" requests snapshot from Iframe, injects `ProjectState` as JSON, triggers download.
2431
-
2432
- ## 3. Context Switching (Global vs. Local)
2433
- * **Header/Footer** selection → Global Mode, `site.json`.
2434
- * Any other section → Page Mode, current `[slug].json`.
2435
-
2436
- ## 4. Section Lifecycle Management
2437
- 1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).
2438
- 2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.
2439
- 3. **Delete:** Confirmation; remove from array, clear selection.
2440
-
2441
- ## 5. Stage Isolation & Overlay
2442
- * **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.
2443
- * **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).
2444
-
2445
- ## 6. "Green Build" Validation
2446
- Studio enforces `tsc && vite build`. No export with TypeScript errors.
2447
-
2448
- ## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)
2449
- * Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.
2450
- * Sidebar expansion state for nested arrays must be derived from all path segments.
2451
- * Flat-only matching may open/close wrong branches and is non-compliant in strict mode.
2452
-
2453
- **Perché servono (JAP):** Stage in iframe + Inspector + Control Bar separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Bake ed Export richiedono uno stato coerente. Global vs Page mode evita confusione su dove si sta editando (site.json vs [slug].json). Add/Reorder/Delete sono gestiti in un solo modo (Working Draft + ASC). Green Build garantisce che ciò che si esporta compili. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.
2454
-
2455
- ---
2456
-
2457
- ## Compliance: Legacy vs Full UX (v1.3)
2458
-
2459
- | Dimension | Legacy / Less UX | Full UX (Core-aligned) |
2460
- |-----------|-------------------|-------------------------|
2461
- | **ICE binding** | No `data-jp-*`; Inspector cannot bind. | IDAC (§6) on every editable section/field/item. |
2462
- | **Section wrapper** | Plain `<section>`; no overlay contract. | Core wrapper + overlay; Tenant CSS per TOCC (§7). |
2463
- | **Design tokens** | Raw BEM / fixed classes. | Local tokens (§4.4); `var(--local-*)` only. |
2464
- | **Base schemas** | Ad hoc. | BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. |
2465
- | **Add Section** | Ad hoc defaults. | ASC (§9): addableSectionTypes, labels, getDefaultSectionData. |
2466
- | **Bootstrap** | Implicit. | JEB (§10): JsonPagesConfig + JsonPagesEngine. |
2467
- | **Selection payload** | Flat `itemField/itemId`. | Path-only `itemPath: SelectionPath` (JEB §10.3). |
2468
- | **Nested array expansion** | Single-segment or field-only heuristics. | Root-to-leaf path expansion (ECIP §5.5, JAP §7). |
2469
- | **Array item identity (strict)** | Index fallback tolerated. | Stable `id` required for editable object arrays. |
2470
-
2471
- **Rule:** Every page section (non-header/footer) that appears in the Stage and in `SECTION_SCHEMAS` must comply with §6, §7, §4.4, §8, §9, §10 for full Studio UX.
2472
-
2473
- ---
2474
-
2475
- ## Summary of v1.3 Additions
2476
-
2477
- | § | Title | Purpose |
2478
- |---|--------|--------|
2479
- | 5.5 | Path-Only Nested Selection & Expansion | ECIP: root→leaf `SelectionPath`; remove flat matching in strict mode. |
2480
- | 6.5 | Strict Path Extraction for Nested Arrays | IDAC: path-based nested targeting; no strict flat fallback. |
2481
- | 10.3 | Studio Selection Event Contract | JEB: `SECTION_SELECT` uses `itemPath`; remove `itemField/itemId`. |
2482
- | JAP §7 | Path-Deterministic Selection & Sidebar Expansion | Studio state synchronization for nested arrays. |
2483
- | Compliance | Legacy vs Full UX (v1.3) | Explicit breaking delta for flat protocol removal and strict IDs. |
2484
- | **Appendix A.6** | **v1.3 Path/Nested Strictness Addendum** | Type/export and migration checklist for path-only protocol. |
2485
-
2486
- ---
2487
-
2488
- # Appendix A — Tenant Type & Code-Generation Annex
2489
-
2490
- **Objective:** Make the specification **sufficient** to generate or audit a full tenant (new site, new components, new data) without a reference codebase. Defines TypeScript types, JSON shapes, schema contract, file paths, and integration pattern.
2491
-
2492
- **Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.
2493
-
2494
- ---
2495
-
2496
- ## A.1 Core-Provided Types (from `@olonjs/core`)
2497
-
2498
- The following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.
2499
-
2500
- | Type | Description |
2501
- |------|-------------|
2502
- | **SectionType** | `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. |
2503
- | **Section** | Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. |
2504
- | **BaseSectionSettings** | Optional base type for section settings (may align with BSDS §8.3). |
2505
- | **MenuItem** | Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). |
2506
- | **AddSectionConfig** | See §9. |
2507
- | **JsonPagesConfig** | See §10.1. |
2508
-
2509
- **Perché servono (A.1):** Il Tenant deve conoscere i tipi esportati dal Core (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig) per tipizzare registry, config e augmentation senza dipendere da implementazioni interne.
2510
-
2511
- ---
2512
-
2513
- ## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)
2514
-
2515
- The Tenant **must** define the following in one module (e.g. **`src/types.ts`**). This module **must** perform the **module augmentation** of `@olonjs/core` for **SectionDataRegistry** and **SectionSettingsRegistry**, and **must** export **SectionComponentPropsMap** and re-export from `@olonjs/core` so that **SectionType** is available after augmentation.
2516
-
2517
- ### A.2.1 SectionComponentPropsMap
2518
-
2519
- Maps each section type to the props of its React component. **Header** is the only type that receives **menu**.
2520
-
2521
- **Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.
2522
-
2523
- ```typescript
2524
- import type { MenuItem } from '@olonjs/core';
2525
- // Import Data/Settings from each capsule.
2526
-
2527
- export type SectionComponentPropsMap = {
2528
- 'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
2529
- 'footer': { data: FooterData; settings?: FooterSettings };
2530
- 'hero': { data: HeroData; settings?: HeroSettings };
2531
- // ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.
2532
- };
2533
- ```
2534
-
2535
- **Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**
2536
-
2537
- ```typescript
2538
- import type { MenuItem } from '@olonjs/core';
2539
-
2540
- export type SectionComponentPropsMap = {
2541
- [K in SectionType]: K extends 'header'
2542
- ? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }
2543
- : { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };
2544
- };
2545
- ```
2546
-
2547
- SectionType is imported from Core (after Tenant augmentation). In practice Option A is the reference pattern; Option B is valid if the Tenant prefers a single derived definition.
2548
-
2549
- **Perché servono (A.2):** SectionComponentPropsMap e i tipi di config (PageConfig, SiteConfig, MenuConfig, ThemeConfig) definiscono il contratto tra dati (JSON, API) e componente; l’augmentation è l’unico modo per estendere i registry del Core senza fork. Senza questi tipi, generazione tenant e refactor sarebbero senza guida e il type-check fallirebbe.
2550
-
2551
- ### A.2.2 ComponentRegistry type
2552
-
2553
- The registry object **must** be typed as:
2554
-
2555
- ```typescript
2556
- import type { SectionType } from '@olonjs/core';
2557
- import type { SectionComponentPropsMap } from '@/types';
2558
-
2559
- export const ComponentRegistry: {
2560
- [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
2561
- } = { /* ... */ };
2562
- ```
2563
-
2564
- File: **`src/lib/ComponentRegistry.tsx`** (or equivalent). Imports one View per section type and assigns it to the corresponding key.
2565
-
2566
- ### A.2.3 PageConfig
2567
-
2568
- Minimum shape for a single page (used in **pages** and in each **`[slug].json`**):
2569
-
2570
- ```typescript
2571
- export interface PageConfig {
2572
- id?: string;
2573
- slug: string;
2574
- meta?: {
2575
- title?: string;
2576
- description?: string;
2577
- };
2578
- sections: Section[];
2579
- }
2580
- ```
2581
-
2582
- **Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.
2583
-
2584
- ### A.2.4 SiteConfig
2585
-
2586
- Minimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):
2587
-
2588
- ```typescript
2589
- export interface SiteConfigIdentity {
2590
- title?: string;
2591
- logoUrl?: string;
2592
- }
2593
-
2594
- export interface SiteConfig {
2595
- identity?: SiteConfigIdentity;
2596
- pages?: Array<{ slug: string; label: string }>;
2597
- header: {
2598
- id: string;
2599
- type: 'header';
2600
- data: HeaderData;
2601
- settings?: HeaderSettings;
2602
- };
2603
- footer: {
2604
- id: string;
2605
- type: 'footer';
2606
- data: FooterData;
2607
- settings?: FooterSettings;
2608
- };
2609
- }
2610
- ```
2611
-
2612
- **HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.
2613
-
2614
- ### A.2.5 MenuConfig
2615
-
2616
- Minimum shape for **menu.json** (and for **menuConfig** in JsonPagesConfig). Structure is tenant-defined; Core expects the header to receive **MenuItem[]**. Common pattern: an object with a key (e.g. **main**) whose value is **MenuItem[]**.
2617
-
2618
- ```typescript
2619
- export interface MenuConfig {
2620
- main?: MenuItem[];
2621
- [key: string]: MenuItem[] | undefined;
2622
- }
2623
- ```
2624
-
2625
- Or simply **`MenuItem[]`** if the app uses a single flat list. The Tenant must ensure that the value passed to the header component as **menu** conforms to **MenuItem[]** (e.g. `menuConfig.main` or `menuConfig` if it is the array).
2626
-
2627
- ### A.2.6 ThemeConfig
2628
-
2629
- Minimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). Tenant-defined; typically tokens for colors, typography, radius.
2630
-
2631
- ```typescript
2632
- export interface ThemeConfig {
2633
- name?: string;
2634
- tokens?: {
2635
- colors?: Record<string, string>;
2636
- typography?: Record<string, string | Record<string, string>>;
2637
- borderRadius?: Record<string, string>;
2638
- };
2639
- [key: string]: unknown;
2640
- }
2641
- ```
2642
-
2643
- ---
2644
-
2645
- ## A.3 Schema Contract (SECTION_SCHEMAS)
2646
-
2647
- **Location:** **`src/lib/schemas.ts`** (or equivalent).
2648
-
2649
- **Contract:**
2650
- * **SECTION_SCHEMAS** is a **single object** whose keys are **SectionType** and whose values are **Zod schemas for the section data** (not settings, unless the Form Factory contract expects a combined or per-type settings schema; then each value may be the data schema only, and settings may be defined per capsule and aggregated elsewhere if needed).
2651
- * The Tenant **must** re-export **BaseSectionData**, **BaseArrayItem**, and optionally **BaseSectionSettingsSchema** from **`src/lib/base-schemas.ts`** (or equivalent). Each capsule’s data schema **must** extend BaseSectionData; each array item schema **must** extend or include BaseArrayItem.
2652
- * **SECTION_SCHEMAS** is typed as **`Record<SectionType, ZodType>`** or **`{ [K in SectionType]: ZodType }`** so that keys match the registry and SectionDataRegistry.
2653
-
2654
- **Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.
2655
-
2656
- **Perché servono (A.3):** Un unico oggetto SECTION_SCHEMAS con chiavi = SectionType e valori = schema data permette al Form Factory di costruire form per tipo senza convenzioni ad hoc; i base schema garantiscono anchorId e id su item. Senza questo contratto, l’Inspector non saprebbe quali campi mostrare né come validare.
2657
-
2658
- ---
2659
-
2660
- ## A.4 File Paths & Data Layout
2661
-
2662
- | Purpose | Path (conventional) | Description |
2663
- |---------|---------------------|-------------|
2664
- | Site config | **`src/data/config/site.json`** | SiteConfig (identity, header, footer, pages list). |
2665
- | Menu config | **`src/data/config/menu.json`** | MenuConfig (e.g. main nav). |
2666
- | Theme config | **`src/data/config/theme.json`** | ThemeConfig (tokens). |
2667
- | Page data | **`src/data/pages/<slug>.json`** | One file per page; content is PageConfig (slug, meta, sections). |
2668
- | Base schemas | **`src/lib/base-schemas.ts`** | BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. |
2669
- | Schema aggregate | **`src/lib/schemas.ts`** | SECTION_SCHEMAS; re-exports base schemas. |
2670
- | Registry | **`src/lib/ComponentRegistry.tsx`** | ComponentRegistry object. |
2671
- | Add-section config | **`src/lib/addSectionConfig.ts`** | addSectionConfig (AddSectionConfig). |
2672
- | Tenant types & augmentation | **`src/types.ts`** | SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from Core. |
2673
- | Bootstrap | **`src/App.tsx`** | Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders **<JsonPagesEngine config={config} />**. |
2674
-
2675
- The app entry (e.g. **main.tsx**) renders **App**. No other bootstrap contract is specified; the Tenant may use Vite aliases (e.g. **@/**) for the paths above.
2676
-
2677
- **Perché servono (A.4):** Path fissi (data/config, data/pages, lib/schemas, types.ts, App.tsx) permettono a CLI, tooling e agenti di trovare sempre gli stessi file; l’onboarding e la generazione da spec sono deterministici. Senza convenzione, ogni tenant sarebbe una struttura diversa.
2678
-
2679
- ---
2680
-
2681
- ## A.5 Integration Checklist (Code-Generation)
2682
-
2683
- When generating or auditing a tenant, ensure the following in order:
2684
-
2685
- 1. **Capsules** — For each section type, create **`src/components/<type>/`** with View.tsx, schema.ts, types.ts, index.ts. Data schema extends BaseSectionData; array items extend BaseArrayItem; View complies with CIP and IDAC (§6.2–6.3 for non-reserved types).
2686
- 2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).
2687
- 3. **types.ts** — Define SectionComponentPropsMap (header with **menu**), PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** and augment SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`.
2688
- 4. **ComponentRegistry** — Import every View; build object **{ [K in SectionType]: ViewComponent }**; type as **{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }**.
2689
- 5. **schemas.ts** — Import base schemas and each capsule’s data schema; export SECTION_SCHEMAS as **{ [K in SectionType]: SchemaK }**; export SectionType as **keyof typeof SECTION_SCHEMAS** if not using Core’s SectionType.
2690
- 6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.
2691
- 7. **App.tsx** — Import site, theme, menu, pages from data paths; build config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss: { tenant }, addSection); render JsonPagesEngine.
2692
- 8. **Data files** — Create or update site.json, menu.json, theme.json, and one or more **<slug>.json** under the paths in A.4. Ensure JSON shapes match SiteConfig, MenuConfig, ThemeConfig, PageConfig.
2693
- 9. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible.
2694
- 10. **Reserved types** — Header and footer capsules receive props per SectionComponentPropsMap; menu is populated from menuConfig (e.g. menuConfig.main) when building the config or inside Core when rendering the header.
2695
-
2696
- **Perché servono (A.5):** La checklist in ordine evita di dimenticare passi (es. augmentation prima del registry, TOCC dopo le View) e rende la spec sufficiente per generare o verificare un tenant senza codebase di riferimento.
2697
-
2698
- ---
2699
-
2700
- ## A.6 v1.3 Path/Nested Strictness Addendum (breaking)
2701
-
2702
- This addendum extends Appendix A without removing prior v1.2 obligations:
2703
-
2704
- 1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.
2705
- 2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.
2706
- 3. **Nested array compliance** — For editable object arrays, item identity must be stable (`id`) and propagated to DOM attributes (`data-jp-item-id`), schema items (BaseArrayItem), and selection path segments (`itemId` when segment targets array item).
2707
- 4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.
2708
-
2709
- ---
2710
-
2711
- **Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3 path types for Studio selection).
2712
- **Distribution:** Core via `.yalc`; tenant projections via `@olonjs/cli`. This annex makes the spec **necessary and sufficient** for tenant code-generation and governance at enterprise grade.
2172
+ echo "Creating scripts/webmcp-feature-check.mjs..."
2173
+ cat << 'END_OF_FILE_CONTENT' > "scripts/webmcp-feature-check.mjs"
2174
+ import fs from 'fs/promises';
2175
+ import path from 'path';
2176
+ import { fileURLToPath } from 'url';
2177
+ import { createRequire } from 'module';
2713
2178
 
2714
- END_OF_FILE_CONTENT
2715
- mkdir -p "src"
2716
- echo "Creating src/App.tsx..."
2717
- cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
2718
- /**
2719
- * Thin Entry Point (Tenant).
2720
- * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
2721
- * Supports Hybrid Persistence: Local Filesystem (Dev) or Cloud Bridge (Prod).
2722
- */
2723
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2724
- import { JsonPagesEngine } from '@olonjs/core';
2725
- import type { JsonPagesConfig, LibraryImageEntry, ProjectState } from '@olonjs/core';
2726
- import { ComponentRegistry } from '@/lib/ComponentRegistry';
2727
- import { SECTION_SCHEMAS } from '@/lib/schemas';
2728
- import { addSectionConfig } from '@/lib/addSectionConfig';
2729
- import { getHydratedData } from '@/lib/draftStorage';
2730
- import type { SiteConfig, ThemeConfig, MenuConfig, PageConfig } from '@/types';
2731
- import type { DeployPhase, StepId } from '@/types/deploy';
2732
- import { DEPLOY_STEPS } from '@/lib/deploySteps';
2733
- import { startCloudSaveStream } from '@/lib/cloudSaveStream';
2734
- import siteData from '@/data/config/site.json';
2735
- import themeData from '@/data/config/theme.json';
2736
- import menuData from '@/data/config/menu.json';
2737
- import { getFilePages } from '@/lib/getFilePages';
2738
- import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
2739
- import { Skeleton } from '@/components/ui/skeleton';
2740
- import { ThemeProvider } from '@/components/ThemeProvider';
2179
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
2180
+ const rootDir = path.resolve(__dirname, '..');
2181
+ const baseUrl = process.env.WEBMCP_BASE_URL ?? 'http://127.0.0.1:4173';
2741
2182
 
2742
- import tenantCss from './index.css?inline';
2183
+ function pageFilePathFromSlug(slug) {
2184
+ return path.resolve(rootDir, 'src', 'data', 'pages', `${slug}.json`);
2185
+ }
2743
2186
 
2744
- // Cloud Configuration (Injected by Vercel/Netlify Env Vars)
2745
- const CLOUD_API_URL =
2746
- import.meta.env.VITE_OLONJS_CLOUD_URL ?? import.meta.env.VITE_JSONPAGES_CLOUD_URL;
2747
- const CLOUD_API_KEY =
2748
- import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
2187
+ function adminUrlFromSlug(slug) {
2188
+ return `${baseUrl}/admin${slug === 'home' ? '' : `/${slug}`}`;
2189
+ }
2749
2190
 
2750
- const themeConfig = themeData as unknown as ThemeConfig;
2751
- const menuConfig: MenuConfig = { main: [] };
2752
- const refDocuments = {
2753
- 'menu.json': menuData,
2754
- 'config/menu.json': menuData,
2755
- 'src/data/config/menu.json': menuData,
2756
- } satisfies NonNullable<JsonPagesConfig['refDocuments']>;
2757
- const TENANT_ID = 'alpha';
2191
+ function isStringSchema(schema) {
2192
+ if (!schema || typeof schema !== 'object') return false;
2193
+ if (schema.type === 'string') return true;
2194
+ if (Array.isArray(schema.anyOf)) {
2195
+ return schema.anyOf.some((entry) => entry && typeof entry === 'object' && entry.type === 'string');
2196
+ }
2197
+ return false;
2198
+ }
2758
2199
 
2759
- const filePages = getFilePages();
2760
- const fileSiteConfig = siteData as unknown as SiteConfig;
2761
- const MAX_UPLOAD_SIZE_BYTES = 5 * 1024 * 1024;
2762
- const ASSET_UPLOAD_MAX_RETRIES = 2;
2763
- const ASSET_UPLOAD_TIMEOUT_MS = 20_000;
2764
- const ALLOWED_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif']);
2200
+ function findTopLevelStringField(sectionSchema) {
2201
+ const properties = sectionSchema?.properties;
2202
+ if (!properties || typeof properties !== 'object') return null;
2203
+ const preferred = ['title', 'sectionTitle', 'label', 'headline', 'name'];
2204
+ for (const key of preferred) {
2205
+ if (isStringSchema(properties[key])) return key;
2206
+ }
2207
+ for (const [key, value] of Object.entries(properties)) {
2208
+ if (isStringSchema(value)) return key;
2209
+ }
2210
+ return null;
2211
+ }
2765
2212
 
2766
- interface CloudSaveUiState {
2767
- isOpen: boolean;
2768
- phase: DeployPhase;
2769
- currentStepId: StepId | null;
2770
- doneSteps: StepId[];
2771
- progress: number;
2772
- errorMessage?: string;
2773
- deployUrl?: string;
2774
- }
2775
-
2776
- type ContentMode = 'cloud' | 'error';
2777
- type ContentStatus = 'ok' | 'empty_namespace' | 'legacy_fallback';
2778
-
2779
- type ContentResponse = {
2780
- ok?: boolean;
2781
- siteConfig?: unknown;
2782
- pages?: unknown;
2783
- items?: unknown;
2784
- error?: string;
2785
- code?: string;
2786
- correlationId?: string;
2787
- contentStatus?: ContentStatus;
2788
- usedUnscopedFallback?: boolean;
2789
- namespace?: string;
2790
- namespaceMatchedKeys?: number;
2791
- };
2792
-
2793
- type CachedCloudContent = {
2794
- keyFingerprint: string;
2795
- savedAt: number;
2796
- siteConfig: unknown | null;
2797
- pages: Record<string, unknown>;
2798
- };
2799
-
2800
- const CLOUD_CACHE_KEY = 'jp_cloud_content_cache_v1';
2801
- const CLOUD_CACHE_TTL_MS = 5 * 60 * 1000;
2802
-
2803
- function normalizeApiBase(raw: string): string {
2804
- return raw.trim().replace(/\/+$/, '');
2805
- }
2806
-
2807
- function buildApiCandidates(raw: string): string[] {
2808
- const base = normalizeApiBase(raw);
2809
- const withApi = /\/api\/v1$/i.test(base) ? base : `${base}/api/v1`;
2810
- const candidates = [withApi, base];
2811
- return Array.from(new Set(candidates.filter(Boolean)));
2213
+ async function loadPlaywright() {
2214
+ const require = createRequire(import.meta.url);
2215
+ try {
2216
+ return require('playwright');
2217
+ } catch (error) {
2218
+ const message = error instanceof Error ? error.message : String(error);
2219
+ throw new Error(
2220
+ `Playwright is required for WebMCP verification. Install it before running this script. Original error: ${message}`
2221
+ );
2222
+ }
2812
2223
  }
2813
2224
 
2814
- function getInitialData() {
2815
- return getHydratedData(TENANT_ID, filePages, fileSiteConfig);
2225
+ async function readPageJson(slug) {
2226
+ const pageFilePath = pageFilePathFromSlug(slug);
2227
+ const raw = await fs.readFile(pageFilePath, 'utf8');
2228
+ return { raw, json: JSON.parse(raw), pageFilePath };
2816
2229
  }
2817
2230
 
2818
- function getInitialCloudSaveUiState(): CloudSaveUiState {
2819
- return {
2820
- isOpen: false,
2821
- phase: 'idle',
2822
- currentStepId: null,
2823
- doneSteps: [],
2824
- progress: 0,
2825
- };
2231
+ async function waitFor(predicate, timeoutMs, label) {
2232
+ const startedAt = Date.now();
2233
+ for (;;) {
2234
+ const result = await predicate();
2235
+ if (result) return result;
2236
+ if (Date.now() - startedAt > timeoutMs) {
2237
+ throw new Error(`Timed out while waiting for ${label}.`);
2238
+ }
2239
+ await new Promise((resolve) => setTimeout(resolve, 150));
2240
+ }
2826
2241
  }
2827
2242
 
2828
- function stepProgress(doneSteps: StepId[]): number {
2829
- return Math.round((doneSteps.length / DEPLOY_STEPS.length) * 100);
2243
+ async function waitForFileFieldValue(slug, sectionId, fieldKey, expectedValue) {
2244
+ await waitFor(async () => {
2245
+ const { json } = await readPageJson(slug);
2246
+ const section = Array.isArray(json.sections)
2247
+ ? json.sections.find((item) => item?.id === sectionId)
2248
+ : null;
2249
+ return section?.data?.[fieldKey] === expectedValue;
2250
+ }, 8_000, `file field "${fieldKey}" = "${expectedValue}"`);
2830
2251
  }
2831
2252
 
2832
- function isObjectRecord(value: unknown): value is Record<string, unknown> {
2833
- return typeof value === 'object' && value !== null;
2834
- }
2253
+ async function ensureResponseOk(response, label) {
2254
+ if (!response.ok) {
2255
+ const text = await response.text();
2256
+ throw new Error(`${label} failed with ${response.status}: ${text}`);
2257
+ }
2258
+ return response;
2259
+ }
2260
+
2261
+ async function fetchJson(relativePath, label) {
2262
+ const response = await ensureResponseOk(await fetch(`${baseUrl}${relativePath}`), label);
2263
+ return response.json();
2264
+ }
2265
+
2266
+ async function selectTarget() {
2267
+ const siteIndex = await fetchJson('/mcp-manifest.json', 'Manifest index request');
2268
+ const requestedSlug = typeof process.env.WEBMCP_TARGET_SLUG === 'string' && process.env.WEBMCP_TARGET_SLUG.trim()
2269
+ ? process.env.WEBMCP_TARGET_SLUG.trim()
2270
+ : null;
2271
+
2272
+ const candidatePages = requestedSlug
2273
+ ? (siteIndex.pages ?? []).filter((page) => page?.slug === requestedSlug)
2274
+ : (siteIndex.pages ?? []);
2275
+
2276
+ for (const pageEntry of candidatePages) {
2277
+ if (!pageEntry?.slug || !pageEntry?.manifestHref || !pageEntry?.contractHref) continue;
2278
+ const pageManifest = await fetchJson(pageEntry.manifestHref, `Page manifest request for ${pageEntry.slug}`);
2279
+ const pageContract = await fetchJson(pageEntry.contractHref, `Page contract request for ${pageEntry.slug}`);
2280
+ const localInstances = Array.isArray(pageContract.sectionInstances)
2281
+ ? pageContract.sectionInstances.filter((section) => section?.scope === 'local')
2282
+ : [];
2283
+ const tools = Array.isArray(pageManifest.tools) ? pageManifest.tools : [];
2284
+
2285
+ for (const tool of tools) {
2286
+ const sectionType = tool?.sectionType;
2287
+ if (typeof tool?.name !== 'string' || typeof sectionType !== 'string') continue;
2288
+ const targetInstance = localInstances.find((section) => section?.type === sectionType);
2289
+ if (!targetInstance?.id) continue;
2290
+ const targetFieldKey = findTopLevelStringField(pageContract.sectionSchemas?.[sectionType]);
2291
+ if (!targetFieldKey) continue;
2292
+ const pageState = await readPageJson(pageEntry.slug);
2293
+ const section = Array.isArray(pageState.json.sections)
2294
+ ? pageState.json.sections.find((item) => item?.id === targetInstance.id)
2295
+ : null;
2296
+ const originalValue = section?.data?.[targetFieldKey];
2297
+ if (typeof originalValue !== 'string') continue;
2298
+
2299
+ return {
2300
+ slug: pageEntry.slug,
2301
+ manifestHref: pageEntry.manifestHref,
2302
+ contractHref: pageEntry.contractHref,
2303
+ toolName: tool.name,
2304
+ sectionId: targetInstance.id,
2305
+ fieldKey: targetFieldKey,
2306
+ originalValue,
2307
+ originalState: pageState,
2308
+ };
2309
+ }
2310
+ }
2835
2311
 
2836
- function asString(value: unknown, fallback: string): string {
2837
- return typeof value === 'string' && value.trim() ? value : fallback;
2312
+ throw new Error(
2313
+ requestedSlug
2314
+ ? `No valid WebMCP verification target found for page "${requestedSlug}".`
2315
+ : 'No valid WebMCP verification target found in manifest index.'
2316
+ );
2838
2317
  }
2839
2318
 
2840
- function normalizeRouteSlug(value: string): string {
2841
- return value
2842
- .toLowerCase()
2843
- .replace(/[^a-z0-9/_-]/g, '-')
2844
- .replace(/^\/+|\/+$/g, '') || 'home';
2845
- }
2319
+ async function main() {
2320
+ const { chromium } = await loadPlaywright();
2321
+ const target = await selectTarget();
2322
+ const nextValue = `${target.originalValue} WebMCP ${Date.now()}`;
2323
+ const browser = await chromium.launch({ headless: true });
2324
+ const page = await browser.newPage();
2325
+ const consoleEvents = [];
2326
+ let mutationApplied = false;
2327
+
2328
+ page.on('console', (message) => {
2329
+ if (message.type() === 'error' || message.type() === 'warning') {
2330
+ consoleEvents.push(`[console:${message.type()}] ${message.text()}`);
2331
+ }
2332
+ });
2333
+ page.on('pageerror', (error) => {
2334
+ consoleEvents.push(`[pageerror] ${error.message}`);
2335
+ });
2846
2336
 
2847
- function coercePageConfig(slug: string, value: unknown): PageConfig | null {
2848
- let input = value;
2849
- if (typeof input === 'string') {
2337
+ const restoreOriginal = async () => {
2850
2338
  try {
2851
- input = JSON.parse(input) as unknown;
2339
+ await page.evaluate(
2340
+ async ({ toolName, slug, sectionId, fieldKey, value }) => {
2341
+ const runtime = navigator.modelContextTesting;
2342
+ if (!runtime?.executeTool) return;
2343
+ await runtime.executeTool(
2344
+ toolName,
2345
+ JSON.stringify({
2346
+ slug,
2347
+ sectionId,
2348
+ fieldKey,
2349
+ value,
2350
+ })
2351
+ );
2352
+ },
2353
+ {
2354
+ toolName: target.toolName,
2355
+ slug: target.slug,
2356
+ sectionId: target.sectionId,
2357
+ fieldKey: target.fieldKey,
2358
+ value: target.originalValue,
2359
+ }
2360
+ );
2361
+ await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, target.originalValue);
2852
2362
  } catch {
2853
- return null;
2363
+ await fs.writeFile(target.originalState.pageFilePath, target.originalState.raw, 'utf8');
2854
2364
  }
2855
- }
2856
- if (!isObjectRecord(input) || !Array.isArray(input.sections)) return null;
2365
+ };
2857
2366
 
2858
- const inputMeta = isObjectRecord(input.meta) ? input.meta : {};
2859
- const normalizedSlug = asString(input.slug, slug);
2860
- const normalizedId = asString(input.id, `${normalizedSlug}-page`);
2861
- const title = asString(inputMeta.title, normalizedSlug);
2862
- const description = asString(inputMeta.description, '');
2367
+ try {
2368
+ const pageManifest = await fetchJson(target.manifestHref, `Manifest request for ${target.slug}`);
2369
+ if (!Array.isArray(pageManifest.tools) || !pageManifest.tools.some((tool) => tool?.name === target.toolName)) {
2370
+ throw new Error(`Manifest does not expose ${target.toolName}.`);
2371
+ }
2863
2372
 
2864
- return {
2865
- id: normalizedId,
2866
- slug: normalizedSlug,
2867
- meta: { title, description },
2868
- sections: input.sections as PageConfig['sections'],
2869
- ...(typeof input['global-header'] === 'boolean' ? { 'global-header': input['global-header'] } : {}),
2870
- };
2871
- }
2373
+ const pageContract = await fetchJson(target.contractHref, `Contract request for ${target.slug}`);
2374
+ if (!Array.isArray(pageContract.tools) || !pageContract.tools.some((tool) => tool?.name === target.toolName)) {
2375
+ throw new Error(`Page contract does not expose ${target.toolName}.`);
2376
+ }
2377
+
2378
+ await page.goto(adminUrlFromSlug(target.slug), { waitUntil: 'networkidle' });
2872
2379
 
2873
- function coerceSiteConfig(value: unknown): SiteConfig | null {
2874
- let input = value;
2875
- if (typeof input === 'string') {
2876
2380
  try {
2877
- input = JSON.parse(input) as unknown;
2878
- } catch {
2879
- return null;
2381
+ await page.waitForFunction(
2382
+ ({ manifestHref, contractHref }) => {
2383
+ const manifestLink = document.head.querySelector('link[rel="mcp-manifest"]');
2384
+ const contractLink = document.head.querySelector('link[rel="olon-contract"]');
2385
+ return manifestLink?.getAttribute('href') === manifestHref
2386
+ && contractLink?.getAttribute('href') === contractHref;
2387
+ },
2388
+ { manifestHref: target.manifestHref, contractHref: target.contractHref },
2389
+ { timeout: 10_000 }
2390
+ );
2391
+ } catch (error) {
2392
+ const diagnostics = await page.evaluate(() => ({
2393
+ head: document.head.innerHTML,
2394
+ bodyText: document.body.innerText,
2395
+ }));
2396
+ throw new Error(
2397
+ [
2398
+ error instanceof Error ? error.message : String(error),
2399
+ `head=${diagnostics.head}`,
2400
+ `body=${diagnostics.bodyText}`,
2401
+ ...consoleEvents,
2402
+ ].join('\n')
2403
+ );
2880
2404
  }
2881
- }
2882
- if (!isObjectRecord(input)) return null;
2883
- if (!isObjectRecord(input.identity)) return null;
2884
- if (!Array.isArray(input.pages)) return null;
2885
2405
 
2886
- return input as unknown as SiteConfig;
2887
- }
2406
+ const toolNames = await page.evaluate(() => {
2407
+ const runtime = navigator.modelContextTesting;
2408
+ return runtime?.listTools?.().map((tool) => tool.name) ?? [];
2409
+ });
2410
+ if (!toolNames.includes(target.toolName)) {
2411
+ throw new Error(`Runtime did not register ${target.toolName}. Found: ${toolNames.join(', ')}`);
2412
+ }
2888
2413
 
2889
- function toPagesRecord(value: unknown): Record<string, PageConfig> | null {
2890
- const directPage = coercePageConfig('home', value);
2891
- if (directPage) {
2892
- const directSlug = normalizeRouteSlug(asString(directPage.slug, 'home'));
2893
- return { [directSlug]: { ...directPage, slug: directSlug } };
2894
- }
2414
+ const rawResult = await page.evaluate(
2415
+ async ({ toolName, slug, sectionId, fieldKey, value }) => {
2416
+ const runtime = navigator.modelContextTesting;
2417
+ if (!runtime?.executeTool) {
2418
+ throw new Error('navigator.modelContextTesting.executeTool is unavailable.');
2419
+ }
2420
+ return runtime.executeTool(
2421
+ toolName,
2422
+ JSON.stringify({
2423
+ slug,
2424
+ sectionId,
2425
+ fieldKey,
2426
+ value,
2427
+ })
2428
+ );
2429
+ },
2430
+ {
2431
+ toolName: target.toolName,
2432
+ slug: target.slug,
2433
+ sectionId: target.sectionId,
2434
+ fieldKey: target.fieldKey,
2435
+ value: nextValue,
2436
+ }
2437
+ );
2895
2438
 
2896
- if (!isObjectRecord(value)) return null;
2897
- const next: Record<string, PageConfig> = {};
2898
- for (const [rawKey, payload] of Object.entries(value)) {
2899
- const rawKeyTrimmed = rawKey.trim();
2900
- const slugFromNamespacedKey = rawKeyTrimmed.match(/^t_[a-z0-9-]+_page_(.+)$/i)?.[1];
2901
- const slug = normalizeRouteSlug(slugFromNamespacedKey ?? rawKeyTrimmed);
2902
- const page = coercePageConfig(slug, payload);
2903
- if (!page) continue;
2904
- next[slug] = { ...page, slug };
2439
+ const parsedResult = JSON.parse(rawResult);
2440
+ if (parsedResult?.isError) {
2441
+ throw new Error(`WebMCP tool returned an error: ${rawResult}`);
2442
+ }
2443
+
2444
+ mutationApplied = true;
2445
+ await waitForFileFieldValue(target.slug, target.sectionId, target.fieldKey, nextValue);
2446
+ await page.frameLocator('iframe').getByText(nextValue, { exact: true }).waitFor({ state: 'attached' });
2447
+
2448
+ console.log(
2449
+ JSON.stringify({
2450
+ ok: true,
2451
+ slug: target.slug,
2452
+ manifestHref: target.manifestHref,
2453
+ contractHref: target.contractHref,
2454
+ toolName: target.toolName,
2455
+ sectionId: target.sectionId,
2456
+ fieldKey: target.fieldKey,
2457
+ toolNames,
2458
+ })
2459
+ );
2460
+ } finally {
2461
+ if (mutationApplied) {
2462
+ await restoreOriginal();
2463
+ }
2464
+ await browser.close();
2905
2465
  }
2906
- return next;
2907
2466
  }
2908
2467
 
2909
- function normalizePageRegistry(value: unknown): Record<string, PageConfig> {
2910
- if (!isObjectRecord(value)) return {};
2911
- const normalized: Record<string, PageConfig> = {};
2468
+ main().catch((error) => {
2469
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
2470
+ process.exit(1);
2471
+ });
2912
2472
 
2913
- for (const [registrySlug, rawPageValue] of Object.entries(value)) {
2914
- const canonicalSlug = normalizeRouteSlug(registrySlug);
2915
- const direct = coercePageConfig(canonicalSlug, rawPageValue);
2916
- if (direct) {
2917
- // Canonical key comes from registry/path, not from page JSON internal slug.
2918
- normalized[canonicalSlug] = { ...direct, slug: canonicalSlug };
2919
- continue;
2920
- }
2473
+ END_OF_FILE_CONTENT
2474
+ mkdir -p "specs"
2475
+ echo "Creating specs/olonjsSpecs_V.1.3.md..."
2476
+ cat << 'END_OF_FILE_CONTENT' > "specs/olonjsSpecs_V.1.3.md"
2477
+ # 📐 OlonJS Architecture Specifications v1.3
2921
2478
 
2922
- const nested = toPagesRecord(rawPageValue);
2923
- if (nested && Object.keys(nested).length > 0) {
2924
- Object.assign(normalized, nested);
2925
- }
2926
- }
2479
+ **Status:** Mandatory Standard
2480
+ **Version:** 1.3.0 (Sovereign Core Edition Architecture + Studio/ICE UX, Path-Deterministic Nested Editing)
2481
+ **Target:** Senior Architects / AI Agents / Enterprise Governance
2927
2482
 
2928
- return normalized;
2929
- }
2483
+ **Scope v1.3:** This edition preserves the complete v1.2 architecture (MTRP, JSP, TBP, CIP, ECIP, JAP + Studio/ICE UX contract: IDAC, TOCC, BSDS, ASC, JEB + Tenant Type & Code-Generation Annex) as a **faithful superset**, and adds strict path-based/nested-array behavior for Studio selection and Inspector expansion.
2484
+ **Scope note (breaking):** In strict v1.3 Studio semantics, the legacy flat protocol (`itemField` / `itemId`) is removed in favor of `itemPath` (root-to-leaf path segments).
2930
2485
 
2931
- function extractContentSources(payload: ContentResponse | Record<string, unknown>): {
2932
- pagesSource: unknown;
2933
- siteSource: unknown;
2934
- } {
2935
- // Canonical contract: { pages, siteConfig }
2936
- if (isObjectRecord(payload) && isObjectRecord(payload.pages)) {
2937
- return { pagesSource: payload.pages, siteSource: payload.siteConfig };
2938
- }
2486
+ ---
2939
2487
 
2940
- // Edge public JSON contract: { digest, updatedAt, items: { ... } }
2941
- if (isObjectRecord(payload) && isObjectRecord(payload.items)) {
2942
- const items = payload.items;
2943
- let siteSource: unknown = null;
2944
- const pageEntries: Record<string, unknown> = {};
2945
- for (const [key, value] of Object.entries(items)) {
2946
- if (/(_config_site|config_site|config:site)$/i.test(key)) {
2947
- siteSource = value;
2948
- continue;
2949
- }
2950
- if (/(_page_|^page_|page:)/i.test(key)) {
2951
- pageEntries[key] = value;
2952
- }
2953
- }
2954
- return { pagesSource: pageEntries, siteSource };
2955
- }
2488
+ ## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2
2956
2489
 
2957
- // Raw map fallback: treat payload object itself as page map.
2958
- return { pagesSource: payload, siteSource: null };
2959
- }
2490
+ **Objective:** Establish a strictly typed, open-ended protocol for extending content data structures where the **Core Engine** is the orchestrator and the **Tenant** is the provider.
2960
2491
 
2961
- type CloudLoadFailure = {
2962
- reasonCode: string;
2963
- message: string;
2964
- correlationId?: string;
2965
- };
2492
+ ### 1.1 The Sovereign Dependency Inversion
2493
+ The **Core** defines the empty `SectionDataRegistry`. The **Tenant** "injects" its specific definitions using **Module Augmentation**. This allows the Core to be distributed as a compiled NPM package while remaining aware of Tenant-specific types at compile-time.
2966
2494
 
2967
- function isCloudLoadFailure(value: unknown): value is CloudLoadFailure {
2968
- return (
2969
- isObjectRecord(value) &&
2970
- typeof value.reasonCode === 'string' &&
2971
- typeof value.message === 'string'
2972
- );
2973
- }
2495
+ ### 1.2 Technical Implementation (`@olonjs/core/kernel`)
2496
+ ```typescript
2497
+ export interface SectionDataRegistry {} // Augmented by Tenant
2498
+ export interface SectionSettingsRegistry {} // Augmented by Tenant
2974
2499
 
2975
- function toCloudLoadFailure(value: unknown): CloudLoadFailure {
2976
- if (isCloudLoadFailure(value)) return value;
2977
- if (value instanceof Error) {
2978
- return { reasonCode: 'CLOUD_LOAD_FAILED', message: value.message };
2979
- }
2980
- return { reasonCode: 'CLOUD_LOAD_FAILED', message: 'Cloud content unavailable.' };
2500
+ export interface BaseSection<K extends keyof SectionDataRegistry> {
2501
+ id: string;
2502
+ type: K;
2503
+ data: SectionDataRegistry[K];
2504
+ settings?: K extends keyof SectionSettingsRegistry
2505
+ ? SectionSettingsRegistry[K]
2506
+ : BaseSectionSettings;
2981
2507
  }
2982
2508
 
2983
- function sleep(ms: number): Promise<void> {
2984
- return new Promise((resolve) => setTimeout(resolve, ms));
2985
- }
2509
+ export type Section = {
2510
+ [K in keyof SectionDataRegistry]: BaseSection<K>
2511
+ }[keyof SectionDataRegistry];
2512
+ ```
2986
2513
 
2987
- function isRetryableStatus(status: number): boolean {
2988
- return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
2989
- }
2514
+ **SectionType:** Core exports (or Tenant infers) **`SectionType`** as **`keyof SectionDataRegistry`**. After Tenant module augmentation, this is the union of all section type keys (e.g. `'header' | 'footer' | 'hero' | ...`). The Tenant uses this type for the ComponentRegistry and SECTION_SCHEMAS keys.
2990
2515
 
2991
- function backoffDelayMs(attempt: number): number {
2992
- const base = 250 * Math.pow(2, attempt);
2993
- const jitter = Math.floor(Math.random() * 120);
2994
- return base + jitter;
2995
- }
2516
+ **Perché servono:** Il Core deve poter renderizzare section senza conoscere i tipi concreti a compile-time; il Tenant deve poter aggiungere nuovi tipi senza modificare il Core. I registry vuoti + module augmentation permettono di distribuire Core come pacchetto NPM e mantenere type-safety end-to-end (Section, registry, config). Senza MTRP, ogni nuovo tipo richiederebbe cambi nel Core o tipi deboli (`any`).
2996
2517
 
2997
- function logBootstrapEvent(event: string, details: Record<string, unknown>) {
2998
- console.info('[boot]', { event, at: new Date().toISOString(), ...details });
2999
- }
2518
+ ---
3000
2519
 
3001
- function cloudFingerprint(apiBase: string, apiKey: string): string {
3002
- return `${normalizeApiBase(apiBase)}::${apiKey.slice(-8)}`;
3003
- }
2520
+ ## 2. 📐 JsonPages Site Protocol (JSP) v1.8
3004
2521
 
3005
- function normalizeSlugForCache(slug: string): string {
3006
- return (
3007
- slug
3008
- .trim()
3009
- .toLowerCase()
3010
- .replace(/[^a-z0-9/_-]/g, '-')
3011
- .replace(/^\/+|\/+$/g, '') || 'home'
3012
- );
3013
- }
2522
+ **Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).
3014
2523
 
3015
- function readCachedCloudContent(fingerprint: string): CachedCloudContent | null {
3016
- try {
3017
- const raw = localStorage.getItem(CLOUD_CACHE_KEY);
3018
- if (!raw) return null;
3019
- const parsed = JSON.parse(raw) as CachedCloudContent;
3020
- if (!parsed || parsed.keyFingerprint !== fingerprint) return null;
3021
- if (!parsed.savedAt || Date.now() - parsed.savedAt > CLOUD_CACHE_TTL_MS) return null;
3022
- return parsed;
3023
- } catch {
3024
- return null;
3025
- }
3026
- }
2524
+ ### 2.1 The File System Ontology (The Silo Contract)
2525
+ Every site must reside in an isolated directory. Global Governance is physically separated from Local Content.
2526
+ * **`/config/site.json`** Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.
2527
+ * **`/config/menu.json`** — Navigation Tree (SSOT for System Header). See Appendix A.
2528
+ * **`/config/theme.json`** Theme tokens (optional but recommended). See Appendix A.
2529
+ * **`/pages/[slug].json`** Local Body Content per page. See Appendix A (PageConfig).
3027
2530
 
3028
- function writeCachedCloudContent(entry: CachedCloudContent): void {
3029
- try {
3030
- localStorage.setItem(CLOUD_CACHE_KEY, JSON.stringify(entry));
3031
- } catch {
3032
- // non-blocking cache path
3033
- }
3034
- }
2531
+ **Application path convention:** The runtime app typically imports these via an alias (e.g. **`@/data/config/`** and **`@/data/pages/`**). The physical silo may be `src/data/config/` and `src/data/pages/` so that `site.json`, `menu.json`, `theme.json` live under `src/data/config/`, and page JSONs under `src/data/pages/`. The CLI or projection script may use `/config/` and `/pages/` at repo root; the **contract** is that the app receives **siteConfig**, **menuConfig**, **themeConfig**, and **pages** as defined in JEB (§10) and Appendix A.
3035
2532
 
3036
- function buildThemeFontVarsCss(input: unknown): string {
3037
- if (!isObjectRecord(input)) return '';
3038
- const tokens = isObjectRecord(input.tokens) ? input.tokens : null;
3039
- const typography = tokens && isObjectRecord(tokens.typography) ? tokens.typography : null;
3040
- const fontFamily = typography && isObjectRecord(typography.fontFamily) ? typography.fontFamily : null;
3041
- const primary = typeof fontFamily?.primary === 'string' ? fontFamily.primary : "'Instrument Sans', system-ui, sans-serif";
3042
- const serif = typeof fontFamily?.serif === 'string' ? fontFamily.serif : "'Instrument Serif', Georgia, serif";
3043
- const mono = typeof fontFamily?.mono === 'string' ? fontFamily.mono : "'JetBrains Mono', monospace";
3044
- return `:root{--theme-font-primary:${primary};--theme-font-serif:${serif};--theme-font-mono:${mono};}`;
3045
- }
2533
+ ### 2.2 Deterministic Projection (CLI Workflow)
2534
+ The CLI (`@olonjs/cli`) creates new tenants by:
2535
+ 1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).
2536
+ 2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).
2537
+ 3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.
3046
2538
 
3047
- function setTenantPreviewReady(ready: boolean): void {
3048
- if (typeof window !== 'undefined') {
3049
- (window as Window & { __TENANT_PREVIEW_READY__?: boolean }).__TENANT_PREVIEW_READY__ = ready;
3050
- }
3051
- if (typeof document !== 'undefined' && document.body) {
3052
- document.body.dataset.previewReady = ready ? '1' : '0';
3053
- }
3054
- }
2539
+ **Perché servono:** Una struttura file deterministica (config vs pages) separa governance globale (site, menu, theme) dal contenuto per pagina; il CLI può rigenerare tenant e tooling può trovare dati e schemi sempre negli stessi path. Senza JSP, ogni tenant sarebbe una struttura ad hoc e ingestione/export/Bake sarebbero fragili.
3055
2540
 
3056
- function App() {
3057
- const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
3058
- const localInitialData = useMemo(() => (isCloudMode ? null : getInitialData()), [isCloudMode]);
3059
- const localInitialPages = useMemo(() => {
3060
- if (!localInitialData) return {};
3061
- const normalized = normalizePageRegistry(localInitialData.pages as unknown);
3062
- return Object.keys(normalized).length > 0 ? normalized : localInitialData.pages;
3063
- }, [localInitialData]);
3064
- const [pages, setPages] = useState<Record<string, PageConfig>>(localInitialPages);
3065
- const [siteConfig, setSiteConfig] = useState<SiteConfig>(
3066
- localInitialData?.siteConfig ?? fileSiteConfig
3067
- );
3068
- const [assetsManifest, setAssetsManifest] = useState<LibraryImageEntry[]>([]);
3069
- const [cloudSaveUi, setCloudSaveUi] = useState<CloudSaveUiState>(getInitialCloudSaveUiState);
3070
- const [contentMode, setContentMode] = useState<ContentMode>('cloud');
3071
- const [contentFallback, setContentFallback] = useState<CloudLoadFailure | null>(null);
3072
- const [showTopProgress, setShowTopProgress] = useState(false);
3073
- const [hasInitialCloudResolved, setHasInitialCloudResolved] = useState(!isCloudMode);
3074
- const [bootstrapRunId, setBootstrapRunId] = useState(0);
3075
- const activeCloudSaveController = useRef<AbortController | null>(null);
3076
- const contentLoadInFlight = useRef<Promise<void> | null>(null);
3077
- const pendingCloudSave = useRef<{ state: ProjectState; slug: string } | null>(null);
3078
- const cloudApiCandidates = useMemo(
3079
- () => (isCloudMode && CLOUD_API_URL ? buildApiCandidates(CLOUD_API_URL) : []),
3080
- [isCloudMode, CLOUD_API_URL]
3081
- );
2541
+ ---
3082
2542
 
3083
- const loadAssetsManifest = useCallback(async (): Promise<void> => {
3084
- if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
3085
- const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
3086
- for (const apiBase of apiBases) {
3087
- try {
3088
- const res = await fetch(`${apiBase}/assets/list?limit=200`, {
3089
- method: 'GET',
3090
- headers: {
3091
- Authorization: `Bearer ${CLOUD_API_KEY}`,
3092
- },
3093
- });
3094
- const body = (await res.json().catch(() => ({}))) as { items?: LibraryImageEntry[] };
3095
- if (!res.ok) continue;
3096
- const items = Array.isArray(body.items) ? body.items : [];
3097
- setAssetsManifest(items);
3098
- return;
3099
- } catch {
3100
- // try next candidate
3101
- }
3102
- }
3103
- setAssetsManifest([]);
3104
- return;
3105
- }
2543
+ ## 3. 🧱 Tenant Block Protocol (TBP) v1.0
3106
2544
 
3107
- fetch('/api/list-assets')
3108
- .then((r) => (r.ok ? r.json() : []))
3109
- .then((list: LibraryImageEntry[]) => setAssetsManifest(Array.isArray(list) ? list : []))
3110
- .catch(() => setAssetsManifest([]));
3111
- }, [isCloudMode, CLOUD_API_URL, CLOUD_API_KEY, cloudApiCandidates]);
2545
+ **Objective:** Standardize the "Capsule" structure for components to enable automated ingestion (Pull) by the SaaS.
3112
2546
 
3113
- useEffect(() => {
3114
- void loadAssetsManifest();
3115
- }, [loadAssetsManifest]);
2547
+ ### 3.1 The Atomic Capsule Structure
2548
+ Components are self-contained directories under **`src/components/<sectionType>/`**:
2549
+ * **`View.tsx`** — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).
2550
+ * **`schema.ts`** — Zod schema(s) for the **data** contract (and optionally **settings**). Exports at least one schema (e.g. `HeroSchema`) used as the **data** schema for that type. Must extend BaseSectionData (§8) for data; array items must extend BaseArrayItem (§8).
2551
+ * **`types.ts`** — TypeScript interfaces inferred from the schema (e.g. `HeroData`, `HeroSettings`). Export types with names **`<SectionType>Data`** and **`<SectionType>Settings`** (or equivalent) so the Tenant can aggregate them in a single types module.
2552
+ * **`index.ts`** — Public API: re-exports View, schema(s), and types.
3116
2553
 
3117
- useEffect(() => {
3118
- return () => {
3119
- activeCloudSaveController.current?.abort();
3120
- };
3121
- }, []);
2554
+ ### 3.2 Reserved System Types
2555
+ * **`type: 'header'`** — Reserved for `site.json`. Receives **`menu: MenuItem[]`** in addition to `data` and `settings`. Menu is sourced from `menu.json` (see Appendix A). The Tenant **must** type `SectionComponentPropsMap['header']` as `{ data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] }`.
2556
+ * **`type: 'footer'`** — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).
2557
+ * **`type: 'sectionHeader'`** — A standard local block. Must define its own `links` array in its local schema if used.
3122
2558
 
3123
- useEffect(() => {
3124
- setTenantPreviewReady(false);
3125
- return () => {
3126
- setTenantPreviewReady(false);
3127
- };
3128
- }, []);
2559
+ **Perché servono:** La capsula (View + schema + types + index) è l’unità di estensione: il Core e il Form Factory possono scoprire tipi e contratti per tipo senza convenzioni ad hoc. Header/footer riservati evitano conflitti tra globale e locale. Senza TBP, aggregazione di SECTION_SCHEMAS e registry sarebbe incoerente e l’ingestion da SaaS non sarebbe automatizzabile.
3129
2560
 
3130
- useEffect(() => {
3131
- if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
3132
- setContentMode('cloud');
3133
- setContentFallback(null);
3134
- setShowTopProgress(false);
3135
- setHasInitialCloudResolved(true);
3136
- logBootstrapEvent('boot.local.ready', { mode: 'local' });
3137
- return;
3138
- }
3139
- if (contentLoadInFlight.current) {
3140
- return;
3141
- }
2561
+ ---
3142
2562
 
3143
- const controller = new AbortController();
3144
- const maxRetryAttempts = 2;
3145
- const startedAt = Date.now();
3146
- const primaryApiBase = cloudApiCandidates[0] ?? normalizeApiBase(CLOUD_API_URL);
3147
- const fingerprint = cloudFingerprint(primaryApiBase, CLOUD_API_KEY);
3148
- const cached = readCachedCloudContent(fingerprint);
3149
- const cachedPages = cached ? toPagesRecord(cached.pages) : null;
3150
- const cachedSite = cached ? coerceSiteConfig(cached.siteConfig) : null;
3151
- const hasCachedFallback = Boolean((cachedPages && Object.keys(cachedPages).length > 0) || cachedSite);
3152
- if (cached) {
3153
- logBootstrapEvent('boot.cloud.cache_hit', { ageMs: Date.now() - cached.savedAt });
3154
- }
3155
- setContentMode('cloud');
3156
- setContentFallback(null);
3157
- setShowTopProgress(true);
3158
- setHasInitialCloudResolved(false);
3159
- logBootstrapEvent('boot.start', { mode: 'cloud', apiCandidates: cloudApiCandidates.length });
2563
+ ## 4. 🧱 Component Implementation Protocol (CIP) v1.5
3160
2564
 
3161
- const loadCloudContent = async () => {
3162
- try {
3163
- let payload: ContentResponse | null = null;
3164
- let lastFailure: CloudLoadFailure | null = null;
2565
+ **Objective:** Ensure system-wide stability and Admin UI integrity.
3165
2566
 
3166
- for (const apiBase of cloudApiCandidates) {
3167
- for (let attempt = 0; attempt <= maxRetryAttempts; attempt += 1) {
3168
- try {
3169
- const res = await fetch(`${apiBase}/content`, {
3170
- method: 'GET',
3171
- cache: 'no-store',
3172
- headers: {
3173
- Authorization: `Bearer ${CLOUD_API_KEY}`,
3174
- },
3175
- signal: controller.signal,
3176
- });
2567
+ 1. **The "Sovereign View" Law:** Components receive `data` and `settings` (and `menu` for header only) and return JSX. They are metadata-blind (never import Zod schemas).
2568
+ 2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.
2569
+ 3. **Agnostic Asset Protocol:** Use `resolveAssetUrl(path, tenantId)` for all media. Resolved URLs are under **`/assets/...`** with no tenantId segment in the path (e.g. relative `img/hero.jpg` → `/assets/img/hero.jpg`).
3177
2570
 
3178
- const contentType = (res.headers.get('content-type') || '').toLowerCase();
3179
- if (!contentType.includes('application/json')) {
3180
- lastFailure = {
3181
- reasonCode: 'NON_JSON_RESPONSE',
3182
- message: `Non-JSON response from ${apiBase}/content`,
3183
- };
3184
- break;
3185
- }
2571
+ ### 4.4 Local Design Tokens (v1.2)
2572
+ Section Views that control their own background, text, borders, or radii **shall** define a **local scope** via an inline `style` object on the section root: e.g. `--local-bg`, `--local-text`, `--local-text-muted`, `--local-surface`, `--local-border`, `--local-radius-lg`, `--local-accent`, mapped to theme variables. All Tailwind classes that affect color or radius in that section **must** use these variables (e.g. `bg-[var(--local-bg)]`, `text-[var(--local-text)]`). No naked utilities (e.g. `bg-blue-500`). An optional **`label`** in section data may be rendered with class **`jp-section-label`** for overlay type labels.
3186
2573
 
3187
- const parsed = (await res.json().catch(() => ({}))) as ContentResponse;
3188
- if (!res.ok) {
3189
- lastFailure = {
3190
- reasonCode: parsed.code || `HTTP_${res.status}`,
3191
- message: parsed.error || `Cloud content read failed: ${res.status} (${apiBase}/content)`,
3192
- correlationId: parsed.correlationId,
3193
- };
3194
- if (isRetryableStatus(res.status) && attempt < maxRetryAttempts) {
3195
- await sleep(backoffDelayMs(attempt));
3196
- continue;
3197
- }
3198
- break;
3199
- }
2574
+ ### 4.5 Z-Index & Overlay Governance (v1.2)
2575
+ Section content root **must** stay at **`z-index` ≤ 1** (prefer `z-0`) so the Sovereign Overlay can sit above with high z-index in Tenant CSS (§7). Header/footer may use a higher z-index (e.g. 50) only as a documented exception for global chrome.
3200
2576
 
3201
- payload = parsed;
3202
- break;
3203
- } catch (error: unknown) {
3204
- if (controller.signal.aborted) throw error;
3205
- const message = error instanceof Error ? error.message : 'Network error';
3206
- lastFailure = {
3207
- reasonCode: 'NETWORK_TRANSIENT',
3208
- message: `${message} (${apiBase}/content)`,
3209
- };
3210
- if (attempt < maxRetryAttempts) {
3211
- await sleep(backoffDelayMs(attempt));
3212
- continue;
3213
- }
3214
- }
3215
- }
3216
- if (payload) {
3217
- break;
3218
- }
3219
- }
2577
+ **Perché servono (CIP):** View “dumb” (solo data/settings) e senza import di Zod evita accoppiamento e permette al Form Factory di essere l’unica fonte di verità sugli schemi. Z-index basso evita che il contenuto copra l’overlay di selezione in Studio. Asset via `resolveAssetUrl`: i path relativi vengono risolti in `/assets/...` (senza segmento tenantId nel path). Token locali (`--local-*`) rendono le section temabili e coerenti con overlay e tema; senza, stili “nudi” creano drift visivo e conflitti con l’UI di editing.
3220
2578
 
3221
- if (!payload) {
3222
- throw (
3223
- lastFailure || {
3224
- reasonCode: 'CLOUD_ENDPOINT_UNREACHABLE',
3225
- message: 'Cloud content endpoint not reachable as JSON.',
3226
- }
3227
- );
3228
- }
2579
+ ---
3229
2580
 
3230
- const { pagesSource, siteSource } = extractContentSources(payload);
3231
- const remotePages = toPagesRecord(pagesSource);
3232
- const remoteSite = coerceSiteConfig(siteSource);
3233
- const remotePageCount = remotePages ? Object.keys(remotePages).length : 0;
3234
- if (remotePageCount === 0 && !remoteSite) {
3235
- throw {
3236
- reasonCode: payload.contentStatus === 'empty_namespace' ? 'EMPTY_NAMESPACE' : 'EMPTY_PAYLOAD',
3237
- message: 'Cloud payload is empty for this tenant namespace.',
3238
- correlationId: payload.correlationId,
3239
- } satisfies CloudLoadFailure;
3240
- }
3241
- if (import.meta.env.DEV) {
3242
- console.info('[content] cloud diagnostics', {
3243
- contentStatus: payload.contentStatus ?? 'ok',
3244
- namespace: payload.namespace,
3245
- namespaceMatchedKeys: payload.namespaceMatchedKeys,
3246
- usedUnscopedFallback: payload.usedUnscopedFallback,
3247
- correlationId: payload.correlationId,
3248
- });
3249
- }
3250
- if (remotePages && remotePageCount > 0) {
3251
- setPages(remotePages);
3252
- }
3253
- if (remoteSite) {
3254
- setSiteConfig(remoteSite);
3255
- }
3256
- writeCachedCloudContent({
3257
- keyFingerprint: fingerprint,
3258
- savedAt: Date.now(),
3259
- siteConfig: remoteSite ?? null,
3260
- pages: (remotePages ?? {}) as Record<string, unknown>,
3261
- });
3262
- setContentMode('cloud');
3263
- setContentFallback(null);
3264
- setHasInitialCloudResolved(true);
3265
- logBootstrapEvent('boot.cloud.success', {
3266
- mode: 'cloud',
3267
- elapsedMs: Date.now() - startedAt,
3268
- contentStatus: payload.contentStatus ?? 'ok',
3269
- correlationId: payload.correlationId ?? null,
3270
- });
3271
- } catch (error: unknown) {
3272
- if (controller.signal.aborted) return;
3273
- const failure = toCloudLoadFailure(error);
3274
- if (hasCachedFallback) {
3275
- if (cachedPages && Object.keys(cachedPages).length > 0) {
3276
- setPages(cachedPages);
3277
- }
3278
- if (cachedSite) {
3279
- setSiteConfig(cachedSite);
3280
- }
3281
- setContentMode('cloud');
3282
- setContentFallback({
3283
- reasonCode: 'CLOUD_REFRESH_FAILED',
3284
- message: failure.message,
3285
- correlationId: failure.correlationId,
3286
- });
3287
- setHasInitialCloudResolved(true);
3288
- } else {
3289
- setContentMode('error');
3290
- setContentFallback(failure);
3291
- setHasInitialCloudResolved(true);
3292
- }
3293
- logBootstrapEvent('boot.cloud.error', {
3294
- mode: 'cloud',
3295
- elapsedMs: Date.now() - startedAt,
3296
- reasonCode: failure.reasonCode,
3297
- correlationId: failure.correlationId ?? null,
3298
- });
3299
- }
3300
- };
2581
+ ## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5
3301
2582
 
3302
- let inFlight: Promise<void> | null = null;
3303
- inFlight = loadCloudContent().finally(() => {
3304
- setShowTopProgress(false);
3305
- if (contentLoadInFlight.current === inFlight) {
3306
- contentLoadInFlight.current = null;
3307
- }
3308
- });
3309
- contentLoadInFlight.current = inFlight;
3310
- return () => controller.abort();
3311
- }, [isCloudMode, CLOUD_API_KEY, CLOUD_API_URL, cloudApiCandidates, bootstrapRunId]);
2583
+ **Objective:** Standardize the Polymorphic ICE engine.
3312
2584
 
3313
- const runCloudSave = useCallback(
3314
- async (
3315
- payload: { state: ProjectState; slug: string },
3316
- rejectOnError: boolean
3317
- ): Promise<void> => {
3318
- if (!CLOUD_API_URL || !CLOUD_API_KEY) {
3319
- const noCloudError = new Error('Cloud mode is not configured.');
3320
- if (rejectOnError) throw noCloudError;
3321
- return;
3322
- }
2585
+ 1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.
2586
+ 2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.
2587
+ 3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.
3323
2588
 
3324
- pendingCloudSave.current = payload;
3325
- activeCloudSaveController.current?.abort();
3326
- const controller = new AbortController();
3327
- activeCloudSaveController.current = controller;
2589
+ ### 5.4 UI Metadata Vocabulary (v1.2)
2590
+ Standard keys for the Form Factory:
3328
2591
 
3329
- setCloudSaveUi({
3330
- isOpen: true,
3331
- phase: 'running',
3332
- currentStepId: null,
3333
- doneSteps: [],
3334
- progress: 0,
3335
- });
2592
+ | Key | Use case |
2593
+ |-----|----------|
2594
+ | `ui:text` | Single-line text input. |
2595
+ | `ui:textarea` | Multi-line text. |
2596
+ | `ui:select` | Enum / single choice. |
2597
+ | `ui:number` | Numeric input. |
2598
+ | `ui:list` | Array of items; list editor (add/remove/reorder). |
2599
+ | `ui:icon-picker` | Icon selection. |
3336
2600
 
3337
- try {
3338
- await startCloudSaveStream({
3339
- apiBaseUrl: CLOUD_API_URL,
3340
- apiKey: CLOUD_API_KEY,
3341
- path: `src/data/pages/${payload.slug}.json`,
3342
- content: payload.state.page,
3343
- message: `Content update for ${payload.slug} via Visual Editor`,
3344
- signal: controller.signal,
3345
- onStep: (event) => {
3346
- setCloudSaveUi((prev) => {
3347
- if (event.status === 'running') {
3348
- return {
3349
- ...prev,
3350
- isOpen: true,
3351
- phase: 'running',
3352
- currentStepId: event.id,
3353
- errorMessage: undefined,
3354
- };
3355
- }
2601
+ Unknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.
3356
2602
 
3357
- if (prev.doneSteps.includes(event.id)) {
3358
- return prev;
3359
- }
2603
+ ### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)
2604
+ In strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.
3360
2605
 
3361
- const nextDone = [...prev.doneSteps, event.id];
3362
- return {
3363
- ...prev,
3364
- isOpen: true,
3365
- phase: 'running',
3366
- currentStepId: event.id,
3367
- doneSteps: nextDone,
3368
- progress: stepProgress(nextDone),
3369
- };
3370
- });
3371
- },
3372
- onDone: (event) => {
3373
- const completed = DEPLOY_STEPS.map((step) => step.id);
3374
- setCloudSaveUi({
3375
- isOpen: true,
3376
- phase: 'done',
3377
- currentStepId: 'live',
3378
- doneSteps: completed,
3379
- progress: 100,
3380
- deployUrl: event.deployUrl,
3381
- });
3382
- },
3383
- });
3384
- } catch (error: unknown) {
3385
- const message = error instanceof Error ? error.message : 'Cloud save failed.';
3386
- setCloudSaveUi((prev) => ({
3387
- ...prev,
3388
- isOpen: true,
3389
- phase: 'error',
3390
- errorMessage: message,
3391
- }));
3392
- if (rejectOnError) throw new Error(message);
3393
- } finally {
3394
- if (activeCloudSaveController.current === controller) {
3395
- activeCloudSaveController.current = null;
3396
- }
3397
- }
3398
- },
3399
- []
3400
- );
2606
+ ```typescript
2607
+ export type SelectionPathSegment = { fieldKey: string; itemId?: string };
2608
+ export type SelectionPath = SelectionPathSegment[];
2609
+ ```
3401
2610
 
3402
- const closeCloudDrawer = useCallback(() => {
3403
- setCloudSaveUi(getInitialCloudSaveUiState());
3404
- }, []);
2611
+ Rules:
2612
+ * Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.
2613
+ * Matching by `fieldKey` alone is non-compliant for nested structures.
2614
+ * Legacy flat payload fields **`itemField`** and **`itemId`** are removed from strict v1.3 selection protocol.
3405
2615
 
3406
- const retryCloudSave = useCallback(() => {
3407
- if (!pendingCloudSave.current) return;
3408
- void runCloudSave(pendingCloudSave.current, false);
3409
- }, [runCloudSave]);
2616
+ **Perché servono (ECIP):** Il Form Factory deve sapere quale widget usare (text, textarea, select, list, …) senza hardcodare per tipo; `.describe('ui:...')` è il contratto. BaseArrayItem con `id` su ogni item di array garantisce chiavi stabili in React e reorder/delete corretti nell’Inspector. In v1.3 la selezione/espansione path-only elimina ambiguità su array annidati: senza path completo root→leaf, la sidebar può aprire il ramo sbagliato o non aprire il target.
3410
2617
 
3411
- const config: JsonPagesConfig = {
3412
- tenantId: TENANT_ID,
3413
- registry: ComponentRegistry as JsonPagesConfig['registry'],
3414
- schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
3415
- pages,
3416
- siteConfig,
3417
- themeConfig,
3418
- menuConfig,
3419
- refDocuments,
3420
- themeCss: { tenant: `${buildThemeFontVarsCss(themeConfig)}\n${tenantCss}` },
3421
- addSection: addSectionConfig,
3422
- persistence: {
3423
- async saveToFile(state: ProjectState, slug: string): Promise<void> {
3424
- // 💻 LOCAL FILESYSTEM (Development / legacy fallback)
3425
- console.log(`💻 Saving ${slug} to Local Filesystem...`);
3426
- const res = await fetch('/api/save-to-file', {
3427
- method: 'POST',
3428
- headers: { 'Content-Type': 'application/json' },
3429
- body: JSON.stringify({ projectState: state, slug }),
3430
- });
3431
-
3432
- const body = (await res.json().catch(() => ({}))) as { error?: string };
3433
- if (!res.ok) throw new Error(body.error ?? `Save to file failed: ${res.status}`);
3434
- },
3435
- async hotSave(state: ProjectState, slug: string): Promise<void> {
3436
- if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
3437
- throw new Error('Cloud mode is not configured for hot save.');
3438
- }
3439
- const apiBase = CLOUD_API_URL.replace(/\/$/, '');
3440
- const res = await fetch(`${apiBase}/hotSave`, {
3441
- method: 'POST',
3442
- headers: {
3443
- 'Content-Type': 'application/json',
3444
- Authorization: `Bearer ${CLOUD_API_KEY}`,
3445
- },
3446
- body: JSON.stringify({
3447
- slug,
3448
- page: state.page,
3449
- siteConfig: state.site,
3450
- }),
3451
- });
3452
- const body = (await res.json().catch(() => ({}))) as { error?: string; code?: string };
3453
- if (!res.ok) {
3454
- throw new Error(body.error || body.code || `Hot save failed: ${res.status}`);
3455
- }
3456
- const keyFingerprint = cloudFingerprint(apiBase, CLOUD_API_KEY);
3457
- const normalizedSlug = normalizeSlugForCache(slug);
3458
- const existing = readCachedCloudContent(keyFingerprint);
3459
- writeCachedCloudContent({
3460
- keyFingerprint,
3461
- savedAt: Date.now(),
3462
- siteConfig: state.site ?? null,
3463
- pages: {
3464
- ...(existing?.pages ?? {}),
3465
- [normalizedSlug]: state.page,
3466
- },
3467
- });
3468
- },
3469
- showLegacySave: !isCloudMode,
3470
- showHotSave: isCloudMode,
3471
- },
3472
- assets: {
3473
- assetsBaseUrl: '/assets',
3474
- assetsManifest,
3475
- async onAssetUpload(file: File): Promise<string> {
3476
- if (!file.type.startsWith('image/')) throw new Error('Invalid file type.');
3477
- if (!ALLOWED_IMAGE_MIME_TYPES.has(file.type)) {
3478
- throw new Error('Unsupported image format. Allowed: jpeg, png, webp, gif, avif.');
3479
- }
3480
- if (file.size > MAX_UPLOAD_SIZE_BYTES) throw new Error(`File too large. Max ${MAX_UPLOAD_SIZE_BYTES / 1024 / 1024}MB.`);
2618
+ ---
3481
2619
 
3482
- if (isCloudMode && CLOUD_API_URL && CLOUD_API_KEY) {
3483
- const apiBases = cloudApiCandidates.length > 0 ? cloudApiCandidates : [normalizeApiBase(CLOUD_API_URL)];
3484
- let lastError: Error | null = null;
3485
- for (const apiBase of apiBases) {
3486
- for (let attempt = 0; attempt <= ASSET_UPLOAD_MAX_RETRIES; attempt += 1) {
3487
- try {
3488
- const formData = new FormData();
3489
- formData.append('file', file);
3490
- formData.append('filename', file.name);
3491
- const controller = new AbortController();
3492
- const timeout = window.setTimeout(() => controller.abort(), ASSET_UPLOAD_TIMEOUT_MS);
3493
- const res = await fetch(`${apiBase}/assets/upload`, {
3494
- method: 'POST',
3495
- headers: {
3496
- Authorization: `Bearer ${CLOUD_API_KEY}`,
3497
- 'X-Correlation-Id': crypto.randomUUID(),
3498
- },
3499
- body: formData,
3500
- signal: controller.signal,
3501
- }).finally(() => window.clearTimeout(timeout));
3502
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string; code?: string };
3503
- if (res.ok && typeof body.url === 'string') {
3504
- await loadAssetsManifest().catch(() => undefined);
3505
- return body.url;
3506
- }
3507
- lastError = new Error(body.error || body.code || `Cloud upload failed: ${res.status}`);
3508
- if (isRetryableStatus(res.status) && attempt < ASSET_UPLOAD_MAX_RETRIES) {
3509
- await sleep(backoffDelayMs(attempt));
3510
- continue;
3511
- }
3512
- break;
3513
- } catch (error: unknown) {
3514
- const message = error instanceof Error ? error.message : 'Cloud upload failed.';
3515
- lastError = new Error(message);
3516
- if (attempt < ASSET_UPLOAD_MAX_RETRIES) {
3517
- await sleep(backoffDelayMs(attempt));
3518
- continue;
3519
- }
3520
- break;
3521
- }
3522
- }
3523
- }
3524
- throw lastError ?? new Error('Cloud upload failed.');
3525
- }
2620
+ ## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1
3526
2621
 
3527
- const base64 = await new Promise<string>((resolve, reject) => {
3528
- const reader = new FileReader();
3529
- reader.onload = () => resolve((reader.result as string).split(',')[1] ?? '');
3530
- reader.onerror = () => reject(reader.error);
3531
- reader.readAsDataURL(file);
3532
- });
2622
+ **Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.
3533
2623
 
3534
- const res = await fetch('/api/upload-asset', {
3535
- method: 'POST',
3536
- headers: { 'Content-Type': 'application/json' },
3537
- body: JSON.stringify({ filename: file.name, mimeType: file.type || undefined, data: base64 }),
3538
- });
3539
- const body = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
3540
- if (!res.ok) throw new Error(body.error || `Upload failed: ${res.status}`);
3541
- if (typeof body.url !== 'string') throw new Error('Invalid server response: missing url');
3542
- await loadAssetsManifest().catch(() => undefined);
3543
- return body.url;
3544
- },
3545
- },
3546
- };
2624
+ ### 6.1 Section-Level Markup (Core-Provided)
2625
+ **SectionRenderer** (Core) wraps each section root with:
2626
+ * **`data-section-id`** Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.
2627
+ * Sibling overlay element **`data-jp-section-overlay`** Selection ring and type label. **Tenant does not add this;** Core injects it.
3547
2628
 
3548
- const shouldRenderEngine = !isCloudMode || hasInitialCloudResolved;
2629
+ Tenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.
3549
2630
 
3550
- useEffect(() => {
3551
- if (!shouldRenderEngine) {
3552
- setTenantPreviewReady(false);
3553
- return;
3554
- }
3555
- let cancelled = false;
3556
- let raf1 = 0;
3557
- let raf2 = 0;
3558
- raf1 = window.requestAnimationFrame(() => {
3559
- raf2 = window.requestAnimationFrame(() => {
3560
- if (!cancelled) setTenantPreviewReady(true);
3561
- });
3562
- });
3563
- return () => {
3564
- cancelled = true;
3565
- window.cancelAnimationFrame(raf1);
3566
- window.cancelAnimationFrame(raf2);
3567
- setTenantPreviewReady(false);
3568
- };
3569
- }, [shouldRenderEngine, pages, siteConfig]);
2631
+ ### 6.2 Field-Level Binding (Tenant-Provided)
2632
+ For every **editable scalar field** the View **must** attach **`data-jp-field="<fieldKey>"`** (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).
3570
2633
 
3571
- return (
3572
- <ThemeProvider>
3573
- <>
3574
- {isCloudMode && showTopProgress ? (
3575
- <>
3576
- <style>
3577
- {`@keyframes jp-top-progress-slide { 0% { transform: translateX(-120%); } 100% { transform: translateX(320%); } }`}
3578
- </style>
3579
- <div
3580
- role="status"
3581
- aria-live="polite"
3582
- aria-label="Cloud loading progress"
3583
- style={{
3584
- position: 'fixed',
3585
- top: 0,
3586
- left: 0,
3587
- right: 0,
3588
- height: 2,
3589
- zIndex: 1300,
3590
- background: 'rgba(255,255,255,0.08)',
3591
- overflow: 'hidden',
3592
- }}
3593
- >
3594
- <div
3595
- style={{
3596
- width: '32%',
3597
- height: '100%',
3598
- background: 'linear-gradient(90deg, rgba(88,166,255,0.15) 0%, rgba(88,166,255,0.85) 50%, rgba(88,166,255,0.15) 100%)',
3599
- animation: 'jp-top-progress-slide 1.15s ease-in-out infinite',
3600
- willChange: 'transform',
3601
- }}
3602
- />
3603
- </div>
3604
- </>
3605
- ) : null}
3606
- {isCloudMode && !hasInitialCloudResolved ? (
3607
- <div className="fixed inset-0 z-[1290] bg-background/80 backdrop-blur-sm">
3608
- <div className="mx-auto w-full max-w-[1600px] p-6">
3609
- <div className="grid gap-4 lg:grid-cols-[1fr_420px]">
3610
- <div className="space-y-4">
3611
- <Skeleton className="h-10 w-64" />
3612
- <Skeleton className="h-[220px] w-full rounded-xl" />
3613
- <Skeleton className="h-[220px] w-full rounded-xl" />
3614
- </div>
3615
- <div className="space-y-3 rounded-xl border border-border/50 bg-card/60 p-4">
3616
- <Skeleton className="h-8 w-32" />
3617
- <Skeleton className="h-5 w-full" />
3618
- <Skeleton className="h-5 w-5/6" />
3619
- <Skeleton className="h-5 w-4/6" />
3620
- <Skeleton className="h-24 w-full rounded-lg" />
3621
- </div>
3622
- </div>
3623
- </div>
3624
- </div>
3625
- ) : null}
3626
- {shouldRenderEngine ? <JsonPagesEngine config={config} /> : null}
3627
- {isCloudMode && (contentMode === 'error' || contentFallback?.reasonCode === 'CLOUD_REFRESH_FAILED') ? (
3628
- <div
3629
- role="status"
3630
- aria-live="polite"
3631
- style={{
3632
- position: 'fixed',
3633
- top: 12,
3634
- right: 12,
3635
- zIndex: 1200,
3636
- background: 'rgba(179, 65, 24, 0.92)',
3637
- border: '1px solid rgba(255,255,255,0.18)',
3638
- color: '#fff',
3639
- padding: '8px 12px',
3640
- borderRadius: 10,
3641
- fontSize: 12,
3642
- maxWidth: 360,
3643
- boxShadow: '0 8px 24px rgba(0,0,0,0.25)',
3644
- }}
3645
- >
3646
- {contentMode === 'error' ? 'Cloud content unavailable.' : 'Cloud refresh failed, showing cached content.'}
3647
- {contentFallback ? (
3648
- <div style={{ opacity: 0.85, marginTop: 4 }}>
3649
- <div>{contentFallback.message}</div>
3650
- <div style={{ marginTop: 2 }}>
3651
- Reason: {contentFallback.reasonCode}
3652
- {contentFallback.correlationId ? ` | Correlation: ${contentFallback.correlationId}` : ''}
3653
- </div>
3654
- <div style={{ marginTop: 8 }}>
3655
- <button
3656
- type="button"
3657
- onClick={() => {
3658
- contentLoadInFlight.current = null;
3659
- setContentMode('cloud');
3660
- setContentFallback(null);
3661
- setHasInitialCloudResolved(false);
3662
- setShowTopProgress(true);
3663
- setBootstrapRunId((prev) => prev + 1);
3664
- }}
3665
- style={{
3666
- border: '1px solid rgba(255,255,255,0.3)',
3667
- borderRadius: 8,
3668
- padding: '4px 10px',
3669
- background: 'transparent',
3670
- color: '#fff',
3671
- cursor: 'pointer',
3672
- fontSize: 12,
3673
- }}
3674
- >
3675
- Retry
3676
- </button>
3677
- </div>
3678
- </div>
3679
- ) : null}
3680
- </div>
3681
- ) : null}
3682
- <DopaDrawer
3683
- isOpen={cloudSaveUi.isOpen}
3684
- phase={cloudSaveUi.phase}
3685
- currentStepId={cloudSaveUi.currentStepId}
3686
- doneSteps={cloudSaveUi.doneSteps}
3687
- progress={cloudSaveUi.progress}
3688
- errorMessage={cloudSaveUi.errorMessage}
3689
- deployUrl={cloudSaveUi.deployUrl}
3690
- onClose={closeCloudDrawer}
3691
- onRetry={retryCloudSave}
3692
- />
3693
- </>
3694
- </ThemeProvider>
3695
- );
2634
+ ### 6.3 Array-Item Binding (Tenant-Provided)
2635
+ For every **editable array item** the View **must** attach:
2636
+ * **`data-jp-item-id="<stableId>"`** — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.
2637
+ * **`data-jp-item-field="<arrayKey>"`** e.g. `cards`, `layers`, `products`, `paragraphs`.
2638
+
2639
+ ### 6.4 Compliance
2640
+ **Reserved types** (`header`, `footer`): ICE attributes optional unless Studio edits them. **All other section types** in the Stage and in `SECTION_SCHEMAS` **must** implement §6.2 and §6.3 for every editable field and array item.
2641
+
2642
+ ### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)
2643
+ For nested array targets, the Core/Inspector contract is path-based:
2644
+ * The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).
2645
+ * Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.
2646
+ * In strict mode, index-based identity fallback is non-compliant for editable object arrays.
2647
+
2648
+ **Perché servono (IDAC):** Lo Stage è in un iframe e l’Inspector deve sapere **quale campo o item** corrisponde al click (o alla selezione) senza conoscere la struttura DOM del Tenant. **`data-jp-field`** associa un nodo DOM al path dello schema (es. `title`, `description`): così il Core può evidenziare la riga giusta nella sidebar, applicare opacità attivo/inattivo e aprire il form sul campo corretto. **`data-jp-item-id`** e **`data-jp-item-field`** fanno lo stesso per gli item di array (liste, reorder, delete). In v1.3, `itemPath` rende deterministico anche il caso nested (array dentro array), eliminando mismatch tra selezione canvas e ramo aperto in sidebar.
2649
+
2650
+ ---
2651
+
2652
+ ## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0
2653
+
2654
+ **Objective:** The Stage iframe loads only Tenant HTML/CSS. Core injects overlay **markup** but does **not** ship overlay styles. The Tenant **must** supply CSS so overlay is visible.
2655
+
2656
+ ### 7.1 Required Selectors (Tenant global CSS)
2657
+ 1. **`[data-jp-section-overlay]`** — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.
2658
+ 2. **`[data-section-id]:hover [data-jp-section-overlay]`** — Hover: e.g. dashed border, subtle tint.
2659
+ 3. **`[data-section-id][data-jp-selected] [data-jp-section-overlay]`** — Selected: solid border, optional tint.
2660
+ 4. **`[data-jp-section-overlay] > div`** (type label) — Position and visibility (e.g. visible on hover/selected).
2661
+
2662
+ ### 7.2 Z-Index
2663
+ Overlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).
2664
+
2665
+ ### 7.3 Responsibility
2666
+ **Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.
2667
+
2668
+ **Perché servono (TOCC):** L’iframe dello Stage carica solo HTML/CSS del Tenant; il Core inietta il markup dell’overlay ma non gli stili. Senza CSS Tenant per i selettori TOCC, bordo hover/selected e type label non sarebbero visibili: l’autore non vedrebbe quale section è selezionata né il label del tipo. TOCC chiarisce la responsabilità (Core = markup, Tenant = aspetto) e garantisce UX uniforme tra tenant.
2669
+
2670
+ ---
2671
+
2672
+ ## 8. 📦 Base Section Data & Settings (BSDS) v1.0
2673
+
2674
+ **Objective:** Standardize base schema fragments for anchors, array items, and section settings.
2675
+
2676
+ ### 8.1 BaseSectionData
2677
+ Every section data schema **must** extend a base with at least **`anchorId`** (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):
2678
+
2679
+ ```typescript
2680
+ export const BaseSectionData = z.object({
2681
+ anchorId: z.string().optional().describe('ui:text'),
2682
+ });
2683
+ ```
2684
+
2685
+ ### 8.2 BaseArrayItem
2686
+ Every array item schema editable in the Inspector **must** include **`id`** (optional string minimum). Canonical Zod:
2687
+
2688
+ ```typescript
2689
+ export const BaseArrayItem = z.object({
2690
+ id: z.string().optional(),
2691
+ });
2692
+ ```
2693
+
2694
+ Recommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.
2695
+
2696
+ ### 8.3 BaseSectionSettings (Optional)
2697
+ Common section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):
2698
+
2699
+ ```typescript
2700
+ export const BaseSectionSettingsSchema = z.object({
2701
+ paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2702
+ paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),
2703
+ theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),
2704
+ container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),
2705
+ });
2706
+ ```
2707
+
2708
+ Capsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.
2709
+
2710
+ **Perché servono (BSDS):** anchorId permette deep-link e navigazione in-page; id sugli array item è necessario per `data-jp-item-id`, reorder e React reconciliation. BaseSectionSettings comuni (padding, theme, container) evitano ripetizione e allineano il Form Factory tra capsule. Senza base condivisi, ogni capsule inventa convenzioni e validazione/add-section diventano fragili.
2711
+
2712
+ ---
2713
+
2714
+ ## 9. 📌 AddSectionConfig (ASC) v1.0
2715
+
2716
+ **Objective:** Formalize the "Add Section" contract used by the Studio.
2717
+
2718
+ **Type (Core exports `AddSectionConfig`):**
2719
+ ```typescript
2720
+ interface AddSectionConfig {
2721
+ addableSectionTypes: readonly string[];
2722
+ sectionTypeLabels: Record<string, string>;
2723
+ getDefaultSectionData(sectionType: string): Record<string, unknown>;
2724
+ }
2725
+ ```
2726
+
2727
+ **Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:
2728
+ * **`addableSectionTypes`** — Readonly array of section type keys. Only these types appear in the Add Section Library. Must be a subset of (or equal to) the keys in SectionDataRegistry.
2729
+ * **`sectionTypeLabels`** Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).
2730
+ * **`getDefaultSectionData(sectionType: string): Record<string, unknown>`** — Returns default `data` for a new section. Must conform to the capsule’s data schema so the new section validates.
2731
+
2732
+ Core creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.
2733
+
2734
+ **Perché servono (ASC):** Lo Studio deve mostrare una libreria “Aggiungi sezione” con nomi leggibili e, alla scelta, creare una section con dati iniziali validi. addableSectionTypes, sectionTypeLabels e getDefaultSectionData sono il contratto: il Tenant è l’unica fonte di verità su quali tipi sono addabili e con quali default. Senza ASC, il Core non saprebbe cosa mostrare in modal né come popolare i dati della nuova section.
2735
+
2736
+ ---
2737
+
2738
+ ## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1
2739
+
2740
+ **Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.
2741
+
2742
+ ### 10.1 JsonPagesConfig (required fields)
2743
+ The Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:
2744
+
2745
+ | Field | Type | Description |
2746
+ |-------|------|-------------|
2747
+ | **tenantId** | string | Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are **`/assets/...`** with no tenantId segment in the path. |
2748
+ | **registry** | `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` | Component registry. Must match MTRP keys. See Appendix A. |
2749
+ | **schemas** | `Record<SectionType, ZodType>` or equivalent | SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. |
2750
+ | **pages** | `Record<string, PageConfig>` | Slug → page config. See Appendix A. |
2751
+ | **siteConfig** | SiteConfig | Global site (identity, header/footer blocks). See Appendix A. |
2752
+ | **themeConfig** | ThemeConfig | Theme tokens. See Appendix A. |
2753
+ | **menuConfig** | MenuConfig | Navigation tree (SSOT for header menu). See Appendix A. |
2754
+ | **themeCss** | `{ tenant: string }` | At least **tenant**: string (inline CSS or URL) for Stage iframe injection. |
2755
+ | **addSection** | AddSectionConfig | Add-section config (§9). |
2756
+
2757
+ Core may define optional fields. The Tenant must not omit required fields.
2758
+
2759
+ ### 10.2 JsonPagesEngine
2760
+ Root component: **`<JsonPagesEngine config={config} />`**. Responsibilities: route → page, SectionRenderer per section; in Studio mode Sovereign Shell (Inspector, Control Bar, postMessage); section wrappers and overlay per IDAC and JAP. Tenant does not implement the Shell.
2761
+
2762
+ ### 10.3 Studio Selection Event Contract (v1.3, breaking)
2763
+ In strict v1.3 Studio, section selection payload for nested targets is path-based:
2764
+
2765
+ ```typescript
2766
+ type SectionSelectMessage = {
2767
+ type: 'SECTION_SELECT';
2768
+ section: { id: string; type: string; scope: 'global' | 'local' };
2769
+ itemPath?: SelectionPath; // root -> leaf
2770
+ };
2771
+ ```
2772
+
2773
+ Removed from strict protocol:
2774
+ * `itemField`
2775
+ * `itemId`
2776
+
2777
+ **Perché servono (JEB):** Un unico punto di bootstrap (config + Engine) evita che il Tenant replichi logica di routing, Shell e overlay. I campi obbligatori in JsonPagesConfig (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection) sono il minimo per far funzionare rendering, Studio e Form Factory; omissioni causano errori a runtime. In v1.3, il payload `itemPath` sincronizza in modo non ambiguo Stage e Inspector su nested arrays.
2778
+
2779
+ ---
2780
+
2781
+ # 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2
2782
+
2783
+ **Status:** Mandatory Standard
2784
+ **Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)
2785
+ **Objective:** Deterministic orchestration of the "Studio" environment (ICE Level 1).
2786
+
2787
+ ---
2788
+
2789
+ ## 1. The Sovereign Shell Topology
2790
+ The Admin interface is a **Sovereign Shell** from `@olonjs/core`.
2791
+ 1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).
2792
+ 2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.
2793
+ 3. **The Studio Actions:** Save to file, Hot Save, Add Section.
2794
+
2795
+ ## 2. State Orchestration & Persistence
2796
+ * **Working Draft:** Reactive local state for unsaved changes.
2797
+ * **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.
2798
+ * **Persistence Protocol:** Studio invokes tenant-provided `saveToFile` and `hotSave` callbacks for editorial persistence.
2799
+
2800
+ ## 3. Context Switching (Global vs. Local)
2801
+ * **Header/Footer** selection → Global Mode, `site.json`.
2802
+ * Any other section → Page Mode, current `[slug].json`.
2803
+
2804
+ ## 4. Section Lifecycle Management
2805
+ 1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).
2806
+ 2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.
2807
+ 3. **Delete:** Confirmation; remove from array, clear selection.
2808
+
2809
+ ## 5. Stage Isolation & Overlay
2810
+ * **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.
2811
+ * **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).
2812
+
2813
+ ## 6. "Green Build" Validation
2814
+ Studio enforces `tsc && vite build`. No Studio or SSG build should proceed with TypeScript errors.
2815
+
2816
+ ## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)
2817
+ * Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.
2818
+ * Sidebar expansion state for nested arrays must be derived from all path segments.
2819
+ * Flat-only matching may open/close wrong branches and is non-compliant in strict mode.
2820
+
2821
+ **Perché servono (JAP):** Stage in iframe + Inspector + Studio actions separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Save to file e Hot Save richiedono uno stato coerente. Global vs Page mode evita confusione su dove si sta editando (site.json vs [slug].json). Add/Reorder/Delete sono gestiti in un solo modo (Working Draft + ASC). Green Build garantisce che Studio e SSG compilino correttamente. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.
2822
+
2823
+ ---
2824
+
2825
+ ## Compliance: Legacy vs Full UX (v1.3)
2826
+
2827
+ | Dimension | Legacy / Less UX | Full UX (Core-aligned) |
2828
+ |-----------|-------------------|-------------------------|
2829
+ | **ICE binding** | No `data-jp-*`; Inspector cannot bind. | IDAC (§6) on every editable section/field/item. |
2830
+ | **Section wrapper** | Plain `<section>`; no overlay contract. | Core wrapper + overlay; Tenant CSS per TOCC (§7). |
2831
+ | **Design tokens** | Raw BEM / fixed classes. | Local tokens (§4.4); `var(--local-*)` only. |
2832
+ | **Base schemas** | Ad hoc. | BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. |
2833
+ | **Add Section** | Ad hoc defaults. | ASC (§9): addableSectionTypes, labels, getDefaultSectionData. |
2834
+ | **Bootstrap** | Implicit. | JEB (§10): JsonPagesConfig + JsonPagesEngine. |
2835
+ | **Selection payload** | Flat `itemField/itemId`. | Path-only `itemPath: SelectionPath` (JEB §10.3). |
2836
+ | **Nested array expansion** | Single-segment or field-only heuristics. | Root-to-leaf path expansion (ECIP §5.5, JAP §7). |
2837
+ | **Array item identity (strict)** | Index fallback tolerated. | Stable `id` required for editable object arrays. |
2838
+
2839
+ **Rule:** Every page section (non-header/footer) that appears in the Stage and in `SECTION_SCHEMAS` must comply with §6, §7, §4.4, §8, §9, §10 for full Studio UX.
2840
+
2841
+ ---
2842
+
2843
+ ## Summary of v1.3 Additions
2844
+
2845
+ | § | Title | Purpose |
2846
+ |---|--------|--------|
2847
+ | 5.5 | Path-Only Nested Selection & Expansion | ECIP: root→leaf `SelectionPath`; remove flat matching in strict mode. |
2848
+ | 6.5 | Strict Path Extraction for Nested Arrays | IDAC: path-based nested targeting; no strict flat fallback. |
2849
+ | 10.3 | Studio Selection Event Contract | JEB: `SECTION_SELECT` uses `itemPath`; remove `itemField/itemId`. |
2850
+ | JAP §7 | Path-Deterministic Selection & Sidebar Expansion | Studio state synchronization for nested arrays. |
2851
+ | Compliance | Legacy vs Full UX (v1.3) | Explicit breaking delta for flat protocol removal and strict IDs. |
2852
+ | **Appendix A.6** | **v1.3 Path/Nested Strictness Addendum** | Type/export and migration checklist for path-only protocol. |
2853
+
2854
+ ---
2855
+
2856
+ # Appendix A — Tenant Type & Code-Generation Annex
2857
+
2858
+ **Objective:** Make the specification **sufficient** to generate or audit a full tenant (new site, new components, new data) without a reference codebase. Defines TypeScript types, JSON shapes, schema contract, file paths, and integration pattern.
2859
+
2860
+ **Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.
2861
+
2862
+ ---
2863
+
2864
+ ## A.1 Core-Provided Types (from `@olonjs/core`)
2865
+
2866
+ The following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.
2867
+
2868
+ | Type | Description |
2869
+ |------|-------------|
2870
+ | **SectionType** | `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. |
2871
+ | **Section** | Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. |
2872
+ | **BaseSectionSettings** | Optional base type for section settings (may align with BSDS §8.3). |
2873
+ | **MenuItem** | Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). |
2874
+ | **AddSectionConfig** | See §9. |
2875
+ | **JsonPagesConfig** | See §10.1. |
2876
+
2877
+ **Perché servono (A.1):** Il Tenant deve conoscere i tipi esportati dal Core (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig) per tipizzare registry, config e augmentation senza dipendere da implementazioni interne.
2878
+
2879
+ ---
2880
+
2881
+ ## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)
2882
+
2883
+ The Tenant **must** define the following in one module (e.g. **`src/types.ts`**). This module **must** perform the **module augmentation** of `@olonjs/core` for **SectionDataRegistry** and **SectionSettingsRegistry**, and **must** export **SectionComponentPropsMap** and re-export from `@olonjs/core` so that **SectionType** is available after augmentation.
2884
+
2885
+ ### A.2.1 SectionComponentPropsMap
2886
+
2887
+ Maps each section type to the props of its React component. **Header** is the only type that receives **menu**.
2888
+
2889
+ **Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.
2890
+
2891
+ ```typescript
2892
+ import type { MenuItem } from '@olonjs/core';
2893
+ // Import Data/Settings from each capsule.
2894
+
2895
+ export type SectionComponentPropsMap = {
2896
+ 'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };
2897
+ 'footer': { data: FooterData; settings?: FooterSettings };
2898
+ 'hero': { data: HeroData; settings?: HeroSettings };
2899
+ // ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.
2900
+ };
2901
+ ```
2902
+
2903
+ **Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**
2904
+
2905
+ ```typescript
2906
+ import type { MenuItem } from '@olonjs/core';
2907
+
2908
+ export type SectionComponentPropsMap = {
2909
+ [K in SectionType]: K extends 'header'
2910
+ ? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }
2911
+ : { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };
2912
+ };
2913
+ ```
2914
+
2915
+ SectionType is imported from Core (after Tenant augmentation). In practice Option A is the reference pattern; Option B is valid if the Tenant prefers a single derived definition.
2916
+
2917
+ **Perché servono (A.2):** SectionComponentPropsMap e i tipi di config (PageConfig, SiteConfig, MenuConfig, ThemeConfig) definiscono il contratto tra dati (JSON, API) e componente; l’augmentation è l’unico modo per estendere i registry del Core senza fork. Senza questi tipi, generazione tenant e refactor sarebbero senza guida e il type-check fallirebbe.
2918
+
2919
+ ### A.2.2 ComponentRegistry type
2920
+
2921
+ The registry object **must** be typed as:
2922
+
2923
+ ```typescript
2924
+ import type { SectionType } from '@olonjs/core';
2925
+ import type { SectionComponentPropsMap } from '@/types';
2926
+
2927
+ export const ComponentRegistry: {
2928
+ [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;
2929
+ } = { /* ... */ };
2930
+ ```
2931
+
2932
+ File: **`src/lib/ComponentRegistry.tsx`** (or equivalent). Imports one View per section type and assigns it to the corresponding key.
2933
+
2934
+ ### A.2.3 PageConfig
2935
+
2936
+ Minimum shape for a single page (used in **pages** and in each **`[slug].json`**):
2937
+
2938
+ ```typescript
2939
+ export interface PageConfig {
2940
+ id?: string;
2941
+ slug: string;
2942
+ meta?: {
2943
+ title?: string;
2944
+ description?: string;
2945
+ };
2946
+ sections: Section[];
3696
2947
  }
2948
+ ```
3697
2949
 
3698
- export default App;
2950
+ **Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.
2951
+
2952
+ ### A.2.4 SiteConfig
2953
+
2954
+ Minimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):
2955
+
2956
+ ```typescript
2957
+ export interface SiteConfigIdentity {
2958
+ title?: string;
2959
+ logoUrl?: string;
2960
+ }
2961
+
2962
+ export interface SiteConfig {
2963
+ identity?: SiteConfigIdentity;
2964
+ pages?: Array<{ slug: string; label: string }>;
2965
+ header: {
2966
+ id: string;
2967
+ type: 'header';
2968
+ data: HeaderData;
2969
+ settings?: HeaderSettings;
2970
+ };
2971
+ footer: {
2972
+ id: string;
2973
+ type: 'footer';
2974
+ data: FooterData;
2975
+ settings?: FooterSettings;
2976
+ };
2977
+ }
2978
+ ```
2979
+
2980
+ **HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.
2981
+
2982
+ ### A.2.5 MenuConfig
2983
+
2984
+ Minimum shape for **menu.json** (and for **menuConfig** in JsonPagesConfig). Structure is tenant-defined; Core expects the header to receive **MenuItem[]**. Common pattern: an object with a key (e.g. **main**) whose value is **MenuItem[]**.
2985
+
2986
+ ```typescript
2987
+ export interface MenuConfig {
2988
+ main?: MenuItem[];
2989
+ [key: string]: MenuItem[] | undefined;
2990
+ }
2991
+ ```
2992
+
2993
+ Or simply **`MenuItem[]`** if the app uses a single flat list. The Tenant must ensure that the value passed to the header component as **menu** conforms to **MenuItem[]** (e.g. `menuConfig.main` or `menuConfig` if it is the array).
2994
+
2995
+ ### A.2.6 ThemeConfig
2996
+
2997
+ Minimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). Tenant-defined; typically tokens for colors, typography, radius.
2998
+
2999
+ ```typescript
3000
+ export interface ThemeConfig {
3001
+ name?: string;
3002
+ tokens?: {
3003
+ colors?: Record<string, string>;
3004
+ typography?: Record<string, string | Record<string, string>>;
3005
+ borderRadius?: Record<string, string>;
3006
+ };
3007
+ [key: string]: unknown;
3008
+ }
3009
+ ```
3010
+
3011
+ ---
3012
+
3013
+ ## A.3 Schema Contract (SECTION_SCHEMAS)
3014
+
3015
+ **Location:** **`src/lib/schemas.ts`** (or equivalent).
3016
+
3017
+ **Contract:**
3018
+ * **SECTION_SCHEMAS** is a **single object** whose keys are **SectionType** and whose values are **Zod schemas for the section data** (not settings, unless the Form Factory contract expects a combined or per-type settings schema; then each value may be the data schema only, and settings may be defined per capsule and aggregated elsewhere if needed).
3019
+ * The Tenant **must** re-export **BaseSectionData**, **BaseArrayItem**, and optionally **BaseSectionSettingsSchema** from **`src/lib/base-schemas.ts`** (or equivalent). Each capsule’s data schema **must** extend BaseSectionData; each array item schema **must** extend or include BaseArrayItem.
3020
+ * **SECTION_SCHEMAS** is typed as **`Record<SectionType, ZodType>`** or **`{ [K in SectionType]: ZodType }`** so that keys match the registry and SectionDataRegistry.
3021
+
3022
+ **Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.
3023
+
3024
+ **Perché servono (A.3):** Un unico oggetto SECTION_SCHEMAS con chiavi = SectionType e valori = schema data permette al Form Factory di costruire form per tipo senza convenzioni ad hoc; i base schema garantiscono anchorId e id su item. Senza questo contratto, l’Inspector non saprebbe quali campi mostrare né come validare.
3025
+
3026
+ ---
3027
+
3028
+ ## A.4 File Paths & Data Layout
3029
+
3030
+ | Purpose | Path (conventional) | Description |
3031
+ |---------|---------------------|-------------|
3032
+ | Site config | **`src/data/config/site.json`** | SiteConfig (identity, header, footer, pages list). |
3033
+ | Menu config | **`src/data/config/menu.json`** | MenuConfig (e.g. main nav). |
3034
+ | Theme config | **`src/data/config/theme.json`** | ThemeConfig (tokens). |
3035
+ | Page data | **`src/data/pages/<slug>.json`** | One file per page; content is PageConfig (slug, meta, sections). |
3036
+ | Base schemas | **`src/lib/base-schemas.ts`** | BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. |
3037
+ | Schema aggregate | **`src/lib/schemas.ts`** | SECTION_SCHEMAS; re-exports base schemas. |
3038
+ | Registry | **`src/lib/ComponentRegistry.tsx`** | ComponentRegistry object. |
3039
+ | Add-section config | **`src/lib/addSectionConfig.ts`** | addSectionConfig (AddSectionConfig). |
3040
+ | Tenant types & augmentation | **`src/types.ts`** | SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from Core. |
3041
+ | Bootstrap | **`src/App.tsx`** | Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders **<JsonPagesEngine config={config} />**. |
3042
+
3043
+ The app entry (e.g. **main.tsx**) renders **App**. No other bootstrap contract is specified; the Tenant may use Vite aliases (e.g. **@/**) for the paths above.
3044
+
3045
+ **Perché servono (A.4):** Path fissi (data/config, data/pages, lib/schemas, types.ts, App.tsx) permettono a CLI, tooling e agenti di trovare sempre gli stessi file; l’onboarding e la generazione da spec sono deterministici. Senza convenzione, ogni tenant sarebbe una struttura diversa.
3046
+
3047
+ ---
3048
+
3049
+ ## A.5 Integration Checklist (Code-Generation)
3050
+
3051
+ When generating or auditing a tenant, ensure the following in order:
3052
+
3053
+ 1. **Capsules** — For each section type, create **`src/components/<type>/`** with View.tsx, schema.ts, types.ts, index.ts. Data schema extends BaseSectionData; array items extend BaseArrayItem; View complies with CIP and IDAC (§6.2–6.3 for non-reserved types).
3054
+ 2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).
3055
+ 3. **types.ts** — Define SectionComponentPropsMap (header with **menu**), PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** and augment SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`.
3056
+ 4. **ComponentRegistry** — Import every View; build object **{ [K in SectionType]: ViewComponent }**; type as **{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }**.
3057
+ 5. **schemas.ts** — Import base schemas and each capsule’s data schema; export SECTION_SCHEMAS as **{ [K in SectionType]: SchemaK }**; export SectionType as **keyof typeof SECTION_SCHEMAS** if not using Core’s SectionType.
3058
+ 6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.
3059
+ 7. **App.tsx** — Import site, theme, menu, pages from data paths; build config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss: { tenant }, addSection); render JsonPagesEngine.
3060
+ 8. **Data files** — Create or update site.json, menu.json, theme.json, and one or more **<slug>.json** under the paths in A.4. Ensure JSON shapes match SiteConfig, MenuConfig, ThemeConfig, PageConfig.
3061
+ 9. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible.
3062
+ 10. **Reserved types** — Header and footer capsules receive props per SectionComponentPropsMap; menu is populated from menuConfig (e.g. menuConfig.main) when building the config or inside Core when rendering the header.
3063
+
3064
+ **Perché servono (A.5):** La checklist in ordine evita di dimenticare passi (es. augmentation prima del registry, TOCC dopo le View) e rende la spec sufficiente per generare o verificare un tenant senza codebase di riferimento.
3065
+
3066
+ ---
3699
3067
 
3068
+ ## A.6 v1.3 Path/Nested Strictness Addendum (breaking)
3069
+
3070
+ This addendum extends Appendix A without removing prior v1.2 obligations:
3071
+
3072
+ 1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.
3073
+ 2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.
3074
+ 3. **Nested array compliance** — For editable object arrays, item identity must be stable (`id`) and propagated to DOM attributes (`data-jp-item-id`), schema items (BaseArrayItem), and selection path segments (`itemId` when segment targets array item).
3075
+ 4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.
3076
+
3077
+ ---
3078
+
3079
+ **Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3 path types for Studio selection).
3080
+ **Distribution:** Core via `.yalc`; tenant projections via `@olonjs/cli`. This annex makes the spec **necessary and sufficient** for tenant code-generation and governance at enterprise grade.
3700
3081
 
3701
3082
  END_OF_FILE_CONTENT
3702
- echo "Creating src/App_.tsx..."
3703
- cat << 'END_OF_FILE_CONTENT' > "src/App_.tsx"
3083
+ mkdir -p "src"
3084
+ echo "Creating src/App.tsx..."
3085
+ cat << 'END_OF_FILE_CONTENT' > "src/App.tsx"
3704
3086
  /**
3705
3087
  * Thin Entry Point (Tenant).
3706
3088
  * Data from getHydratedData (file-backed or draft); assets from public/assets/images.
@@ -3723,6 +3105,7 @@ import menuData from '@/data/config/menu.json';
3723
3105
  import { getFilePages } from '@/lib/getFilePages';
3724
3106
  import { DopaDrawer } from '@/components/save-drawer/DopaDrawer';
3725
3107
  import { Skeleton } from '@/components/ui/skeleton';
3108
+ import { ThemeProvider } from '@/components/ThemeProvider';
3726
3109
 
3727
3110
  import tenantCss from './index.css?inline';
3728
3111
 
@@ -3733,7 +3116,12 @@ const CLOUD_API_KEY =
3733
3116
  import.meta.env.VITE_OLONJS_API_KEY ?? import.meta.env.VITE_JSONPAGES_API_KEY;
3734
3117
 
3735
3118
  const themeConfig = themeData as unknown as ThemeConfig;
3736
- const menuConfig = menuData as unknown as MenuConfig;
3119
+ const menuConfig: MenuConfig = { main: [] };
3120
+ const refDocuments = {
3121
+ 'menu.json': menuData,
3122
+ 'config/menu.json': menuData,
3123
+ 'src/data/config/menu.json': menuData,
3124
+ } satisfies NonNullable<JsonPagesConfig['refDocuments']>;
3737
3125
  const TENANT_ID = 'alpha';
3738
3126
 
3739
3127
  const filePages = getFilePages();
@@ -3817,6 +3205,13 @@ function asString(value: unknown, fallback: string): string {
3817
3205
  return typeof value === 'string' && value.trim() ? value : fallback;
3818
3206
  }
3819
3207
 
3208
+ function normalizeRouteSlug(value: string): string {
3209
+ return value
3210
+ .toLowerCase()
3211
+ .replace(/[^a-z0-9/_-]/g, '-')
3212
+ .replace(/^\/+|\/+$/g, '') || 'home';
3213
+ }
3214
+
3820
3215
  function coercePageConfig(slug: string, value: unknown): PageConfig | null {
3821
3216
  let input = value;
3822
3217
  if (typeof input === 'string') {
@@ -3862,11 +3257,8 @@ function coerceSiteConfig(value: unknown): SiteConfig | null {
3862
3257
  function toPagesRecord(value: unknown): Record<string, PageConfig> | null {
3863
3258
  const directPage = coercePageConfig('home', value);
3864
3259
  if (directPage) {
3865
- const directSlug = asString(directPage.slug, 'home')
3866
- .toLowerCase()
3867
- .replace(/[^a-z0-9/_-]/g, '-')
3868
- .replace(/^\/+|\/+$/g, '') || 'home';
3869
- return { [directSlug]: directPage };
3260
+ const directSlug = normalizeRouteSlug(asString(directPage.slug, 'home'));
3261
+ return { [directSlug]: { ...directPage, slug: directSlug } };
3870
3262
  }
3871
3263
 
3872
3264
  if (!isObjectRecord(value)) return null;
@@ -3874,14 +3266,10 @@ function toPagesRecord(value: unknown): Record<string, PageConfig> | null {
3874
3266
  for (const [rawKey, payload] of Object.entries(value)) {
3875
3267
  const rawKeyTrimmed = rawKey.trim();
3876
3268
  const slugFromNamespacedKey = rawKeyTrimmed.match(/^t_[a-z0-9-]+_page_(.+)$/i)?.[1];
3877
- const normalizedSlug = (slugFromNamespacedKey ?? rawKeyTrimmed)
3878
- .toLowerCase()
3879
- .replace(/[^a-z0-9/_-]/g, '-')
3880
- .replace(/^\/+|\/+$/g, '');
3881
- const slug = normalizedSlug || 'home';
3269
+ const slug = normalizeRouteSlug(slugFromNamespacedKey ?? rawKeyTrimmed);
3882
3270
  const page = coercePageConfig(slug, payload);
3883
3271
  if (!page) continue;
3884
- next[slug] = page;
3272
+ next[slug] = { ...page, slug };
3885
3273
  }
3886
3274
  return next;
3887
3275
  }
@@ -3891,9 +3279,11 @@ function normalizePageRegistry(value: unknown): Record<string, PageConfig> {
3891
3279
  const normalized: Record<string, PageConfig> = {};
3892
3280
 
3893
3281
  for (const [registrySlug, rawPageValue] of Object.entries(value)) {
3894
- const direct = coercePageConfig(registrySlug, rawPageValue);
3282
+ const canonicalSlug = normalizeRouteSlug(registrySlug);
3283
+ const direct = coercePageConfig(canonicalSlug, rawPageValue);
3895
3284
  if (direct) {
3896
- normalized[direct.slug || registrySlug] = direct;
3285
+ // Canonical key comes from registry/path, not from page JSON internal slug.
3286
+ normalized[canonicalSlug] = { ...direct, slug: canonicalSlug };
3897
3287
  continue;
3898
3288
  }
3899
3289
 
@@ -4022,6 +3412,15 @@ function buildThemeFontVarsCss(input: unknown): string {
4022
3412
  return `:root{--theme-font-primary:${primary};--theme-font-serif:${serif};--theme-font-mono:${mono};}`;
4023
3413
  }
4024
3414
 
3415
+ function setTenantPreviewReady(ready: boolean): void {
3416
+ if (typeof window !== 'undefined') {
3417
+ (window as Window & { __TENANT_PREVIEW_READY__?: boolean }).__TENANT_PREVIEW_READY__ = ready;
3418
+ }
3419
+ if (typeof document !== 'undefined' && document.body) {
3420
+ document.body.dataset.previewReady = ready ? '1' : '0';
3421
+ }
3422
+ }
3423
+
4025
3424
  function App() {
4026
3425
  const isCloudMode = Boolean(CLOUD_API_URL && CLOUD_API_KEY);
4027
3426
  const localInitialData = useMemo(() => (isCloudMode ? null : getInitialData()), [isCloudMode]);
@@ -4089,6 +3488,13 @@ function App() {
4089
3488
  };
4090
3489
  }, []);
4091
3490
 
3491
+ useEffect(() => {
3492
+ setTenantPreviewReady(false);
3493
+ return () => {
3494
+ setTenantPreviewReady(false);
3495
+ };
3496
+ }, []);
3497
+
4092
3498
  useEffect(() => {
4093
3499
  if (!isCloudMode || !CLOUD_API_URL || !CLOUD_API_KEY) {
4094
3500
  setContentMode('cloud');
@@ -4378,11 +3784,16 @@ function App() {
4378
3784
  siteConfig,
4379
3785
  themeConfig,
4380
3786
  menuConfig,
3787
+ refDocuments,
4381
3788
  themeCss: { tenant: `${buildThemeFontVarsCss(themeConfig)}\n${tenantCss}` },
4382
3789
  addSection: addSectionConfig,
3790
+ webmcp: {
3791
+ enabled: true,
3792
+ namespace: typeof window !== 'undefined' ? window.location.href : '',
3793
+ },
4383
3794
  persistence: {
4384
3795
  async saveToFile(state: ProjectState, slug: string): Promise<void> {
4385
- // 💻 LOCAL FILESYSTEM (Development / legacy fallback)
3796
+ // 💻 LOCAL FILESYSTEM (development path)
4386
3797
  console.log(`💻 Saving ${slug} to Local Filesystem...`);
4387
3798
  const res = await fetch('/api/save-to-file', {
4388
3799
  method: 'POST',
@@ -4508,8 +3919,30 @@ function App() {
4508
3919
 
4509
3920
  const shouldRenderEngine = !isCloudMode || hasInitialCloudResolved;
4510
3921
 
3922
+ useEffect(() => {
3923
+ if (!shouldRenderEngine) {
3924
+ setTenantPreviewReady(false);
3925
+ return;
3926
+ }
3927
+ let cancelled = false;
3928
+ let raf1 = 0;
3929
+ let raf2 = 0;
3930
+ raf1 = window.requestAnimationFrame(() => {
3931
+ raf2 = window.requestAnimationFrame(() => {
3932
+ if (!cancelled) setTenantPreviewReady(true);
3933
+ });
3934
+ });
3935
+ return () => {
3936
+ cancelled = true;
3937
+ window.cancelAnimationFrame(raf1);
3938
+ window.cancelAnimationFrame(raf2);
3939
+ setTenantPreviewReady(false);
3940
+ };
3941
+ }, [shouldRenderEngine, pages, siteConfig]);
3942
+
4511
3943
  return (
4512
- <>
3944
+ <ThemeProvider>
3945
+ <>
4513
3946
  {isCloudMode && showTopProgress ? (
4514
3947
  <>
4515
3948
  <style>
@@ -4629,7 +4062,8 @@ function App() {
4629
4062
  onClose={closeCloudDrawer}
4630
4063
  onRetry={retryCloudSave}
4631
4064
  />
4632
- </>
4065
+ </>
4066
+ </ThemeProvider>
4633
4067
  );
4634
4068
  }
4635
4069
 
@@ -10686,10 +10120,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/config/site.json"
10686
10120
  "brandText": "Olon",
10687
10121
  "copyright": "© 2025 OlonJS · v1.4 · Holon",
10688
10122
  "links": [
10689
- {
10690
- "label": "Docs",
10691
- "href": "/docs"
10692
- },
10693
10123
  {
10694
10124
  "label": "GitHub",
10695
10125
  "href": "#"
@@ -10888,27 +10318,25 @@ END_OF_FILE_CONTENT
10888
10318
  mkdir -p "src/data/pages"
10889
10319
  echo "Creating src/data/pages/design-system.json..."
10890
10320
  cat << 'END_OF_FILE_CONTENT' > "src/data/pages/design-system.json"
10891
- {
10892
- "id": "design-system-page",
10893
- "slug": "design-system",
10894
- "global-header": false,
10895
- "meta": {
10896
- "title": "Olon Design System Design Language",
10897
- "description": "Token reference, color system, typography, components and brand identity for the OlonJS design language."
10898
- },
10899
- "sections": [
10900
-
10901
- {
10902
- "id": "ds-main",
10903
- "type": "design-system",
10904
- "data": {
10905
- "title": "Olon"
10906
- },
10907
- "settings": {}
10908
- }
10909
- ]
10910
- }
10911
-
10321
+ {
10322
+ "id": "design-system-page",
10323
+ "slug": "design-system",
10324
+ "meta": {
10325
+ "title": "Olon Design System — Design Language",
10326
+ "description": "Token reference, color system, typography, components and brand identity for the OlonJS design language."
10327
+ },
10328
+ "sections": [
10329
+ {
10330
+ "id": "ds-main",
10331
+ "type": "design-system",
10332
+ "data": {
10333
+ "title": "Olon"
10334
+ },
10335
+ "settings": {}
10336
+ }
10337
+ ],
10338
+ "global-header": false
10339
+ }
10912
10340
  END_OF_FILE_CONTENT
10913
10341
  # SKIP: src/data/pages/design-system.json:Zone.Identifier is binary and cannot be embedded as text.
10914
10342
  echo "Creating src/data/pages/docs.json..."
@@ -10917,20 +10345,21 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/docs.json"
10917
10345
  "id": "docs-page",
10918
10346
  "slug": "docs",
10919
10347
  "meta": {
10920
- "title": "OlonJS Architecture Specifications v1.3",
10921
- "description": "Mandatory Standard — Sovereign Core Edition. Architecture, Studio/ICE UX, Path-Deterministic Nested Editing."
10348
+ "title": "OlonJS Architecture Specifications v1.5",
10349
+ "description": "Mandatory Standard — Sovereign Core Edition. Canonical Studio actions, SSG, Save to file, and Hot Save."
10922
10350
  },
10923
10351
  "sections": [
10924
10352
  {
10925
10353
  "id": "docs-main",
10926
10354
  "type": "tiptap",
10927
10355
  "data": {
10928
- "content": "# 📐 OlonJS Architecture Specifications v1.5s\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\n**Scope v1.5:** This edition preserves the complete v1.4 architecture (MTRP, JSP, TBP, CIP, ECIP, JAP + Studio/ICE UX contract: IDAC, TOCC, BSDS, ASC, JEB + Tenant Type & Code-Generation Annex + strict path-based/nested-array behavior) as a **faithful superset**, and upgrades **Local Design Tokens** from a principle to a deterministic implementation contract.\\\n⚠️ **Scope note (breaking):** In strict v1.3+ Studio semantics, the legacy flat protocol (`itemField` / `itemId`) is removed in favor of `itemPath` (root-to-leaf path segments).\\\nℹ️ **Scope note (clarification):** In v1.5, `theme.json` is the tenant theme source of truth for themed tenants; runtime theme publication is mandatory for compliant themed tenants; section-local tokens (`--local-*`) are the required scoping layer for section-owned color and radius concerns.\n\n---\n\n## 1. 📐 Modular Type Registry Pattern (MTRP) v1.2\n\n**Objective:** Establish a strictly typed, open-ended protocol for extending content data structures where the **Core Engine** is the orchestrator and the **Tenant** is the provider.\n\n### 1.1 The Sovereign Dependency Inversion\n\nThe **Core** defines the empty `SectionDataRegistry`. The **Tenant** \"injects\" its specific definitions using **Module Augmentation**. This allows the Core to be distributed as a compiled NPM package while remaining aware of Tenant-specific types at compile-time.\n\n### 1.2 Technical Implementation (`@olonjs/core/kernel`)\n\n```typescript\nexport interface SectionDataRegistry {} // Augmented by Tenant\nexport interface SectionSettingsRegistry {} // Augmented by Tenant\n\nexport interface BaseSection<K extends keyof SectionDataRegistry> {\n id: string;\n type: K;\n data: SectionDataRegistry[K];\n settings?: K extends keyof SectionSettingsRegistry\n ? SectionSettingsRegistry[K]\n : BaseSectionSettings;\n}\n\nexport type Section = {\n [K in keyof SectionDataRegistry]: BaseSection<K>\n}[keyof SectionDataRegistry];\n```\n\n**SectionType:** Core exports (or Tenant infers) `SectionType` as `keyof SectionDataRegistry`. After Tenant module augmentation, this is the union of all section type keys (e.g. `'header' | 'footer' | 'hero' | ...`). The Tenant uses this type for the ComponentRegistry and SECTION_SCHEMAS keys.\n\n**Why ❔:** The Core must be able to render section without knowing the concrete types to compile-time; the Tenant must be able to add new types without modifying the Core. Empty registry + module augmentation allow you to deploy Core as an NPM package and keep type-safety end-to-end (Section, registry, config). Without MTRP, each new type would require changes in the Core or weak types (any).\n\n---\n\n## 2. 📐 JsonPages Site Protocol (JSP) v1.8\n\n**Objective:** Define the deterministic file system and the **Sovereign Projection Engine** (CLI).\n\n### 2.1 The File System Ontology (The Silo Contract)\n\nEvery site must reside in an isolated directory. Global Governance is physically separated from Local Content.\n\n- `/config/site.json` — Global Identity & Reserved System Blocks (Header/Footer). See Appendix A for typed shape.\n- `/config/menu.json` — Navigation Tree (SSOT for System Header). See Appendix A.\n- `/config/theme.json` — Theme tokens for themed tenants. See Appendix A.\n- `/pages/[slug].json` — Local Body Content per page. See Appendix A (PageConfig).\n\n**Application path convention:** The runtime app typically imports these via an alias (e.g. `@/data/config/` and `@/data/pages/`). The physical silo may be `src/data/config/` and `src/data/pages/` so that `site.json`, `menu.json`, `theme.json` live under `src/data/config/`, and page JSONs under `src/data/pages/`. The CLI or projection script may use `/config/` and `/pages/` at repo root; the **contract** is that the app receives **siteConfig**, **menuConfig**, **themeConfig**, and **pages** as defined in JEB (§10) and Appendix A.\n\n**Rule:** For a tenant that claims v1.4 design-token compliance, `theme.json` is not optional in practice. If a tenant omits a physical `theme.json`, it must still provide an equivalent `ThemeConfig` object before bootstrap; otherwise the tenant is outside full v1.4 theme compliance.\n\n### 2.2 Deterministic Projection (CLI Workflow)\n\nThe CLI (`@olonjs/cli`) creates new tenants by:\n\n1. **Infra Projection:** Generating `package.json`, `tsconfig.json`, and `vite.config.ts` (The Shell).\n2. **Source Projection:** Executing a deterministic script (`src_tenant_alpha.sh`) to reconstruct the `src` folder (The DNA).\n3. **Dependency Resolution:** Enforcing specific versions of React, Radix, and Tailwind v4.\n\n**Why they are needed:** A deterministic file structure (config vs pages) separates global governance (site, menu, theme) from content per page; CLI can regenerate tenants and tooling can find data and schematics always in the same paths. Without JSP, each tenant would be an ad hoc structure and ingestion/export/Bake would be fragile.\n\n---\n\n## 3. 🧱 Tenant Block Protocol (TBP) v1.0\n\n**Objective:** Standardize the \"Capsule\" structure for components to enable automated ingestion (Pull) by the SaaS.\n\n### 3.1 The Atomic Capsule Structure\n\nComponents are self-contained directories under `src/components/<sectionType>/`:\n\n- `View.tsx` — The pure React component (Dumb View). Props: see Appendix A (SectionComponentPropsMap).\n- `schema.ts` — Zod schema(s) for the **data** contract (and optionally **settings**). Exports at least one schema (e.g. `HeroSchema`) used as the **data** schema for that type. Must extend BaseSectionData (§8) for data; array items must extend BaseArrayItem (§8).\n- `types.ts` — TypeScript interfaces inferred from the schema (e.g. `HeroData`, `HeroSettings`). Export types with names `<SectionType>Data` and `<SectionType>Settings` (or equivalent) so the Tenant can aggregate them in a single types module.\n- `index.ts` — Public API: re-exports View, schema(s), and types.\n\n### 3.2 Reserved System Types\n\n- `type: 'header'` — Reserved for `site.json`. Receives `menu: MenuItem[]` in addition to `data` and `settings`. Menu is sourced from `menu.json` (see Appendix A). The Tenant **must** type `SectionComponentPropsMap['header']` as `{ data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] }`.\n- `type: 'footer'` — Reserved for `site.json`. Props: `{ data: FooterData; settings?: FooterSettings }` only (no `menu`).\n- `type: 'sectionHeader'` — A standard local block. Must define its own `links` array in its local schema if used.\n\n**Perché servono:** La capsula (View + schema + types + index) è l’unità di estensione: il Core e il Form Factory possono scoprire tipi e contratti per tipo senza convenzioni ad hoc. Header/footer riservati evitano conflitti tra globale e locale. Senza TBP, aggregazione di SECTION_SCHEMAS e registry sarebbe incoerente e l’ingestion da SaaS non sarebbe automatizzabile.\n\n---\n\n## 4. 🧱 Component Implementation Protocol (CIP) v1.6\n\n**Objective:** Ensure system-wide stability and Admin UI integrity.\n\n1. **The \"Sovereign View\" Law:** Components receive `data` and `settings` (and `menu` for header only) and return JSX. They are metadata-blind (never import Zod schemas).\n2. **Z-Index Neutrality:** Components must not use `z-index > 1`. Layout delegation (sticky/fixed) is managed by the `SectionRenderer`.\n3. **Agnostic Asset Protocol:** Use `resolveAssetUrl(path, tenantId)` for all media. Resolved URLs are under `/assets/...` with no tenantId segment in the path (e.g. relative `img/hero.jpg` → `/assets/img/hero.jpg`).\n\n### 4.4 Local Design Tokens (v1.4)\n\n**Objective:** Standardize how a section consumes tenant theme values without leaking global styling assumptions into the section implementation.\n\n#### 4.4.1 The Required Four-Layer Chain\n\nFor any section that controls background, text color, border color, accent color, or radii, the following chain is normative:\n\n1. **Tenant theme source of truth** — Values are declared in `src/data/config/theme.json`.\n2. **Runtime theme publication** — The Core and/or tenant bootstrap **must** publish those values as CSS custom properties.\n3. **Section-local scope** — The View root **must** define `--local-*` variables mapped to the published theme variables for the concerns the section owns.\n4. **Rendered classes** — Section-owned color/radius utilities **must** consume `var(--local-*)`.\n\n**Rule:** A section may not skip layer 3 when it visually owns those concerns. Directly using global theme variables throughout the JSX is non-canonical for a fully themed section and must be treated as non-compliant unless the usage falls under an explicitly allowed exception.\n\n#### 4.4.2 Source Of Truth: `theme.json`\n\n`theme.json` is the tenant-level source of truth for theme values. Example:\n\n```json\n{\n \"name\": \"JsonPages Landing\",\n \"tokens\": {\n \"colors\": {\n \"primary\": \"#3b82f6\",\n \"secondary\": \"#22d3ee\",\n \"accent\": \"#60a5fa\",\n \"background\": \"#060d1b\",\n \"surface\": \"#0b1529\",\n \"surfaceAlt\": \"#101e38\",\n \"text\": \"#e2e8f0\",\n \"textMuted\": \"#94a3b8\",\n \"border\": \"#162a4d\"\n },\n \"typography\": {\n \"fontFamily\": {\n \"primary\": \"'Instrument Sans', system-ui, sans-serif\",\n \"mono\": \"'JetBrains Mono', monospace\",\n \"display\": \"'Bricolage Grotesque', system-ui, sans-serif\"\n }\n },\n \"borderRadius\": {\n \"sm\": \"0px\",\n \"md\": \"0px\",\n \"lg\": \"2px\"\n }\n }\n}\n```\n\n**Rule:** For a themed tenant, `theme.json` must contain the canonical semantic keys defined in Appendix A. Extra brand-specific keys are allowed only as extensions to those canonical groups, not as replacements for them.\n\n#### 4.4.3 Runtime Theme Publication\n\nThe tenant and/or Core **must** expose theme values as CSS variables before section rendering. The compliant bridge is a **three-layer chain** implemented in the tenant's `index.css`. Runtime publication is mandatory for themed tenants.\n\n##### Layer architecture\n\n```\ntheme.json → engine injection → :root bridge → @theme (Tailwind) → JSX classes\n```\n\n**Layer 0 — Engine injection (Core-provided)** `@olonjs/core` reads `theme.json` and injects all token values as flattened CSS custom properties before section rendering. The naming convention is:\n\nJSON path Injected CSS var `tokens.colors.{name}` `--theme-colors-{name}` `tokens.typography.fontFamily.{role}` `--theme-font-{role}` `tokens.typography.scale.{step}` `--theme-typography-scale-{step}` `tokens.typography.tracking.{name}` `--theme-typography-tracking-{name}` `tokens.typography.leading.{name}` `--theme-typography-leading-{name}` `tokens.typography.wordmark.*` `--theme-typography-wordmark-*` `tokens.borderRadius.{name}` `--theme-border-radius-{name}` `tokens.spacing.{name}` `--theme-spacing-{name}` `tokens.zIndex.{name}` `--theme-z-index-{name}` `tokens.modes.{mode}.colors.{name}` `--theme-modes-{mode}-colors-{name}`\n\nThe engine also publishes shorthand aliases for the most common radius and font tokens (e.g. `--theme-radius-sm`, `--theme-font-primary`). Tokens not covered by the shorthand aliases must be bridged in the tenant `:root`.\n\n**Layer 1 —** `:root` **semantic bridge (Tenant-provided,** `index.css`**)** The tenant maps engine-injected vars to its own semantic naming. **The naming in this layer is the tenant's sovereign choice** — it is not imposed by the Core. Any naming convention is valid as long as it is consistent throughout the tenant.\n\n```css\n:root {\n /* Backgrounds */\n --background: var(--theme-colors-background);\n --card: var(--theme-colors-card);\n --elevated: var(--theme-colors-elevated);\n --overlay: var(--theme-colors-overlay);\n --popover: var(--theme-colors-popover);\n --popover-foreground: var(--theme-colors-popover-foreground);\n\n /* Foregrounds */\n --foreground: var(--theme-colors-foreground);\n --card-foreground: var(--theme-colors-card-foreground);\n --muted-foreground: var(--theme-colors-muted-foreground);\n --placeholder: var(--theme-colors-placeholder);\n\n /* Brand ramp */\n --primary: var(--theme-colors-primary);\n --primary-foreground: var(--theme-colors-primary-foreground);\n --primary-light: var(--theme-colors-primary-light);\n --primary-dark: var(--theme-colors-primary-dark);\n /* ... full ramp --primary-50 through --primary-900 ... */\n\n /* Accent, secondary, muted, border, input, ring */\n --accent: var(--theme-colors-accent);\n --accent-foreground: var(--theme-colors-accent-foreground);\n --secondary: var(--theme-colors-secondary);\n --secondary-foreground: var(--theme-colors-secondary-foreground);\n --muted: var(--theme-colors-muted);\n --border: var(--theme-colors-border);\n --border-strong: var(--theme-colors-border-strong);\n --input: var(--theme-colors-input);\n --ring: var(--theme-colors-ring);\n\n /* Feedback */\n --destructive: var(--theme-colors-destructive);\n --destructive-foreground: var(--theme-colors-destructive-foreground);\n --success: var(--theme-colors-success);\n --success-foreground: var(--theme-colors-success-foreground);\n --warning: var(--theme-colors-warning);\n --warning-foreground: var(--theme-colors-warning-foreground);\n --info: var(--theme-colors-info);\n --info-foreground: var(--theme-colors-info-foreground);\n\n /* Typography scale, tracking, leading */\n --theme-text-xs: var(--theme-typography-scale-xs);\n --theme-text-sm: var(--theme-typography-scale-sm);\n /* ... full scale ... */\n --theme-tracking-tight: var(--theme-typography-tracking-tight);\n --theme-leading-normal: var(--theme-typography-leading-normal);\n /* ... */\n\n /* Spacing */\n --theme-container-max: var(--theme-spacing-container-max);\n --theme-section-y: var(--theme-spacing-section-y);\n --theme-header-h: var(--theme-spacing-header-h);\n --theme-sidebar-w: var(--theme-spacing-sidebar-w);\n\n /* Z-index */\n --z-base: var(--theme-z-index-base);\n --z-elevated: var(--theme-z-index-elevated);\n --z-dropdown: var(--theme-z-index-dropdown);\n --z-sticky: var(--theme-z-index-sticky);\n --z-overlay: var(--theme-z-index-overlay);\n --z-modal: var(--theme-z-index-modal);\n --z-toast: var(--theme-z-index-toast);\n}\n```\n\n**Layer 2 —** `@theme` **Tailwind v4 bridge (Tenant-provided,** `index.css`**)** Every semantic variable from Layer 1 is re-exposed under the Tailwind v4 `@theme` namespace so it becomes a utility class. Pattern: `--color-{slug}: var(--{slug})`.\n\n```css\n@theme {\n --color-background: var(--background);\n --color-card: var(--card);\n --color-foreground: var(--foreground);\n --color-primary: var(--primary);\n --color-accent: var(--accent);\n --color-border: var(--border);\n /* ... full token set ... */\n\n --font-primary: var(--theme-font-primary);\n --font-mono: var(--theme-font-mono);\n --font-display: var(--theme-font-display);\n\n --radius-sm: var(--theme-radius-sm);\n --radius-md: var(--theme-radius-md);\n --radius-lg: var(--theme-radius-lg);\n --radius-xl: var(--theme-radius-xl);\n --radius-full: var(--theme-radius-full);\n}\n```\n\nAfter this bridge, the full Tailwind utility vocabulary (`bg-primary`, `text-foreground`, `rounded-lg`, `font-display`, etc.) resolves to live theme values — with no hardcoded hex anywhere in the React layer.\n\n**Light mode / additional modes** are bridged by overriding the Layer 1 semantic vars under a `[data-theme=\"light\"]` selector (or equivalent), pointing to the engine-injected mode vars (`--theme-modes-light-colors-*`). The `@theme` layer requires no changes.\n\n**Rule:** A tenant `index.css` must implement all three layers. Skipping Layer 2 breaks Tailwind utility resolution. Skipping Layer 1 couples sections to engine-internal naming. Hardcoding values in either layer is non-compliant.\n\n#### 4.4.4 Section-Local Scope\n\nIf a section controls its own visual language, it **shall** establish a local token scope on the section root. Example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-text-muted': 'var(--muted-foreground)',\n '--local-primary': 'var(--primary)',\n '--local-border': 'var(--border)',\n '--local-surface': 'var(--card)',\n '--local-radius-sm': 'var(--theme-radius-sm)',\n '--local-radius-md': 'var(--theme-radius-md)',\n '--local-radius-lg': 'var(--theme-radius-lg)',\n } as React.CSSProperties}\n>\n```\n\n**Rule:** `--local-*` values must map to published theme variables. They must **not** be defined as hardcoded brand values such as `#fff`, `#111827`, `12px`, or `Inter, sans-serif` if those values belong to the tenant theme layer.\n\n**Rule:** Local tokens are **mandatory** for section-owned color and radius concerns. They are **optional** for font-family concerns unless the section must remap or isolate font roles locally.\n\n#### 4.4.5 Canonical Typography Rule\n\nTypography follows a deterministic rule distinct from color/radius:\n\n1. **Canonical font publication** — Tenant/Core must publish semantic font variables such as `--theme-font-primary`, `--theme-font-mono`, and `--theme-font-display` when those roles exist in the theme.\n2. **Canonical font consumption** — Sections must consume typography through semantic tenant font utilities or variables backed by those published theme roles (for example `.font-display` backed by `--font-display`, itself backed by `--theme-font-display`).\n3. **Local font tokens** — `--local-font-*` is optional and should be used only when a section needs to remap a font role locally rather than simply consume the canonical tenant font role.\n\nExample of canonical global semantic bridge:\n\n```css\n:root {\n --font-primary: var(--theme-font-primary);\n --font-display: var(--theme-font-display);\n}\n\n.font-display {\n font-family: var(--font-display, var(--font-primary));\n}\n```\n\n**Rule:** A section is compliant if it consumes themed fonts through this published semantic chain. It is **not** required to define `--local-font-display` unless the section needs local remapping. This closes the ambiguity between global semantic typography utilities and local color/radius scoping.\n\n#### 4.4.6 View Consumption\n\nAll section-owned classes that affect color or radius must consume local variables. Font consumption must follow the typography rule above. Example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-primary': 'var(--primary)',\n '--local-border': 'var(--border)',\n '--local-radius-md': 'var(--theme-radius-md)',\n '--local-radius-lg': 'var(--theme-radius-lg)',\n } as React.CSSProperties}\n className=\"bg-[var(--local-bg)]\"\n>\n <h1 className=\"font-display text-[var(--local-text)]\">Build Tenant DNA</h1>\n\n <a className=\"bg-[var(--local-primary)] rounded-[var(--local-radius-md)] text-white\">\n Read the Docs\n </a>\n\n <div className=\"border border-[var(--local-border)] rounded-[var(--local-radius-lg)]\">\n {/* illustration / mockup / card */}\n </div>\n</section>\n```\n\n#### 4.4.7 Compliance Rules\n\nA section is compliant when all of the following are true:\n\n1. `theme.json` is the source of truth for the theme values being used.\n2. Those values are published at runtime as CSS custom properties before the section renders.\n3. The section root defines a local token scope for the color/radius concerns it controls.\n4. Local color/radius tokens map to published theme variables rather than hardcoded literals.\n5. JSX classes use `var(--local-*)` for section-owned color/radius concerns.\n6. Fonts are consumed through the published semantic font chain, and only use local font tokens when local remapping is required.\n7. Hardcoded colors/radii are absent from the primary visual contract of the section.\n\n#### 4.4.8 Allowed Exceptions\n\nThe following are acceptable if documented and intentionally limited:\n\n- Tiny decorative one-off values that are not part of the tenant theme contract (for example an isolated translucent pixel-grid overlay).\n- Temporary compatibility shims during migration, provided the section still exposes a clear compliant path and the literal is not the primary themed value.\n- Semantic alias bridges in tenant CSS (for example `--font-display: var(--theme-font-display)`), as long as the source remains the theme layer.\n\n#### 4.4.9 Non-Compliant Patterns\n\nThe following are non-compliant:\n\n- `style={{ '--local-bg': '#060d1b' }}` when that background belongs to tenant theme.\n- Buttons using `rounded-[7px]`, `bg-blue-500`, `text-zinc-100`, or similar hardcoded utilities inside a section that claims to be theme-driven.\n- A section root that defines `--local-*`, but child elements still use raw `bg-*`, `text-*`, or `rounded-*` utilities for the same owned concerns.\n- Reading `theme.json` directly inside a View instead of consuming published runtime theme variables.\n- Treating brand-specific extension keys as a replacement for canonical semantic keys such as `primary`, `background`, `text`, `border`, or `fontFamily.primary`.\n\n#### 4.4.10 Practical Interpretation\n\n`--local-*` is not the source of truth. It is the **local scoping layer** between tenant theme and section implementation.\n\nCanonical chain:\n\n`theme.json` → published runtime theme vars → section `--local-*` → JSX classes\\`\n\nCanonical font chain:\n\n`theme.json` → published semantic font vars → tenant font utility/variable → section typography\\`\n\n### 4.5 Z-Index & Overlay Governance (v1.2)\n\nSection content root **must** stay at `z-index` **≤ 1** (prefer `z-0`) so the Sovereign Overlay can sit above with high z-index in Tenant CSS (§7). Header/footer may use a higher z-index (e.g. 50) only as a documented exception for global chrome.\n\n**Perché servono (CIP):** View “dumb” (solo data/settings) e senza import di Zod evita accoppiamento e permette al Form Factory di essere l’unica fonte di verità sugli schemi. Z-index basso evita che il contenuto copra l’overlay di selezione in Studio. Asset via `resolveAssetUrl`: i path relativi vengono risolti in `/assets/...` (senza segmento tenantId nel path). In v1.4 la catena `theme.json -> runtime vars -> --local-* -> JSX classes` rende i tenant temabili, riproducibili e compatibili con la Studio UX; senza questa separazione, stili “nudi” o valori hardcoded creano drift visivo, rompono il contratto del brand, e rendono ambiguo ciò che appartiene al tema contro ciò che appartiene alla section.\n\n---\n\n## 5. 🛠️ Editor Component Implementation Protocol (ECIP) v1.5\n\n**Objective:** Standardize the Polymorphic ICE engine.\n\n1. **Recursive Form Factory:** The Admin UI builds forms by traversing the Zod ontology.\n2. **UI Metadata:** Use `.describe('ui:[widget]')` in schemas to pass instructions to the Form Factory.\n3. **Deterministic IDs:** Every object in a `ZodArray` must extend `BaseArrayItem` (containing an `id`) to ensure React reconciliation stability during reordering.\n\n### 5.4 UI Metadata Vocabulary (v1.2)\n\nStandard keys for the Form Factory:\n\nKey Use case `ui:text` Single-line text input. `ui:textarea` Multi-line text. `ui:select` Enum / single choice. `ui:number` Numeric input. `ui:list` Array of items; list editor (add/remove/reorder). `ui:icon-picker` Icon selection.\n\nUnknown keys may be treated as `ui:text`. Array fields must use `BaseArrayItem` for items.\n\n### 5.5 Path-Only Nested Selection & Expansion (v1.3, breaking)\n\nIn strict v1.3 Studio/Inspector behavior, nested editing targets are represented by **path segments from root to leaf**.\n\n```typescript\nexport type SelectionPathSegment = { fieldKey: string; itemId?: string };\nexport type SelectionPath = SelectionPathSegment[];\n```\n\nRules:\n\n- Expansion and focus for nested arrays **must** be computed from `SelectionPath` (root → leaf), not from a single flat pair.\n- Matching by `fieldKey` alone is non-compliant for nested structures.\n- Legacy flat payload fields `itemField` and `itemId` are removed in strict v1.3 selection protocol.\n\n**Perché servono (ECIP):** Il Form Factory deve sapere quale widget usare (text, textarea, select, list, …) senza hardcodare per tipo; `.describe('ui:...')` è il contratto. BaseArrayItem con `id` su ogni item di array garantisce chiavi stabili in React e reorder/delete corretti nell’Inspector. In v1.3 la selezione/espansione path-only elimina ambiguità su array annidati: senza path completo root→leaf, la sidebar può aprire il ramo sbagliato o non aprire il target.\n\n---\n\n## 6. 🎯 ICE Data Attribute Contract (IDAC) v1.1\n\n**Objective:** Mandatory data attributes so the Stage (iframe) and Inspector can bind selection and field/item editing without coupling to Tenant DOM.\n\n### 6.1 Section-Level Markup (Core-Provided)\n\n**SectionRenderer** (Core) wraps each section root with:\n\n- `data-section-id` — Section instance ID (e.g. UUID). On the wrapper that contains content + overlay.\n- Sibling overlay element `data-jp-section-overlay` — Selection ring and type label. **Tenant does not add this;** Core injects it.\n\nTenant Views render the **content** root only (e.g. `<section>` or `<div>`), placed **inside** the Core wrapper.\n\n### 6.2 Field-Level Binding (Tenant-Provided)\n\nFor every **editable scalar field** the View **must** attach `data-jp-field=\"<fieldKey>\"` (key matches schema path: e.g. `title`, `description`, `sectionTitle`, `label`).\n\n### 6.3 Array-Item Binding (Tenant-Provided)\n\nFor every **editable array item** the View **must** attach:\n\n- `data-jp-item-id=\"<stableId>\"` — Prefer `item.id`; fallback e.g. `legacy-${index}` only outside strict mode.\n- `data-jp-item-field=\"<arrayKey>\"` — e.g. `cards`, `layers`, `products`, `paragraphs`.\n\n### 6.4 Compliance\n\n**Reserved types** (`header`, `footer`): ICE attributes optional unless Studio edits them. **All other section types** in the Stage and in `SECTION_SCHEMAS` **must** implement §6.2 and §6.3 for every editable field and array item.\n\n### 6.5 Strict Path Extraction for Nested Arrays (v1.3, breaking)\n\nFor nested array targets, the Core/Inspector contract is path-based:\n\n- The runtime selection target is expressed as `itemPath: SelectionPath` (root → leaf).\n- Flat identity (`itemField` + `itemId`) is not sufficient for nested structures and is removed in strict v1.3 payloads.\n- In strict mode, index-based identity fallback is non-compliant for editable object arrays.\n\n**Perché servono (IDAC):** Lo Stage è in un iframe e l’Inspector deve sapere **quale campo o item** corrisponde al click (o alla selezione) senza conoscere la struttura DOM del Tenant. `data-jp-field` associa un nodo DOM al path dello schema (es. `title`, `description`): così il Core può evidenziare la riga giusta nella sidebar, applicare opacità attivo/inattivo e aprire il form sul campo corretto. `data-jp-item-id` e `data-jp-item-field` fanno lo stesso per gli item di array (liste, reorder, delete). In v1.3, `itemPath` rende deterministico anche il caso nested (array dentro array), eliminando mismatch tra selezione canvas e ramo aperto in sidebar.\n\n---\n\n## 7. 🎨 Tenant Overlay CSS Contract (TOCC) v1.0\n\n**Objective:** The Stage iframe loads only Tenant HTML/CSS. Core injects overlay **markup** but does **not** ship overlay styles. The Tenant **must** supply CSS so overlay is visible.\n\n### 7.1 Required Selectors (Tenant global CSS)\n\n1. `[data-jp-section-overlay]` — `position: absolute; inset: 0`; `pointer-events: none`; base state transparent.\n2. `[data-section-id]:hover [data-jp-section-overlay]` — Hover: e.g. dashed border, subtle tint.\n3. `[data-section-id][data-jp-selected] [data-jp-section-overlay]` — Selected: solid border, optional tint.\n4. `[data-jp-section-overlay] > div` (type label) — Position and visibility (e.g. visible on hover/selected).\n\n### 7.2 Z-Index\n\nOverlay **z-index** high (e.g. 9999). Section content at or below CIP limit (§4.5).\n\n### 7.3 Responsibility\n\n**Core:** Injects wrapper and overlay DOM; sets `data-jp-selected`. **Tenant:** All overlay **visual** rules.\n\n**Perché servono (TOCC):** L’iframe dello Stage carica solo HTML/CSS del Tenant; il Core inietta il markup dell’overlay ma non gli stili. Senza CSS Tenant per i selettori TOCC, bordo hover/selected e type label non sarebbero visibili: l’autore non vedrebbe quale section è selezionata né il label del tipo. TOCC chiarisce la responsabilità (Core = markup, Tenant = aspetto) e garantisce UX uniforme tra tenant.\n\n---\n\n## 8. 📦 Base Section Data & Settings (BSDS) v1.0\n\n**Objective:** Standardize base schema fragments for anchors, array items, and section settings.\n\n### 8.1 BaseSectionData\n\nEvery section data schema **must** extend a base with at least `anchorId` (optional string). Canonical Zod (Tenant `lib/base-schemas.ts` or equivalent):\n\n```typescript\nexport const BaseSectionData = z.object({\n anchorId: z.string().optional().describe('ui:text'),\n});\n```\n\n### 8.2 BaseArrayItem\n\nEvery array item schema editable in the Inspector **must** include `id` (optional string minimum). Canonical Zod:\n\n```typescript\nexport const BaseArrayItem = z.object({\n id: z.string().optional(),\n});\n```\n\nRecommended: required UUID for new items. Used by `data-jp-item-id` and React reconciliation.\n\n### 8.3 BaseSectionSettings (Optional)\n\nCommon section-level settings. Canonical Zod (name **BaseSectionSettingsSchema** or as exported by Core):\n\n```typescript\nexport const BaseSectionSettingsSchema = z.object({\n paddingTop: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),\n paddingBottom: z.enum(['none', 'sm', 'md', 'lg', 'xl', '2xl']).default('md').describe('ui:select'),\n theme: z.enum(['dark', 'light', 'accent']).default('dark').describe('ui:select'),\n container: z.enum(['boxed', 'fluid']).default('boxed').describe('ui:select'),\n});\n```\n\nCapsules may extend this for type-specific settings. Core may export **BaseSectionSettings** as the TypeScript type inferred from this or a superset.\n\n**Perché servono (BSDS):** anchorId permette deep-link e navigazione in-page; id sugli array item è necessario per `data-jp-item-id`, reorder e React reconciliation. BaseSectionSettings comuni (padding, theme, container) evitano ripetizione e allineano il Form Factory tra capsule. Senza base condivisi, ogni capsule inventa convenzioni e validazione/add-section diventano fragili.\n\n---\n\n## 9. 📌 AddSectionConfig (ASC) v1.0\n\n**Objective:** Formalize the \"Add Section\" contract used by the Studio.\n\n**Type (Core exports** `AddSectionConfig`**):**\n\n```typescript\ninterface AddSectionConfig {\n addableSectionTypes: readonly string[];\n sectionTypeLabels: Record<string, string>;\n getDefaultSectionData(sectionType: string): Record<string, unknown>;\n}\n```\n\n**Shape:** Tenant provides one object (e.g. `addSectionConfig`) with:\n\n- `addableSectionTypes` — Readonly array of section type keys. Only these types appear in the Add Section Library. Must be a subset of (or equal to) the keys in SectionDataRegistry.\n- `sectionTypeLabels` — Map type key → display string (e.g. `{ hero: 'Hero', 'cta-banner': 'CTA Banner' }`).\n- `getDefaultSectionData(sectionType: string): Record<string, unknown>` — Returns default `data` for a new section. Must conform to the capsule’s data schema so the new section validates.\n\nCore creates a new section with deterministic UUID, `type`, and `data` from `getDefaultSectionData(type)`.\n\n**Perché servono (ASC):** Lo Studio deve mostrare una libreria “Aggiungi sezione” con nomi leggibili e, alla scelta, creare una section con dati iniziali validi. addableSectionTypes, sectionTypeLabels e getDefaultSectionData sono il contratto: il Tenant è l’unica fonte di verità su quali tipi sono addabili e con quali default. Senza ASC, il Core non saprebbe cosa mostrare in modal né come popolare i dati della nuova section.\n\n---\n\n## 10. ⚙️ JsonPagesConfig & Engine Bootstrap (JEB) v1.1\n\n**Objective:** Bootstrap contract between Tenant app and `@olonjs/core`.\n\n### 10.1 JsonPagesConfig (required fields)\n\nThe Tenant passes a single **config** object to **JsonPagesEngine**. Required fields:\n\nField Type Description **tenantId** string Passed to `resolveAssetUrl(path, tenantId)`; resolved asset URLs are `/assets/...` with no tenantId segment in the path. **registry** `{ [K in SectionType]: React.FC<SectionComponentPropsMap[K]> }` Component registry. Must match MTRP keys. See Appendix A. **schemas** `Record<SectionType, ZodType>` or equivalent SECTION_SCHEMAS: type → **data** Zod schema. Form Factory uses this. See Appendix A. **pages** `Record<string, PageConfig>` Slug → page config. See Appendix A. **siteConfig** SiteConfig Global site (identity, header/footer blocks). See Appendix A. **themeConfig** ThemeConfig Theme tokens. See Appendix A. **menuConfig** MenuConfig Navigation tree (SSOT for header menu). See Appendix A. **themeCss** `{ tenant: string }` At least **tenant**: string (inline CSS or URL) for Stage iframe injection. **addSection** AddSectionConfig Add-section config (§9).\n\nCore may define optional fields. The Tenant must not omit required fields.\n\n### 10.2 JsonPagesEngine\n\nRoot component: `<JsonPagesEngine config={config} />`. Responsibilities: route → page, SectionRenderer per section; in Studio mode Sovereign Shell (Inspector, Control Bar, postMessage); section wrappers and overlay per IDAC and JAP. Tenant does not implement the Shell.\n\n### 10.3 Studio Selection Event Contract (v1.3, breaking)\n\nIn strict v1.3 Studio, section selection payload for nested targets is path-based:\n\n```typescript\ntype SectionSelectMessage = {\n type: 'SECTION_SELECT';\n section: { id: string; type: string; scope: 'global' | 'local' };\n itemPath?: SelectionPath; // root -> leaf\n};\n```\n\nRemoved from strict protocol:\n\n- `itemField`\n- `itemId`\n\n**Perché servono (JEB):** Un unico punto di bootstrap (config + Engine) evita che il Tenant replichi logica di routing, Shell e overlay. I campi obbligatori in JsonPagesConfig (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss, addSection) sono il minimo per far funzionare rendering, Studio e Form Factory; omissioni causano errori a runtime. In v1.3, il payload `itemPath` sincronizza in modo non ambiguo Stage e Inspector su nested arrays.\n\n---\n\n# 🏛️ OlonJS_ADMIN_PROTOCOL (JAP) v1.2\n\n**Status:** Mandatory Standard\\\n**Version:** 1.2.0 (Sovereign Shell Edition — Path/Nested Strictness)\\\n**Objective:** Deterministic orchestration of the \"Studio\" environment (ICE Level 1).\n\n---\n\n## 1. The Sovereign Shell Topology\n\nThe Admin interface is a **Sovereign Shell** from `@olonjs/core`.\n\n1. **The Stage (Canvas):** Isolated Iframe; postMessage for data updates and selection mirroring. Section markup follows **IDAC** (§6); overlay styling follows **TOCC** (§7).\n2. **The Inspector (Sidebar):** Consumes Tenant Zod schemas to generate editors; binding via `data-jp-field` and `data-jp-item-*`.\n3. **The Control Bar:** Save, Export, Add Section.\n\n## 2. State Orchestration & Persistence\n\n- **Working Draft:** Reactive local state for unsaved changes.\n- **Sync Law:** Inspector changes → Working Draft → Stage via `STUDIO_EVENTS.UPDATE_DRAFTS`.\n- **Bake Protocol:** \"Bake HTML\" requests snapshot from Iframe, injects `ProjectState` as JSON, triggers download.\n\n## 3. Context Switching (Global vs. Local)\n\n- **Header/Footer** selection → Global Mode, `site.json`.\n- Any other section → Page Mode, current `[slug].json`.\n\n## 4. Section Lifecycle Management\n\n1. **Add Section:** Modal from Tenant `SECTION_SCHEMAS`; UUID + default data via **AddSectionConfig** (§9).\n2. **Reorder:** Inspector or Stage Overlay; array mutation in Working Draft.\n3. **Delete:** Confirmation; remove from array, clear selection.\n\n## 5. Stage Isolation & Overlay\n\n- **CSS Shielding:** Stage in Iframe; Tenant CSS does not leak into Admin.\n- **Sovereign Overlay:** Selection ring and type labels injected per **IDAC** (§6); Tenant styles them per **TOCC** (§7).\n\n## 6. \"Green Build\" Validation\n\nStudio enforces `tsc && vite build`. No export with TypeScript errors.\n\n## 7. Path-Deterministic Selection & Sidebar Expansion (v1.3, breaking)\n\n- Section/item focus synchronization uses `itemPath` (root → leaf), not flat `itemField/itemId`.\n- Sidebar expansion state for nested arrays must be derived from all path segments.\n- Flat-only matching may open/close wrong branches and is non-compliant in strict mode.\n\n**Perché servono (JAP):** Stage in iframe + Inspector + Control Bar separano il contesto di editing dal sito; postMessage e Working Draft permettono modifiche senza toccare subito i file. Bake ed Export richiedono uno stato coerente. Global vs Page mode evita confusione su dove si sta editando (site.json vs \\[slug\\].json). Add/Reorder/Delete sono gestiti in un solo modo (Working Draft + ASC). Green Build garantisce che ciò che si esporta compili. In v1.3, il path completo elimina ambiguità nella sincronizzazione Stage↔Sidebar su strutture annidate.\n\n---\n\n## Compliance: Legacy vs Full UX (v1.4)\n\nDimension Legacy / Less UX Full UX (Core-aligned) **ICE binding** No `data-jp-*`; Inspector cannot bind. IDAC (§6) on every editable section/field/item. **Section wrapper** Plain `<section>`; no overlay contract. Core wrapper + overlay; Tenant CSS per TOCC (§7). **Design tokens** Raw BEM / fixed classes, or local vars fed by literals. `theme.json` as source of truth, mandatory runtime publication, local color/radius scope via `--local-*`, typography via canonical semantic font chain, no primary hardcoded themed values. **Base schemas** Ad hoc. BSDS (§8): BaseSectionData, BaseArrayItem, BaseSectionSettings. **Add Section** Ad hoc defaults. ASC (§9): addableSectionTypes, labels, getDefaultSectionData. **Bootstrap** Implicit. JEB (§10): JsonPagesConfig + JsonPagesEngine. **Selection payload** Flat `itemField/itemId`. Path-only `itemPath: SelectionPath` (JEB §10.3). **Nested array expansion** Single-segment or field-only heuristics. Root-to-leaf path expansion (ECIP §5.5, JAP §7). **Array item identity (strict)** Index fallback tolerated. Stable `id` required for editable object arrays.\n\n**Rule:** Every page section (non-header/footer) that appears in the Stage and in `SECTION_SCHEMAS` must comply with §6, §7, §4.4, §8, §9, §10 for full Studio UX.\n\n---\n\n## Summary of v1.5 Additions\n\n§ Title Purpose 4.4.3 Three-Layer CSS Bridge Replaces the informal \"publish CSS vars\" rule with the deterministic Layer 0 (engine injection) → Layer 1 (`:root` semantic bridge) → Layer 2 (`@theme` Tailwind bridge) architecture. Documents the engine's `--theme-colors-{name}` naming convention and the tenant's sovereign naming freedom in Layer 1. A.2.6 ThemeConfig (v1.5) Replaces the incorrect `surface/surfaceAlt/text/textMuted` canonical keys with the actual schema-aligned keys (`card`, `elevated`, `foreground`, `muted-foreground`, etc.). Adds `spacing`, `zIndex`, full typography sub-interfaces (`scale`, `tracking`, `leading`, `wordmark`), and `modes`. Establishes `theme.json` as SOT with schema as the formalisation layer.\n\n---\n\n## Summary of v1.4 Additions\n\n§ Title Purpose 4.4 Local Design Tokens Makes the `theme.json -> runtime vars -> --local-* -> JSX classes` chain explicit and normative. 4.4.3 Runtime Theme Publication Makes runtime CSS publication mandatory for themed tenants. 4.4.5 Canonical Typography Rule Removes ambiguity between global semantic font utilities and local token scoping. 4.4.7 Compliance Rules Turns Local Design Tokens into a checklist-grade compliance contract. 4.4.9 Non-Compliant Patterns Makes hardcoded token anti-patterns explicit. **Appendix A.2.6** **Deterministic ThemeConfig** Aligns the spec-level theme contract with the core’s structured semantic keys plus extension policy. **Appendix A.7** **Local Design Tokens Implementation Addendum** Operational checklist and implementation examples for compliant tenant sections.\n\n---\n\n# Appendix A — Tenant Type & Code-Generation Annex\n\n**Objective:** Make the specification **sufficient** to generate or audit a full tenant (new site, new components, new data) without a reference codebase. Defines TypeScript types, JSON shapes, schema contract, file paths, and integration pattern.\n\n**Status:** Mandatory for code-generation and governance. Compliance ensures generated tenants are typed and wired like the reference implementation.\n\n---\n\n## A.1 Core-Provided Types (from `@olonjs/core`)\n\nThe following are assumed to be exported by Core. The Tenant augments **SectionDataRegistry** and **SectionSettingsRegistry**; all other types are consumed as-is.\n\nType Description **SectionType** `keyof SectionDataRegistry` (after Tenant augmentation). Union of all section type keys. **Section** Union of `BaseSection<K>` for all K in SectionDataRegistry. See MTRP §1.2. **BaseSectionSettings** Optional base type for section settings (may align with BSDS §8.3). **MenuItem** Navigation item. **Minimum shape:** `{ label: string; href: string }`. Core may extend (e.g. `children?: MenuItem[]`). **AddSectionConfig** See §9. **JsonPagesConfig** See §10.1.\n\n**Perché servono (A.1):** Il Tenant deve conoscere i tipi esportati dal Core (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig) per tipizzare registry, config e augmentation senza dipendere da implementazioni interne.\n\n---\n\n## A.2 Tenant-Provided Types (single source: `src/types.ts` or equivalent)\n\nThe Tenant **must** define the following in one module (e.g. `src/types.ts`). This module **must** perform the **module augmentation** of `@olonjs/core` for **SectionDataRegistry** and **SectionSettingsRegistry**, and **must** export **SectionComponentPropsMap** and re-export from `@olonjs/core` so that **SectionType** is available after augmentation.\n\n### A.2.1 SectionComponentPropsMap\n\nMaps each section type to the props of its React component. **Header** is the only type that receives **menu**.\n\n**Option A — Explicit (recommended for clarity and tooling):** For each section type K, add one entry. Header receives **menu**.\n\n```typescript\nimport type { MenuItem } from '@olonjs/core';\n// Import Data/Settings from each capsule.\n\nexport type SectionComponentPropsMap = {\n 'header': { data: HeaderData; settings?: HeaderSettings; menu: MenuItem[] };\n 'footer': { data: FooterData; settings?: FooterSettings };\n 'hero': { data: HeroData; settings?: HeroSettings };\n // ... one entry per SectionType, e.g. 'feature-grid', 'cta-banner', etc.\n};\n```\n\n**Option B — Mapped type (DRY, requires SectionDataRegistry/SectionSettingsRegistry in scope):**\n\n```typescript\nimport type { MenuItem } from '@olonjs/core';\n\nexport type SectionComponentPropsMap = {\n [K in SectionType]: K extends 'header'\n ? { data: SectionDataRegistry[K]; settings?: SectionSettingsRegistry[K]; menu: MenuItem[] }\n : { data: SectionDataRegistry[K]; settings?: K extends keyof SectionSettingsRegistry ? SectionSettingsRegistry[K] : BaseSectionSettings };\n};\n```\n\nSectionType is imported from Core (after Tenant augmentation). In practice Option A is the reference pattern; Option B is valid if the Tenant prefers a single derived definition.\n\n**Perché servono (A.2):** SectionComponentPropsMap e i tipi di config (PageConfig, SiteConfig, MenuConfig, ThemeConfig) definiscono il contratto tra dati (JSON, API) e componente; l’augmentation è l’unico modo per estendere i registry del Core senza fork. Senza questi tipi, generazione tenant e refactor sarebbero senza guida e il type-check fallirebbe.\n\n### A.2.2 ComponentRegistry type\n\nThe registry object **must** be typed as:\n\n```typescript\nimport type { SectionType } from '@olonjs/core';\nimport type { SectionComponentPropsMap } from '@/types';\n\nexport const ComponentRegistry: {\n [K in SectionType]: React.FC<SectionComponentPropsMap[K]>;\n} = { /* ... */ };\n```\n\nFile: `src/lib/ComponentRegistry.tsx` (or equivalent). Imports one View per section type and assigns it to the corresponding key.\n\n### A.2.3 PageConfig\n\nMinimum shape for a single page (used in **pages** and in each `[slug].json`):\n\n```typescript\nexport interface PageConfig {\n id?: string;\n slug: string;\n meta?: {\n title?: string;\n description?: string;\n };\n sections: Section[];\n}\n```\n\n**Section** is the union type from MTRP (§1.2). Each element of **sections** has **id**, **type**, **data**, **settings** and conforms to the capsule schemas.\n\n### A.2.4 SiteConfig\n\nMinimum shape for **site.json** (and for **siteConfig** in JsonPagesConfig):\n\n```typescript\nexport interface SiteConfigIdentity {\n title?: string;\n logoUrl?: string;\n}\n\nexport interface SiteConfig {\n identity?: SiteConfigIdentity;\n pages?: Array<{ slug: string; label: string }>;\n header: {\n id: string;\n type: 'header';\n data: HeaderData;\n settings?: HeaderSettings;\n };\n footer: {\n id: string;\n type: 'footer';\n data: FooterData;\n settings?: FooterSettings;\n };\n}\n```\n\n**HeaderData**, **FooterData**, **HeaderSettings**, **FooterSettings** are the types exported from the header and footer capsules.\n\n### A.2.5 MenuConfig\n\nMinimum shape for **menu.json** (and for **menuConfig** in JsonPagesConfig). Structure is tenant-defined; Core expects the header to receive **MenuItem\\[\\]**. Common pattern: an object with a key (e.g. **main**) whose value is **MenuItem\\[\\]**.\n\n```typescript\nexport interface MenuConfig {\n main?: MenuItem[];\n [key: string]: MenuItem[] | undefined;\n}\n```\n\nOr simply `MenuItem[]` if the app uses a single flat list. The Tenant must ensure that the value passed to the header component as **menu** conforms to **MenuItem\\[\\]** (e.g. `menuConfig.main` or `menuConfig` if it is the array).\n\n### A.2.6 ThemeConfig\n\nMinimum shape for **theme.json** (and for **themeConfig** in JsonPagesConfig). `theme.json` is the **source of truth** for the entire visual contract of the tenant. The schema (`design-system.schema.json`) is the machine-readable formalisation of this contract — if the TypeScript interfaces and the JSON Schema diverge, the JSON Schema wins.\n\n**Naming policy:** The keys within `tokens.colors` are the tenant's sovereign choice. The engine flattens all keys to `--theme-colors-{name}` regardless of naming convention. The required keys listed below are the ones the engine's `:root` bridge and the `@theme` Tailwind bridge must be able to resolve. Extra brand-specific keys are always allowed as additive extensions.\n\n```typescript\nexport interface ThemeColors {\n /* Required — backgrounds */\n background: string;\n card: string;\n elevated: string;\n overlay: string;\n popover: string;\n 'popover-foreground': string;\n\n /* Required — foregrounds */\n foreground: string;\n 'card-foreground': string;\n 'muted-foreground': string;\n placeholder: string;\n\n /* Required — brand */\n primary: string;\n 'primary-foreground': string;\n 'primary-light': string;\n 'primary-dark': string;\n\n /* Optional — brand ramp (50–900) */\n 'primary-50'?: string;\n 'primary-100'?: string;\n 'primary-200'?: string;\n 'primary-300'?: string;\n 'primary-400'?: string;\n 'primary-500'?: string;\n 'primary-600'?: string;\n 'primary-700'?: string;\n 'primary-800'?: string;\n 'primary-900'?: string;\n\n /* Required — accent, secondary, muted */\n accent: string;\n 'accent-foreground': string;\n secondary: string;\n 'secondary-foreground': string;\n muted: string;\n\n /* Required — border, form */\n border: string;\n 'border-strong': string;\n input: string;\n ring: string;\n\n /* Required — feedback */\n destructive: string;\n 'destructive-foreground': string;\n 'destructive-border': string;\n 'destructive-ring': string;\n success: string;\n 'success-foreground': string;\n 'success-border': string;\n 'success-indicator': string;\n warning: string;\n 'warning-foreground': string;\n 'warning-border': string;\n info: string;\n 'info-foreground': string;\n 'info-border': string;\n\n [key: string]: string | undefined;\n}\n\nexport interface ThemeFontFamily {\n primary: string;\n mono: string;\n display?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeWordmark {\n fontFamily: string;\n weight: string;\n width: string;\n}\n\nexport interface ThemeTypography {\n fontFamily: ThemeFontFamily;\n wordmark?: ThemeWordmark;\n scale?: Record<string, string>; /* xs sm base md lg xl 2xl 3xl 4xl 5xl 6xl 7xl */\n tracking?: Record<string, string>; /* tight display normal wide label */\n leading?: Record<string, string>; /* none tight snug normal relaxed */\n}\n\nexport interface ThemeBorderRadius {\n sm: string;\n md: string;\n lg: string;\n xl?: string;\n full?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeSpacing {\n 'container-max'?: string;\n 'section-y'?: string;\n 'header-h'?: string;\n 'sidebar-w'?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeZIndex {\n base?: string;\n elevated?: string;\n dropdown?: string;\n sticky?: string;\n overlay?: string;\n modal?: string;\n toast?: string;\n [key: string]: string | undefined;\n}\n\nexport interface ThemeModes {\n [mode: string]: { colors: Partial<ThemeColors> };\n}\n\nexport interface ThemeTokens {\n colors: ThemeColors;\n typography: ThemeTypography;\n borderRadius: ThemeBorderRadius;\n spacing?: ThemeSpacing;\n zIndex?: ThemeZIndex;\n modes?: ThemeModes;\n}\n\nexport interface ThemeConfig {\n name: string;\n tokens: ThemeTokens;\n}\n```\n\n**Rule:** `theme.json` is the single source of truth. All layers downstream (engine injection, `:root` bridge, `@theme` bridge, React JSX) are read-only consumers. No layer below `theme.json` may hardcode a value that belongs to the theme contract.\n\n**Rule:** Brand-specific extension keys (e.g. `colors.primary-50` through `primary-900`, custom spacing tokens) are always allowed as additive extensions within the canonical groups. They must not replace the required semantic keys.\n\n---\n\n## A.3 Schema Contract (SECTION_SCHEMAS)\n\n**Location:** `src/lib/schemas.ts` (or equivalent).\n\n**Contract:**\n\n- **SECTION_SCHEMAS** is a **single object** whose keys are **SectionType** and whose values are **Zod schemas for the section data** (not settings, unless the Form Factory contract expects a combined or per-type settings schema; then each value may be the data schema only, and settings may be defined per capsule and aggregated elsewhere if needed).\n- The Tenant **must** re-export **BaseSectionData**, **BaseArrayItem**, and optionally **BaseSectionSettingsSchema** from `src/lib/base-schemas.ts` (or equivalent). Each capsule’s data schema **must** extend BaseSectionData; each array item schema **must** extend or include BaseArrayItem.\n- **SECTION_SCHEMAS** is typed as `Record<SectionType, ZodType>` or `{ [K in SectionType]: ZodType }` so that keys match the registry and SectionDataRegistry.\n\n**Export:** The app imports **SECTION_SCHEMAS** and passes it as **config.schemas** to JsonPagesEngine. The Form Factory traverses these schemas to build editors.\n\n**Perché servono (A.3):** Un unico oggetto SECTION_SCHEMAS con chiavi = SectionType e valori = schema data permette al Form Factory di costruire form per tipo senza convenzioni ad hoc; i base schema garantiscono anchorId e id su item. Senza questo contratto, l’Inspector non saprebbe quali campi mostrare né come validare.\n\n---\n\n## A.4 File Paths & Data Layout\n\nPurpose Path (conventional) Description Site config `src/data/config/site.json` SiteConfig (identity, header, footer, pages list). Menu config `src/data/config/menu.json` MenuConfig (e.g. main nav). Theme config `src/data/config/theme.json` ThemeConfig (tokens). Page data `src/data/pages/<slug>.json` One file per page; content is PageConfig (slug, meta, sections). Base schemas `src/lib/base-schemas.ts` BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema. Schema aggregate `src/lib/schemas.ts` SECTION_SCHEMAS; re-exports base schemas. Registry `src/lib/ComponentRegistry.tsx` ComponentRegistry object. Add-section config `src/lib/addSectionConfig.ts` addSectionConfig (AddSectionConfig). Tenant types & augmentation `src/types.ts` SectionComponentPropsMap, PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** for SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`. Bootstrap `src/App.tsx` Imports config (site, theme, menu, pages), registry, schemas, addSection, themeCss; builds JsonPagesConfig; renders .\n\nThe app entry (e.g. **main.tsx**) renders **App**. No other bootstrap contract is specified; the Tenant may use Vite aliases (e.g. **@/**) for the paths above.\n\n**Perché servono (A.4):** Path fissi (data/config, data/pages, lib/schemas, types.ts, App.tsx) permettono a CLI, tooling e agenti di trovare sempre gli stessi file; l’onboarding e la generazione da spec sono deterministici. Senza convenzione, ogni tenant sarebbe una struttura diversa.\n\n---\n\n## A.5 Integration Checklist (Code-Generation)\n\nWhen generating or auditing a tenant, ensure the following in order:\n\n 1. **Capsules** — For each section type, create `src/components/<type>/` with View.tsx, schema.ts, types.ts, index.ts. Data schema extends BaseSectionData; array items extend BaseArrayItem; View complies with CIP and IDAC (§6.2–6.3 for non-reserved types).\n 2. **Base schemas** — **src/lib/base-schemas.ts** exports BaseSectionData, BaseArrayItem, BaseSectionSettingsSchema (and optional CtaSchema or similar shared fragments).\n 3. **types.ts** — Define SectionComponentPropsMap (header with **menu**), PageConfig, SiteConfig, MenuConfig, ThemeConfig; **declare module '@olonjs/core'** and augment SectionDataRegistry and SectionSettingsRegistry; re-export from `@olonjs/core`.\n 4. **ComponentRegistry** — Import every View; build object **{ \\[K in SectionType\\]: ViewComponent }**; type as **{ \\[K in SectionType\\]: React.FC&lt;SectionComponentPropsMap\\[K\\]&gt; }**.\n 5. **schemas.ts** — Import base schemas and each capsule’s data schema; export SECTION_SCHEMAS as **{ \\[K in SectionType\\]: SchemaK }**; export SectionType as **keyof typeof SECTION_SCHEMAS** if not using Core’s SectionType.\n 6. **addSectionConfig** — addableSectionTypes, sectionTypeLabels, getDefaultSectionData; export as AddSectionConfig.\n 7. **App.tsx** — Import site, theme, menu, pages from data paths; build config (tenantId, registry, schemas, pages, siteConfig, themeConfig, menuConfig, themeCss: { tenant }, addSection); render JsonPagesEngine.\n 8. **Data files** — Create or update site.json, menu.json, theme.json, and one or more **.json** under the paths in A.4. Ensure JSON shapes match SiteConfig, MenuConfig, ThemeConfig, PageConfig.\n 9. **Runtime theme publication** — Publish the theme contract as runtime CSS custom properties before themed sections render.\n10. **Tenant CSS** — Include TOCC (§7) selectors in global CSS so the Stage overlay is visible, and bridge semantic theme variables where needed.\n11. **Reserved types** — Header and footer capsules receive props per SectionComponentPropsMap; menu is populated from menuConfig (e.g. menuConfig.main) when building the config or inside Core when rendering the header.\n\n**Perché servono (A.5):** La checklist in ordine evita di dimenticare passi (es. augmentation prima del registry, TOCC dopo le View) e rende la spec sufficiente per generare o verificare un tenant senza codebase di riferimento.\n\n---\n\n## A.6 v1.3 Path/Nested Strictness Addendum (breaking)\n\nThis addendum extends Appendix A without removing prior v1.2 obligations:\n\n1. **Type exports** — Core and/or shared types module should expose `SelectionPathSegment` and `SelectionPath` for Studio messaging and Inspector expansion logic.\n2. **Protocol migration** — Replace flat payload fields `itemField` / `itemId` with `itemPath?: SelectionPath` in strict v1.3 channels.\n3. **Nested array compliance** — For editable object arrays, item identity must be stable (`id`) and propagated to DOM attributes (`data-jp-item-id`), schema items (BaseArrayItem), and selection path segments (`itemId` when segment targets array item).\n4. **Backward compatibility policy** — Legacy flat fields may exist only in transitional adapters outside strict mode; normative v1.3 contract is path-only.\n\n---\n\n## A.7 v1.4 Local Design Tokens Implementation Addendum\n\nThis addendum extends Appendix A without removing prior v1.3 obligations:\n\n1. **Theme source of truth** — Tenant theme values belong in `src/data/config/theme.json`.\n2. **Runtime publication** — Core and/or tenant bootstrap **must** expose those values as runtime CSS custom properties before section rendering.\n3. **Local scope** — A themed section must define `--local-*` variables on its root for the color/radius concerns it owns.\n4. **Class consumption** — Section-owned color/radius utilities must consume `var(--local-*)`, not raw hardcoded theme values.\n5. **Typography policy** — Fonts must consume the published semantic font chain; local font tokens are optional and only for local remapping.\n6. **Migration policy** — Hardcoded colors/radii may exist only as temporary compatibility shims or purely decorative exceptions, not as the primary section contract.\n\nCanonical implementation pattern:\n\n```text\ntheme.json -> published runtime theme vars -> section --local-* -> JSX classes\n```\n\nCanonical typography pattern:\n\n```text\ntheme.json -> published semantic font vars -> tenant font utility/variable -> section typography\n```\n\nMinimal compliant example:\n\n```tsx\n<section\n style={{\n '--local-bg': 'var(--background)',\n '--local-text': 'var(--foreground)',\n '--local-primary': 'var(--primary)',\n '--local-radius-md': 'var(--theme-radius-md)',\n } as React.CSSProperties}\n className=\"bg-[var(--local-bg)]\"\n>\n <h2 className=\"font-display text-[var(--local-text)]\">Title</h2>\n <a className=\"bg-[var(--local-primary)] rounded-[var(--local-radius-md)]\">CTA</a>\n</section>\n```\n\nDeterministic compliance checklist:\n\n1. Canonical semantic theme keys exist.\n2. Runtime publication exists.\n3. Section-local color/radius scope exists.\n4. Section-owned color/radius classes consume `var(--local-*)`.\n5. Fonts consume the semantic published font chain.\n6. Primary themed values are not hardcoded.\n\n---\n\n**Validation:** Align with current `@olonjs/core` exports (SectionType, MenuItem, AddSectionConfig, JsonPagesConfig, and in v1.3+ path types for Studio selection), with the deterministic `ThemeConfig` contract, and with the runtime theme publication contract used by tenant CSS.\\\n**Distribution:** Core via `.yalc`; tenant projections via `@olonjs/cli`. This annex makes the spec **necessary and sufficient** for tenant code-generation and governance at enterprise grade."
10356
+ "content": "# 📐 OlonJS Architecture Specifications v1.5\n\n**Status:** Mandatory Standard\\\n**Version:** 1.5.0 (Sovereign Core Edition — Architecture + Studio/ICE UX, Path-Deterministic Nested Editing, Deterministic Local Design Tokens, Three-Layer CSS Bridge Contract)\\\n**Target:** Senior Architects / AI Agents / Enterprise Governance\n\nThis tenant follows the current OlonJS source-of-truth model: the tenant app owns content, schemas, theme, and persistence wiring; `@olonjs/core` owns the Studio shell, routing, preview, and editing engine.\n\n---\n\n## Canonical Editorial Flows\n\nThe supported Studio flows are now:\n\n- `SSG` for static HTML and route output.\n- `Save to file` for local JSON persistence back into tenant source files.\n- `Hot Save` for cloud/editorial persistence when the tenant config provides it.\n- `Add Section` for deterministic section lifecycle management inside Studio.\n\nPrevious one-off bake and JSON export paths are no longer part of Studio.\n\n---\n\n## Persistence Model\n\n`@olonjs/core` no longer performs HTML bake or ZIP export. Studio now invokes tenant-provided persistence callbacks:\n\n- `saveToFile(state, slug)`\n- `hotSave(state, slug)`\n\nThis keeps persistence explicit, tenant-owned, and aligned with the current `JsonPagesConfig` contract.\n\n---\n\n## Tenant Source Of Truth\n\n`apps/tenant-alpha` is the DNA source of truth for this tenant. Generated CLI templates are downstream artifacts and should be regenerated from source apps instead of being edited manually.\n\nThe canonical content and design files remain:\n\n- `src/data/config/site.json`\n- `src/data/config/menu.json`\n- `src/data/config/theme.json`\n- `src/data/pages/<slug>.json`\n\n---\n\n## Reference Specs\n\nUse these monorepo sources for the full protocol and architecture details:\n\n- `specs/olonjsSpecs_V_1_5.md`\n- `apps/tenant-alpha/specs/olonjsSpecs_V.1.3.md`\n\nThese source specs are the maintained references for architecture, Studio behavior, and tenant compliance."
10929
10357
  },
10930
10358
  "settings": {}
10931
10359
  }
10932
10360
  ]
10933
10361
  }
10362
+
10934
10363
  END_OF_FILE_CONTENT
10935
10364
  echo "Creating src/data/pages/home.json..."
10936
10365
  cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
@@ -10942,13 +10371,12 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
10942
10371
  "description": "OlonJS standardizes machine-readable web content across tenants. Predictable page endpoints for agents, typed schema contracts, repeatable governance."
10943
10372
  },
10944
10373
  "sections": [
10945
-
10946
10374
  {
10947
10375
  "id": "hero-main",
10948
10376
  "type": "hero",
10949
10377
  "data": {
10950
10378
  "eyebrow": "Contract layer · v1.4 · Open Core",
10951
- "title": "Start building ",
10379
+ "title": "Start Building",
10952
10380
  "titleHighlight": "for the agentic web.",
10953
10381
  "description": "AI agents are becoming operational actors in commerce, marketing, and support. But websites are still built for humans first: HTML-heavy, CMS-fragmented, and inconsistent across properties. That makes agent integration slow, brittle, and expensive. Olon introduces a deterministic machine contract for websites OlonJS. This makes content reliably readable and operable by agents while preserving normal human UI.",
10954
10382
  "ctas": [
@@ -10965,8 +10393,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
10965
10393
  "variant": "secondary"
10966
10394
  }
10967
10395
  ],
10968
- "docsLabel": "Read the docs",
10969
- "docsHref": "#",
10396
+ "docsLabel": "Explore platform",
10397
+ "docsHref": "/platform/overview",
10970
10398
  "heroImage": {
10971
10399
  "url": "https://bat5elmxofxdroan.public.blob.vercel-storage.com/tenant-assets/511f18d7-d8ac-4292-ad8a-b0efa99401a3/1774286598548-adac7c36-9001-451d-9b16-c3787ac27f57-signup-hero-olon-graded_1_.png",
10972
10400
  "alt": ""
@@ -11114,7 +10542,6 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/home.json"
11114
10542
  }
11115
10543
  }
11116
10544
  ]
11117
-
11118
10545
  }
11119
10546
  END_OF_FILE_CONTENT
11120
10547
  mkdir -p "src/data/pages/platform"
@@ -11125,8 +10552,8 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/platform/overview.json"
11125
10552
  "id": "overview-page",
11126
10553
  "slug": "platform/overview",
11127
10554
  "meta": {
11128
- "title": "OlonJS — Documentation",
11129
- "description": "Architecture specifications, tenant protocol, and developer reference for the OlonJS contract layer."
10555
+ "title": "OlonJS — Platform Overview",
10556
+ "description": "Overview of the OlonJS platform, architecture direction, and product surface."
11130
10557
  },
11131
10558
  "sections": [
11132
10559
  {
@@ -11140,15 +10567,15 @@ cat << 'END_OF_FILE_CONTENT' > "src/data/pages/platform/overview.json"
11140
10567
  "href": "/"
11141
10568
  },
11142
10569
  {
11143
- "id": "crumb-docs",
11144
- "label": "Docs",
11145
- "href": "/doc"
10570
+ "id": "crumb-platform",
10571
+ "label": "Platform",
10572
+ "href": "/platform/overview"
11146
10573
  }
11147
10574
  ],
11148
10575
  "badge": "",
11149
10576
  "title": "Platform",
11150
10577
  "titleItalic": "Overview",
11151
- "description": "The platform overview page."
10578
+ "description": "High-level overview of the OlonJS platform."
11152
10579
  }
11153
10580
  }
11154
10581
  ]
@@ -11640,6 +11067,18 @@ export function getPageMeta(slug: string): { title: string; description: string
11640
11067
  return { title, description };
11641
11068
  }
11642
11069
 
11070
+ export function getWebMcpBuildState(): {
11071
+ pages: Record<string, PageConfig>;
11072
+ schemas: JsonPagesConfig['schemas'];
11073
+ siteConfig: SiteConfig;
11074
+ } {
11075
+ return {
11076
+ pages,
11077
+ schemas: SECTION_SCHEMAS as unknown as JsonPagesConfig['schemas'],
11078
+ siteConfig,
11079
+ };
11080
+ }
11081
+
11643
11082
  END_OF_FILE_CONTENT
11644
11083
  echo "Creating src/fonts.css..."
11645
11084
  cat << 'END_OF_FILE_CONTENT' > "src/fonts.css"
@@ -13331,7 +12770,7 @@ import react from '@vitejs/plugin-react';
13331
12770
  import tailwindcss from '@tailwindcss/vite';
13332
12771
  import path from 'path';
13333
12772
  import fs from 'fs';
13334
- import { fileURLToPath } from 'url';
12773
+ import { fileURLToPath, pathToFileURL } from 'url';
13335
12774
 
13336
12775
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13337
12776
 
@@ -13379,6 +12818,20 @@ function isTenantPageJsonRequest(req, pathname) {
13379
12818
  const viteOrStaticPrefixes = ['/api/', '/assets/', '/src/', '/node_modules/', '/public/', '/@'];
13380
12819
  return !viteOrStaticPrefixes.some((prefix) => pathname.startsWith(prefix));
13381
12820
  }
12821
+
12822
+ function normalizeManifestSlug(raw) {
12823
+ return decodeURIComponent(raw || '')
12824
+ .replace(/^\/+|\/+$/g, '')
12825
+ .replace(/\\/g, '/')
12826
+ .replace(/(\.schema)?\.json$/i, '');
12827
+ }
12828
+
12829
+ async function loadWebMcpBuilders() {
12830
+ const moduleUrl = pathToFileURL(
12831
+ path.resolve(__dirname, '..', '..', 'packages', 'core', 'src', 'lib', 'webmcp-contracts.mjs')
12832
+ ).href;
12833
+ return import(moduleUrl);
12834
+ }
13382
12835
  export default defineConfig({
13383
12836
  plugins: [
13384
12837
  react(),
@@ -13390,6 +12843,75 @@ export default defineConfig({
13390
12843
  const pathname = (req.url || '').split('?')[0];
13391
12844
  const isPageJsonRequest = isTenantPageJsonRequest(req, pathname);
13392
12845
 
12846
+ const handleManifestRequest = async () => {
12847
+ const { buildPageContract, buildPageManifest, buildSiteManifest } = await loadWebMcpBuilders();
12848
+ const ssrEntry = await server.ssrLoadModule('/src/entry-ssg.tsx');
12849
+ const buildState = ssrEntry.getWebMcpBuildState();
12850
+
12851
+ if (req.method === 'GET' && pathname === '/mcp-manifest.json') {
12852
+ sendJson(res, 200, buildSiteManifest({
12853
+ pages: buildState.pages,
12854
+ schemas: buildState.schemas,
12855
+ siteConfig: buildState.siteConfig,
12856
+ }));
12857
+ return true;
12858
+ }
12859
+
12860
+ const pageManifestMatch = pathname.match(/^\/mcp-manifests\/(.+)\.json$/i);
12861
+ if (pageManifestMatch && req.method === 'GET') {
12862
+ const slug = normalizeManifestSlug(pageManifestMatch[1]);
12863
+ const pageConfig = buildState.pages[slug];
12864
+ if (!pageConfig) {
12865
+ sendJson(res, 404, { error: 'Page manifest not found' });
12866
+ return true;
12867
+ }
12868
+
12869
+ sendJson(res, 200, buildPageManifest({
12870
+ slug,
12871
+ pageConfig,
12872
+ schemas: buildState.schemas,
12873
+ siteConfig: buildState.siteConfig,
12874
+ }));
12875
+ return true;
12876
+ }
12877
+
12878
+ const schemaMatch = pathname.match(/^\/schemas\/(.+)\.schema\.json$/i);
12879
+ if (!schemaMatch || req.method !== 'GET') return false;
12880
+
12881
+ const slug = normalizeManifestSlug(schemaMatch[1]);
12882
+ const pageConfig = buildState.pages[slug];
12883
+ if (!pageConfig) {
12884
+ sendJson(res, 404, { error: 'Schema contract not found' });
12885
+ return true;
12886
+ }
12887
+
12888
+ sendJson(res, 200, buildPageContract({
12889
+ slug,
12890
+ pageConfig,
12891
+ schemas: buildState.schemas,
12892
+ siteConfig: buildState.siteConfig,
12893
+ }));
12894
+ return true;
12895
+ };
12896
+
12897
+ if (
12898
+ req.method === 'GET' &&
12899
+ (
12900
+ pathname === '/mcp-manifest.json'
12901
+ || /^\/mcp-manifests\/.+\.json$/i.test(pathname)
12902
+ || /^\/schemas\/.+\.schema\.json$/i.test(pathname)
12903
+ )
12904
+ ) {
12905
+ void handleManifestRequest()
12906
+ .then((handled) => {
12907
+ if (!handled) next();
12908
+ })
12909
+ .catch((error) => {
12910
+ sendJson(res, 500, { error: error?.message || 'Manifest generation failed' });
12911
+ });
12912
+ return;
12913
+ }
12914
+
13393
12915
  if (isPageJsonRequest) {
13394
12916
  const normalizedPath = decodeURIComponent(pathname).replace(/\\/g, '/');
13395
12917
  const slug = normalizedPath.replace(/^\/+/, '').replace(/\.json$/i, '').replace(/^\/+|\/+$/g, '');
@@ -13455,9 +12977,21 @@ export default defineConfig({
13455
12977
  resolve: {
13456
12978
  alias: {
13457
12979
  '@': path.resolve(__dirname, './src'),
12980
+ '@olonjs/core': path.resolve(__dirname, '..', '..', 'packages', 'core', 'src', 'index.ts'),
13458
12981
  'next/link': path.resolve(__dirname, './src/shims/next-link.tsx'),
13459
12982
  },
13460
12983
  },
12984
+ server: {
12985
+ fs: {
12986
+ allow: [
12987
+ path.resolve(__dirname, '..', '..'),
12988
+ ],
12989
+ },
12990
+ watch: {
12991
+ usePolling: true,
12992
+ interval: 300,
12993
+ },
12994
+ },
13461
12995
  });
13462
12996
 
13463
12997