@ai-matrx/alchemy 0.8.31 → 0.8.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/actions.cjs.map +1 -1
- package/dist/actions.d.cts +5 -0
- package/dist/actions.d.ts +5 -0
- package/dist/actions.js.map +1 -1
- package/dist/checks.cjs +1 -1
- package/dist/checks.cjs.map +1 -1
- package/dist/checks.d.cts +4 -2
- package/dist/checks.d.ts +4 -2
- package/dist/checks.js +1 -1
- package/dist/checks.js.map +1 -1
- package/dist/menu.cjs +1 -1
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.d.cts +5 -0
- package/dist/menu.d.ts +5 -0
- package/dist/menu.js +1 -1
- package/dist/menu.js.map +1 -1
- package/dist/react-bar.cjs +1 -1
- package/dist/react-bar.cjs.map +1 -1
- package/dist/react-bar.js +1 -1
- package/dist/react-bar.js.map +1 -1
- package/dist/react-host.d.cts +5 -0
- package/dist/react-host.d.ts +5 -0
- package/dist/react-menu.cjs +1 -1
- package/dist/react-menu.cjs.map +1 -1
- package/dist/react-menu.js +1 -1
- package/dist/react-menu.js.map +1 -1
- package/dist/react-overflow.cjs +1 -1
- package/dist/react-overflow.cjs.map +1 -1
- package/dist/react-overflow.d.cts +5 -0
- package/dist/react-overflow.d.ts +5 -0
- package/dist/react-overflow.js +1 -1
- package/dist/react-overflow.js.map +1 -1
- package/dist/react-palette.cjs +1 -1
- package/dist/react-palette.cjs.map +1 -1
- package/dist/react-palette.js +1 -1
- package/dist/react-palette.js.map +1 -1
- package/dist/react-selection.cjs +1 -1
- package/dist/react-selection.cjs.map +1 -1
- package/dist/react-selection.js +1 -1
- package/dist/react-selection.js.map +1 -1
- package/dist/react-sheet.cjs +1 -1
- package/dist/react-sheet.cjs.map +1 -1
- package/dist/react-sheet.js +1 -1
- package/dist/react-sheet.js.map +1 -1
- package/package.json +3 -3
package/dist/checks.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/checks.ts","../src/declare/keys.ts","../src/declare/sensitivity.ts","../src/checks/sync.ts","../src/checks/routes.ts"],"sourcesContent":["/**\n * `@ai-matrx/alchemy/checks` — the ONE surface sync (CONTRACT §2.7): the row plan\n * every sync path executes, and its SQL rendering. Pure; no database client.\n */\nexport {\n GOVERNANCE_COLUMNS,\n conflictUpdateColumns,\n findStaleRows,\n mirrorRowKey,\n planColumns,\n planSurfaceSync,\n renderSurfaceSyncSql,\n renderSurfaceSyncStatements,\n sqlLiteral,\n type SurfaceRowPlan,\n type SurfaceSyncPlan,\n type SurfaceSyncPlanOptions,\n type SyncSchema,\n type SyncTablePlan,\n} from \"./checks/sync\";\nexport { findDeadUrlPatterns, routeSegments, urlPatternMatchesRoute } from \"./checks/routes\";\n","import type { QualifiedValueKey } from \"./types\";\n\n/** Every value, write-target, item-type and group name (the DB CHECK rule). */\nexport const DECLARATION_NAME_RE = /^[a-z][a-z0-9_]*$/;\n\n/** \"<client>/<local>\" — the surface naming convention. */\nexport const SURFACE_NAME_RE = /^[a-z][a-z0-9-]*\\/[a-z0-9-/]+$/;\n\n/** A bare screen value (`status`) or an item value (`table_row.status`). */\nexport const QUALIFIED_VALUE_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)?$/;\n\nexport interface ParsedValueKey {\n /** \"\" for a screen-level value. */\n itemType: string;\n name: string;\n}\n\n/** Split a qualified key; null when it is not a valid bare or qualified key. */\nexport function parseQualifiedValueKey(key: string): ParsedValueKey | null {\n if (!QUALIFIED_VALUE_KEY_RE.test(key)) return null;\n const dot = key.indexOf(\".\");\n if (dot === -1) return { itemType: \"\", name: key };\n return { itemType: key.slice(0, dot), name: key.slice(dot + 1) };\n}\n\nexport function qualifyValueKey(itemType: string, name: string): QualifiedValueKey {\n return itemType ? `${itemType}.${name}` : name;\n}\n\n/** The client that owns a surface: its name prefix. */\nexport function surfaceClientOf(surfaceName: string): string {\n return surfaceName.split(\"/\")[0] ?? \"\";\n}\n","/**\n * Sensitivity defaults and resolution (CONTRACT §2.1). Its own module so the\n * lightweight entries (`/actions`, the T0 React host) can project without\n * pulling the declaration registry.\n */\nimport type { DeclaredValue, Sensitivity } from \"./types\";\n\nexport const DEFAULT_SENSITIVITY: Sensitivity = Object.freeze({\n exportable: true,\n classification: \"ordinary\",\n includedByDefault: true,\n});\n\nexport function resolveSensitivity(value: Pick<DeclaredValue, \"sensitivity\">): Sensitivity {\n return {\n exportable: value.sensitivity?.exportable ?? DEFAULT_SENSITIVITY.exportable,\n classification: value.sensitivity?.classification ?? DEFAULT_SENSITIVITY.classification,\n includedByDefault: value.sensitivity?.includedByDefault ?? DEFAULT_SENSITIVITY.includedByDefault,\n };\n}\n","/**\n * THE ONE SYNC (CONTRACT §2.7 \"One sync path\", chair ruling B1).\n *\n * `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every\n * sync path (the SQL emitter and the admin sync service) executes this plan and\n * builds no rows of its own. Governance columns (`organization_id`,\n * `visibility`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.\n *\n * Schema generations: the columns added by the ALC-14 migration (item_type,\n * sensitivity, live slots, content_hash) are emitted only when the caller says\n * that migration is live (`schema`), so no emitter ever writes a column — or\n * upserts against a key — the database does not have yet.\n */\nimport { resolveSensitivity, type Resolved } from \"../declare/registry\";\nimport { surfaceClientOf } from \"../declare/keys\";\nimport type { DeclarationExtension, Json, SurfaceDeclaration } from \"../declare/types\";\n\nexport const GOVERNANCE_COLUMNS = [\"organization_id\", \"visibility\"] as const;\n\nexport interface SyncSchema {\n /** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */\n itemType?: boolean;\n /** Sensitivity, kind, live slot and role columns exist on `ui_surface_value`. */\n valueContract?: boolean;\n /** `ui_surface.content_hash` exists. */\n contentHash?: boolean;\n}\n\nexport interface SurfaceSyncPlanOptions<D extends SurfaceDeclaration> {\n organizationId: string;\n syncedFrom: string;\n syncedBy?: string | null;\n /** The address the host derives for a declaration without `urlPattern`. */\n resolveUrlPattern?: (declaration: Resolved<D>) => string | undefined;\n extensions?: readonly DeclarationExtension<D>[];\n schema?: SyncSchema;\n}\n\nexport interface SurfaceRowPlan {\n name: string;\n /** Written only when the row does not exist yet. */\n insert: Record<string, Json>;\n /** Written on every sync (code owns these columns). */\n update: Record<string, Json>;\n}\n\nexport interface SyncTablePlan {\n table: string;\n /** The ON CONFLICT target (a unique index or primary key on exactly these columns). */\n conflict: readonly string[];\n /** Columns set on insert and never on conflict. */\n insertOnly: readonly string[];\n rows: Record<string, Json>[];\n}\n\nexport interface SurfaceSyncPlan {\n surfaces: SurfaceRowPlan[];\n tables: SyncTablePlan[];\n /**\n * The key (ON CONFLICT target) of every mirror table this plan owns,\n * including tables it emits no rows for. Stale detection and deletes use\n * exactly these columns (`findStaleRows`).\n */\n keys: Record<string, readonly string[]>;\n}\n\nfunction childKey(schema: SyncSchema | undefined): readonly string[] {\n return schema?.itemType ? [\"surface_name\", \"item_type\", \"name\"] : [\"surface_name\", \"name\"];\n}\n\nexport function planSurfaceSync<D extends SurfaceDeclaration>(\n declarations: readonly Resolved<D>[],\n options: SurfaceSyncPlanOptions<D>,\n): SurfaceSyncPlan {\n const { organizationId, syncedFrom, schema } = options;\n const syncedBy = options.syncedBy ?? null;\n const provenance = { synced_by: syncedBy, synced_from: syncedFrom };\n const governance = { organization_id: organizationId, visibility: \"public\" };\n const surfaces: SurfaceRowPlan[] = [];\n const values: Record<string, Json>[] = [];\n const targets: Record<string, Json>[] = [];\n const itemTypes: Record<string, Json>[] = [];\n const extensionTables = new Map<string, SyncTablePlan>();\n\n for (const d of declarations) {\n const urlPattern = d.urlPattern?.trim() || options.resolveUrlPattern?.(d) || null;\n const intro = d.intro?.trim() || null;\n const parent = d.inheritsFrom ?? null;\n const client = surfaceClientOf(d.surfaceName);\n const owned: Record<string, Json> = {\n client_name: client,\n execution_mode: d.executionMode,\n description: d.description,\n label: d.label,\n value_groups: (d.groups ?? []) as unknown as Json,\n readiness: d.readiness,\n readiness_note: d.readinessNote?.trim() || null,\n ...(schema?.contentHash ? { content_hash: d.contentHash } : {}),\n };\n // A declaration that does not claim these never clears a DB-authored value.\n const claimed: Record<string, Json> = {\n ...(d.overlayId ? { overlay_id: d.overlayId } : {}),\n ...(urlPattern ? { url_pattern: urlPattern } : {}),\n ...(intro ? { intro } : {}),\n ...(parent ? { parent_surface_name: parent } : {}),\n };\n surfaces.push({\n name: d.surfaceName,\n insert: {\n name: d.surfaceName,\n ...owned,\n overlay_id: d.overlayId ?? null,\n url_pattern: urlPattern,\n intro,\n parent_surface_name: parent,\n },\n update: { ...owned, ...claimed },\n });\n\n for (const v of d.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: \"\" } : {}),\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.groupKey ?? v.group ?? \"general\",\n ...(schema?.valueContract\n ? {\n exportable: resolveSensitivity(v).exportable,\n classification: resolveSensitivity(v).classification,\n included_by_default: resolveSensitivity(v).includedByDefault,\n kind_key: v.valueKind ?? null,\n live_slot: v.liveSlot ?? null,\n role: v.role ?? \"data\",\n }\n : {}),\n ...provenance,\n });\n }\n\n for (const t of d.writeTargets ?? []) {\n targets.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: t.itemType ?? \"\" } : {}),\n name: t.name,\n label: t.label,\n description: t.description,\n value_type: t.valueType,\n mode: t.mode,\n updates_value: t.updatesValue ?? null,\n group_key: t.group ?? \"general\",\n sort_order: t.sortOrder ?? 1000,\n apply_policy: t.applyPolicy ?? \"manual\",\n kind_key: t.valueKind ?? null,\n ...(schema?.valueContract\n ? {\n patchable: t.patchable ?? false,\n approval_comparison: t.approvalComparison ?? null,\n stream_ops: t.streamOps ? [...t.streamOps] : null,\n destination: t.destination ?? false,\n }\n : {}),\n ...provenance,\n });\n }\n\n if (schema?.itemType) {\n for (const [position, item] of (d.itemTypes ?? []).entries()) {\n itemTypes.push({\n ...governance,\n surface_name: d.surfaceName,\n name: item.name,\n label: item.label,\n description: item.description,\n identity: [...item.identity],\n levels: item.levels ? [...item.levels] : null,\n entity_type: item.entityType ?? null,\n sort_order: position,\n ...provenance,\n });\n for (const v of item.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n item_type: item.name,\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.group ?? \"general\",\n ...provenance,\n });\n }\n }\n }\n\n for (const extension of options.extensions ?? []) {\n for (const entry of extension.toRows?.(d as unknown as D) ?? []) {\n const plan = extensionTables.get(entry.table) ?? {\n table: entry.table,\n conflict: entry.conflict,\n insertOnly: GOVERNANCE_COLUMNS,\n rows: [],\n };\n plan.rows.push({ ...governance, ...entry.row, ...provenance });\n extensionTables.set(entry.table, plan);\n }\n }\n }\n\n const keys: Record<string, readonly string[]> = {\n \"ui.ui_surface_value\": childKey(schema),\n \"ui.ui_surface_write_target\": childKey(schema),\n ...(schema?.itemType ? { \"ui.ui_surface_item_type\": [\"surface_name\", \"name\"] } : {}),\n };\n for (const extension of options.extensions ?? []) {\n if (extension.mirror) keys[extension.mirror.table] = extension.mirror.conflict;\n }\n for (const plan of extensionTables.values()) keys[plan.table] ??= plan.conflict;\n\n const tables: SyncTablePlan[] = [\n { table: \"ui.ui_surface_value\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: values },\n { table: \"ui.ui_surface_write_target\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: targets },\n ...(schema?.itemType\n ? [{ table: \"ui.ui_surface_item_type\", conflict: [\"surface_name\", \"name\"], insertOnly: GOVERNANCE_COLUMNS, rows: itemTypes }]\n : []),\n ...extensionTables.values(),\n ];\n return { surfaces, tables: tables.filter((table) => table.rows.length > 0), keys };\n}\n\n/** One row's key under `columns` (a missing column reads as ''). */\nexport function mirrorRowKey(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {\n return columns.map((column) => String(row[column] ?? \"\")).join(\"\\u0000\");\n}\n\n/**\n * Rows of `table` that belong to a surface this plan manages but that the\n * plan no longer declares, judged by the table's full key (`plan.keys`) — so\n * a screen value and an item value that share a name are distinct rows. The\n * caller deletes each returned row filtered by every column of that key.\n */\nexport function findStaleRows<R extends Readonly<Record<string, unknown>>>(\n plan: SurfaceSyncPlan,\n table: string,\n existing: readonly R[],\n): R[] {\n const key = plan.keys[table];\n if (!key) {\n throw new Error(\n `Sync plan declares no key for \"${table}\"; give the extension that owns it a \\`mirror\\` entry.`,\n );\n }\n const managed = new Set(plan.surfaces.map((surface) => surface.name));\n const declared = new Set(\n (plan.tables.find((entry) => entry.table === table)?.rows ?? []).map((row) => mirrorRowKey(row, key)),\n );\n return existing.filter(\n (row) => managed.has(String(row.surface_name)) && !declared.has(mirrorRowKey(row, key)),\n );\n}\n\n/** Every column a table's rows carry, in first-seen order. */\nexport function planColumns(table: SyncTablePlan): string[] {\n const columns: string[] = [];\n for (const row of table.rows) for (const key of Object.keys(row)) if (!columns.includes(key)) columns.push(key);\n return columns;\n}\n\n/** Columns a conflict rewrites: everything except the key and the insert-only set. */\nexport function conflictUpdateColumns(table: SyncTablePlan): string[] {\n return planColumns(table).filter((c) => !table.conflict.includes(c) && !table.insertOnly.includes(c));\n}\n\nexport function sqlLiteral(value: Json | undefined): string {\n if (value === null || value === undefined) return \"NULL\";\n if (typeof value === \"boolean\" || typeof value === \"number\") return String(value);\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (Array.isArray(value) && value.every((entry) => typeof entry === \"string\")) {\n return `ARRAY[${value.map((entry) => sqlLiteral(entry)).join(\", \")}]::text[]`;\n }\n return `'${JSON.stringify(value).replace(/'/g, \"''\")}'::jsonb`;\n}\n\n/**\n * The plan as SQL statements, one per entry: surface insert, one UPDATE per\n * surface, one upsert per table. Callers may run them, or EXPLAIN each one in a\n * read-only transaction to prove the plan against a live schema with no write.\n */\nexport function renderSurfaceSyncStatements(plan: SurfaceSyncPlan): string[] {\n const statements: string[] = [];\n if (plan.surfaces.length > 0) {\n const insertColumns = Object.keys(plan.surfaces[0]!.insert);\n statements.push(\n `INSERT INTO ui.ui_surface (${insertColumns.join(\", \")}) VALUES\\n${plan.surfaces\n .map((s) => `(${insertColumns.map((c) => sqlLiteral(s.insert[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (name) DO NOTHING`,\n );\n for (const s of plan.surfaces) {\n statements.push(\n `UPDATE ui.ui_surface SET ${Object.entries(s.update)\n .map(([c, v]) => `${c} = ${sqlLiteral(v)}`)\n .join(\", \")}, updated_at = now() WHERE name = ${sqlLiteral(s.name)}`,\n );\n }\n }\n for (const table of plan.tables) {\n const columns = planColumns(table);\n const sets = conflictUpdateColumns(table).map((c) => `${c} = EXCLUDED.${c}`);\n statements.push(\n `INSERT INTO ${table.table} (${columns.join(\", \")}) VALUES\\n${table.rows\n .map((row) => `(${columns.map((c) => sqlLiteral(row[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (${table.conflict.join(\", \")}) DO UPDATE SET ${[...sets, \"updated_at = now()\"].join(\", \")}`,\n );\n }\n return statements;\n}\n\n/** The plan as one SQL script (governance columns insert-only). */\nexport function renderSurfaceSyncSql(plan: SurfaceSyncPlan): string {\n return renderSurfaceSyncStatements(plan)\n .map((statement) => `${statement};`)\n .join(\"\\n\\n\");\n}\n","/**\n * The declaration half of the route check (CONTRACT §2.6 `/checks`): does each\n * declared `urlPattern` address a live route? The host supplies its route list\n * (it owns the filesystem); the matching rule lives here, once. Ported from\n * matrx-frontend `scripts/check-surface-routes.ts` (ALC-14).\n */\n\n/** Route or pattern → segments; `[x]` / `:x` → \"*\", `[...x]` / `[[...x]]` → \"**\". */\nexport function routeSegments(path: string): string[] {\n return (path.split(\"?\")[0] ?? \"\")\n .split(\"#\")[0]!\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => {\n if (/^\\[\\[?\\.\\.\\..+\\]\\]?$/.test(segment)) return \"**\";\n if (segment.startsWith(\"[\") || segment.startsWith(\":\")) return \"*\";\n return segment;\n });\n}\n\n/**\n * Does a declared `urlPattern` address a live route? A literal pattern segment\n * must be a literal route segment — a dynamic route segment does NOT satisfy it,\n * or `/marketing/[brandId]/…` would \"cover\" a retired `/marketing/brands/…`.\n * A trailing `*` makes the pattern a prefix.\n */\nexport function urlPatternMatchesRoute(pattern: string, route: readonly string[]): boolean {\n const isPrefix = pattern.trimEnd().endsWith(\"*\") && !pattern.endsWith(\"]*\");\n const segments = routeSegments(pattern.replace(/\\*+\\s*$/, \"\"));\n for (let i = 0; i < segments.length; i += 1) {\n const routeSegment = route[i];\n if (routeSegment === \"**\") return true;\n if (routeSegment === undefined) return false;\n const declared = segments[i];\n if (declared === \"**\") return true;\n if (declared === \"*\" ? routeSegment !== \"*\" : routeSegment !== declared) return false;\n }\n return isPrefix || route.length === segments.length;\n}\n\n/** Every declaration whose `urlPattern` no live route can produce. */\nexport function findDeadUrlPatterns(\n declarations: readonly { surfaceName: string; urlPattern?: string | undefined }[],\n liveRoutes: readonly string[],\n): { surfaceName: string; urlPattern: string }[] {\n const routes = liveRoutes.map(routeSegments);\n const dead: { surfaceName: string; urlPattern: string }[] = [];\n for (const declaration of declarations) {\n const pattern = declaration.urlPattern;\n if (!pattern) continue;\n if (!routes.some((route) => urlPatternMatchesRoute(pattern, route))) {\n dead.push({ surfaceName: declaration.surfaceName, urlPattern: pattern });\n }\n }\n return dead;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/checks.ts","../src/declare/keys.ts","../src/declare/sensitivity.ts","../src/checks/sync.ts","../src/checks/routes.ts"],"sourcesContent":["/**\n * `@ai-matrx/alchemy/checks` — the ONE surface sync (CONTRACT §2.7): the row plan\n * every sync path executes, and its SQL rendering. Pure; no database client.\n */\nexport {\n GOVERNANCE_COLUMNS,\n conflictUpdateColumns,\n findStaleRows,\n mirrorRowKey,\n planColumns,\n planSurfaceSync,\n renderSurfaceSyncSql,\n renderSurfaceSyncStatements,\n sqlLiteral,\n type SurfaceRowPlan,\n type SurfaceSyncPlan,\n type SurfaceSyncPlanOptions,\n type SyncSchema,\n type SyncTablePlan,\n} from \"./checks/sync\";\nexport { findDeadUrlPatterns, routeSegments, urlPatternMatchesRoute } from \"./checks/routes\";\n","import type { QualifiedValueKey } from \"./types\";\n\n/** Every value, write-target, item-type and group name (the DB CHECK rule). */\nexport const DECLARATION_NAME_RE = /^[a-z][a-z0-9_]*$/;\n\n/** \"<client>/<local>\" — the surface naming convention. */\nexport const SURFACE_NAME_RE = /^[a-z][a-z0-9-]*\\/[a-z0-9-/]+$/;\n\n/** A bare screen value (`status`) or an item value (`table_row.status`). */\nexport const QUALIFIED_VALUE_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)?$/;\n\nexport interface ParsedValueKey {\n /** \"\" for a screen-level value. */\n itemType: string;\n name: string;\n}\n\n/** Split a qualified key; null when it is not a valid bare or qualified key. */\nexport function parseQualifiedValueKey(key: string): ParsedValueKey | null {\n if (!QUALIFIED_VALUE_KEY_RE.test(key)) return null;\n const dot = key.indexOf(\".\");\n if (dot === -1) return { itemType: \"\", name: key };\n return { itemType: key.slice(0, dot), name: key.slice(dot + 1) };\n}\n\nexport function qualifyValueKey(itemType: string, name: string): QualifiedValueKey {\n return itemType ? `${itemType}.${name}` : name;\n}\n\n/** The client that owns a surface: its name prefix. */\nexport function surfaceClientOf(surfaceName: string): string {\n return surfaceName.split(\"/\")[0] ?? \"\";\n}\n","/**\n * Sensitivity defaults and resolution (CONTRACT §2.1). Its own module so the\n * lightweight entries (`/actions`, the T0 React host) can project without\n * pulling the declaration registry.\n */\nimport type { DeclaredValue, Sensitivity } from \"./types\";\n\nexport const DEFAULT_SENSITIVITY: Sensitivity = Object.freeze({\n exportable: true,\n classification: \"ordinary\",\n includedByDefault: true,\n});\n\nexport function resolveSensitivity(value: Pick<DeclaredValue, \"sensitivity\">): Sensitivity {\n return {\n exportable: value.sensitivity?.exportable ?? DEFAULT_SENSITIVITY.exportable,\n classification: value.sensitivity?.classification ?? DEFAULT_SENSITIVITY.classification,\n includedByDefault: value.sensitivity?.includedByDefault ?? DEFAULT_SENSITIVITY.includedByDefault,\n };\n}\n","/**\n * THE ONE SYNC (CONTRACT §2.7 \"One sync path\", chair ruling B1).\n *\n * `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every\n * sync path (the SQL emitter and the admin sync service) executes this plan and\n * builds no rows of its own. Governance columns (`organization_id`,\n * `published_to_web`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.\n * Mirror rows are published to the web on insert, written explicitly (access ladder\n * T-13: never left to a column default); the database stamps when and by whom.\n *\n * Schema generations: the columns added by the ALC-14 migration (item_type,\n * sensitivity, live slots, content_hash) are emitted only when the caller says\n * that migration is live (`schema`), so no emitter ever writes a column — or\n * upserts against a key — the database does not have yet.\n */\nimport { resolveSensitivity, type Resolved } from \"../declare/registry\";\nimport { surfaceClientOf } from \"../declare/keys\";\nimport type { DeclarationExtension, Json, SurfaceDeclaration } from \"../declare/types\";\n\nexport const GOVERNANCE_COLUMNS = [\"organization_id\", \"published_to_web\"] as const;\n\nexport interface SyncSchema {\n /** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */\n itemType?: boolean;\n /** Sensitivity, kind, live slot and role columns exist on `ui_surface_value`. */\n valueContract?: boolean;\n /** `ui_surface.content_hash` exists. */\n contentHash?: boolean;\n}\n\nexport interface SurfaceSyncPlanOptions<D extends SurfaceDeclaration> {\n organizationId: string;\n syncedFrom: string;\n syncedBy?: string | null;\n /** The address the host derives for a declaration without `urlPattern`. */\n resolveUrlPattern?: (declaration: Resolved<D>) => string | undefined;\n extensions?: readonly DeclarationExtension<D>[];\n schema?: SyncSchema;\n}\n\nexport interface SurfaceRowPlan {\n name: string;\n /** Written only when the row does not exist yet. */\n insert: Record<string, Json>;\n /** Written on every sync (code owns these columns). */\n update: Record<string, Json>;\n}\n\nexport interface SyncTablePlan {\n table: string;\n /** The ON CONFLICT target (a unique index or primary key on exactly these columns). */\n conflict: readonly string[];\n /** Columns set on insert and never on conflict. */\n insertOnly: readonly string[];\n rows: Record<string, Json>[];\n}\n\nexport interface SurfaceSyncPlan {\n surfaces: SurfaceRowPlan[];\n tables: SyncTablePlan[];\n /**\n * The key (ON CONFLICT target) of every mirror table this plan owns,\n * including tables it emits no rows for. Stale detection and deletes use\n * exactly these columns (`findStaleRows`).\n */\n keys: Record<string, readonly string[]>;\n}\n\nfunction childKey(schema: SyncSchema | undefined): readonly string[] {\n return schema?.itemType ? [\"surface_name\", \"item_type\", \"name\"] : [\"surface_name\", \"name\"];\n}\n\nexport function planSurfaceSync<D extends SurfaceDeclaration>(\n declarations: readonly Resolved<D>[],\n options: SurfaceSyncPlanOptions<D>,\n): SurfaceSyncPlan {\n const { organizationId, syncedFrom, schema } = options;\n const syncedBy = options.syncedBy ?? null;\n const provenance = { synced_by: syncedBy, synced_from: syncedFrom };\n const governance = { organization_id: organizationId, published_to_web: true };\n const surfaces: SurfaceRowPlan[] = [];\n const values: Record<string, Json>[] = [];\n const targets: Record<string, Json>[] = [];\n const itemTypes: Record<string, Json>[] = [];\n const extensionTables = new Map<string, SyncTablePlan>();\n\n for (const d of declarations) {\n const urlPattern = d.urlPattern?.trim() || options.resolveUrlPattern?.(d) || null;\n const intro = d.intro?.trim() || null;\n const parent = d.inheritsFrom ?? null;\n const client = surfaceClientOf(d.surfaceName);\n const owned: Record<string, Json> = {\n client_name: client,\n execution_mode: d.executionMode,\n description: d.description,\n label: d.label,\n value_groups: (d.groups ?? []) as unknown as Json,\n readiness: d.readiness,\n readiness_note: d.readinessNote?.trim() || null,\n ...(schema?.contentHash ? { content_hash: d.contentHash } : {}),\n };\n // A declaration that does not claim these never clears a DB-authored value.\n const claimed: Record<string, Json> = {\n ...(d.overlayId ? { overlay_id: d.overlayId } : {}),\n ...(urlPattern ? { url_pattern: urlPattern } : {}),\n ...(intro ? { intro } : {}),\n ...(parent ? { parent_surface_name: parent } : {}),\n };\n surfaces.push({\n name: d.surfaceName,\n insert: {\n name: d.surfaceName,\n ...owned,\n overlay_id: d.overlayId ?? null,\n url_pattern: urlPattern,\n intro,\n parent_surface_name: parent,\n },\n update: { ...owned, ...claimed },\n });\n\n for (const v of d.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: \"\" } : {}),\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.groupKey ?? v.group ?? \"general\",\n ...(schema?.valueContract\n ? {\n exportable: resolveSensitivity(v).exportable,\n classification: resolveSensitivity(v).classification,\n included_by_default: resolveSensitivity(v).includedByDefault,\n kind_key: v.valueKind ?? null,\n live_slot: v.liveSlot ?? null,\n role: v.role ?? \"data\",\n }\n : {}),\n ...provenance,\n });\n }\n\n for (const t of d.writeTargets ?? []) {\n targets.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: t.itemType ?? \"\" } : {}),\n name: t.name,\n label: t.label,\n description: t.description,\n value_type: t.valueType,\n mode: t.mode,\n updates_value: t.updatesValue ?? null,\n group_key: t.group ?? \"general\",\n sort_order: t.sortOrder ?? 1000,\n apply_policy: t.applyPolicy ?? \"manual\",\n kind_key: t.valueKind ?? null,\n ...(schema?.valueContract\n ? {\n patchable: t.patchable ?? false,\n approval_comparison: t.approvalComparison ?? null,\n stream_ops: t.streamOps ? [...t.streamOps] : null,\n destination: t.destination ?? false,\n }\n : {}),\n ...provenance,\n });\n }\n\n if (schema?.itemType) {\n for (const [position, item] of (d.itemTypes ?? []).entries()) {\n itemTypes.push({\n ...governance,\n surface_name: d.surfaceName,\n name: item.name,\n label: item.label,\n description: item.description,\n identity: [...item.identity],\n levels: item.levels ? [...item.levels] : null,\n entity_type: item.entityType ?? null,\n sort_order: position,\n ...provenance,\n });\n for (const v of item.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n item_type: item.name,\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.group ?? \"general\",\n ...provenance,\n });\n }\n }\n }\n\n for (const extension of options.extensions ?? []) {\n for (const entry of extension.toRows?.(d as unknown as D) ?? []) {\n const plan = extensionTables.get(entry.table) ?? {\n table: entry.table,\n conflict: entry.conflict,\n insertOnly: GOVERNANCE_COLUMNS,\n rows: [],\n };\n plan.rows.push({ ...governance, ...entry.row, ...provenance });\n extensionTables.set(entry.table, plan);\n }\n }\n }\n\n const keys: Record<string, readonly string[]> = {\n \"ui.ui_surface_value\": childKey(schema),\n \"ui.ui_surface_write_target\": childKey(schema),\n ...(schema?.itemType ? { \"ui.ui_surface_item_type\": [\"surface_name\", \"name\"] } : {}),\n };\n for (const extension of options.extensions ?? []) {\n if (extension.mirror) keys[extension.mirror.table] = extension.mirror.conflict;\n }\n for (const plan of extensionTables.values()) keys[plan.table] ??= plan.conflict;\n\n const tables: SyncTablePlan[] = [\n { table: \"ui.ui_surface_value\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: values },\n { table: \"ui.ui_surface_write_target\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: targets },\n ...(schema?.itemType\n ? [{ table: \"ui.ui_surface_item_type\", conflict: [\"surface_name\", \"name\"], insertOnly: GOVERNANCE_COLUMNS, rows: itemTypes }]\n : []),\n ...extensionTables.values(),\n ];\n return { surfaces, tables: tables.filter((table) => table.rows.length > 0), keys };\n}\n\n/** One row's key under `columns` (a missing column reads as ''). */\nexport function mirrorRowKey(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {\n return columns.map((column) => String(row[column] ?? \"\")).join(\"\\u0000\");\n}\n\n/**\n * Rows of `table` that belong to a surface this plan manages but that the\n * plan no longer declares, judged by the table's full key (`plan.keys`) — so\n * a screen value and an item value that share a name are distinct rows. The\n * caller deletes each returned row filtered by every column of that key.\n */\nexport function findStaleRows<R extends Readonly<Record<string, unknown>>>(\n plan: SurfaceSyncPlan,\n table: string,\n existing: readonly R[],\n): R[] {\n const key = plan.keys[table];\n if (!key) {\n throw new Error(\n `Sync plan declares no key for \"${table}\"; give the extension that owns it a \\`mirror\\` entry.`,\n );\n }\n const managed = new Set(plan.surfaces.map((surface) => surface.name));\n const declared = new Set(\n (plan.tables.find((entry) => entry.table === table)?.rows ?? []).map((row) => mirrorRowKey(row, key)),\n );\n return existing.filter(\n (row) => managed.has(String(row.surface_name)) && !declared.has(mirrorRowKey(row, key)),\n );\n}\n\n/** Every column a table's rows carry, in first-seen order. */\nexport function planColumns(table: SyncTablePlan): string[] {\n const columns: string[] = [];\n for (const row of table.rows) for (const key of Object.keys(row)) if (!columns.includes(key)) columns.push(key);\n return columns;\n}\n\n/** Columns a conflict rewrites: everything except the key and the insert-only set. */\nexport function conflictUpdateColumns(table: SyncTablePlan): string[] {\n return planColumns(table).filter((c) => !table.conflict.includes(c) && !table.insertOnly.includes(c));\n}\n\nexport function sqlLiteral(value: Json | undefined): string {\n if (value === null || value === undefined) return \"NULL\";\n if (typeof value === \"boolean\" || typeof value === \"number\") return String(value);\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (Array.isArray(value) && value.every((entry) => typeof entry === \"string\")) {\n return `ARRAY[${value.map((entry) => sqlLiteral(entry)).join(\", \")}]::text[]`;\n }\n return `'${JSON.stringify(value).replace(/'/g, \"''\")}'::jsonb`;\n}\n\n/**\n * The plan as SQL statements, one per entry: surface insert, one UPDATE per\n * surface, one upsert per table. Callers may run them, or EXPLAIN each one in a\n * read-only transaction to prove the plan against a live schema with no write.\n */\nexport function renderSurfaceSyncStatements(plan: SurfaceSyncPlan): string[] {\n const statements: string[] = [];\n if (plan.surfaces.length > 0) {\n const insertColumns = Object.keys(plan.surfaces[0]!.insert);\n statements.push(\n `INSERT INTO ui.ui_surface (${insertColumns.join(\", \")}) VALUES\\n${plan.surfaces\n .map((s) => `(${insertColumns.map((c) => sqlLiteral(s.insert[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (name) DO NOTHING`,\n );\n for (const s of plan.surfaces) {\n statements.push(\n `UPDATE ui.ui_surface SET ${Object.entries(s.update)\n .map(([c, v]) => `${c} = ${sqlLiteral(v)}`)\n .join(\", \")}, updated_at = now() WHERE name = ${sqlLiteral(s.name)}`,\n );\n }\n }\n for (const table of plan.tables) {\n const columns = planColumns(table);\n const sets = conflictUpdateColumns(table).map((c) => `${c} = EXCLUDED.${c}`);\n statements.push(\n `INSERT INTO ${table.table} (${columns.join(\", \")}) VALUES\\n${table.rows\n .map((row) => `(${columns.map((c) => sqlLiteral(row[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (${table.conflict.join(\", \")}) DO UPDATE SET ${[...sets, \"updated_at = now()\"].join(\", \")}`,\n );\n }\n return statements;\n}\n\n/** The plan as one SQL script (governance columns insert-only). */\nexport function renderSurfaceSyncSql(plan: SurfaceSyncPlan): string {\n return renderSurfaceSyncStatements(plan)\n .map((statement) => `${statement};`)\n .join(\"\\n\\n\");\n}\n","/**\n * The declaration half of the route check (CONTRACT §2.6 `/checks`): does each\n * declared `urlPattern` address a live route? The host supplies its route list\n * (it owns the filesystem); the matching rule lives here, once. Ported from\n * matrx-frontend `scripts/check-surface-routes.ts` (ALC-14).\n */\n\n/** Route or pattern → segments; `[x]` / `:x` → \"*\", `[...x]` / `[[...x]]` → \"**\". */\nexport function routeSegments(path: string): string[] {\n return (path.split(\"?\")[0] ?? \"\")\n .split(\"#\")[0]!\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => {\n if (/^\\[\\[?\\.\\.\\..+\\]\\]?$/.test(segment)) return \"**\";\n if (segment.startsWith(\"[\") || segment.startsWith(\":\")) return \"*\";\n return segment;\n });\n}\n\n/**\n * Does a declared `urlPattern` address a live route? A literal pattern segment\n * must be a literal route segment — a dynamic route segment does NOT satisfy it,\n * or `/marketing/[brandId]/…` would \"cover\" a retired `/marketing/brands/…`.\n * A trailing `*` makes the pattern a prefix.\n */\nexport function urlPatternMatchesRoute(pattern: string, route: readonly string[]): boolean {\n const isPrefix = pattern.trimEnd().endsWith(\"*\") && !pattern.endsWith(\"]*\");\n const segments = routeSegments(pattern.replace(/\\*+\\s*$/, \"\"));\n for (let i = 0; i < segments.length; i += 1) {\n const routeSegment = route[i];\n if (routeSegment === \"**\") return true;\n if (routeSegment === undefined) return false;\n const declared = segments[i];\n if (declared === \"**\") return true;\n if (declared === \"*\" ? routeSegment !== \"*\" : routeSegment !== declared) return false;\n }\n return isPrefix || route.length === segments.length;\n}\n\n/** Every declaration whose `urlPattern` no live route can produce. */\nexport function findDeadUrlPatterns(\n declarations: readonly { surfaceName: string; urlPattern?: string | undefined }[],\n liveRoutes: readonly string[],\n): { surfaceName: string; urlPattern: string }[] {\n const routes = liveRoutes.map(routeSegments);\n const dead: { surfaceName: string; urlPattern: string }[] = [];\n for (const declaration of declarations) {\n const pattern = declaration.urlPattern;\n if (!pattern) continue;\n if (!routes.some((route) => urlPatternMatchesRoute(pattern, route))) {\n dead.push({ surfaceName: declaration.surfaceName, urlPattern: pattern });\n }\n }\n return dead;\n}\n"],"mappings":"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","names":[]}
|
package/dist/checks.d.cts
CHANGED
|
@@ -204,7 +204,9 @@ type Resolved<D extends SurfaceDeclaration> = Omit<D, "values" | "groups"> & {
|
|
|
204
204
|
* `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every
|
|
205
205
|
* sync path (the SQL emitter and the admin sync service) executes this plan and
|
|
206
206
|
* builds no rows of its own. Governance columns (`organization_id`,
|
|
207
|
-
* `
|
|
207
|
+
* `published_to_web`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.
|
|
208
|
+
* Mirror rows are published to the web on insert, written explicitly (access ladder
|
|
209
|
+
* T-13: never left to a column default); the database stamps when and by whom.
|
|
208
210
|
*
|
|
209
211
|
* Schema generations: the columns added by the ALC-14 migration (item_type,
|
|
210
212
|
* sensitivity, live slots, content_hash) are emitted only when the caller says
|
|
@@ -212,7 +214,7 @@ type Resolved<D extends SurfaceDeclaration> = Omit<D, "values" | "groups"> & {
|
|
|
212
214
|
* upserts against a key — the database does not have yet.
|
|
213
215
|
*/
|
|
214
216
|
|
|
215
|
-
declare const GOVERNANCE_COLUMNS: readonly ["organization_id", "
|
|
217
|
+
declare const GOVERNANCE_COLUMNS: readonly ["organization_id", "published_to_web"];
|
|
216
218
|
interface SyncSchema {
|
|
217
219
|
/** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */
|
|
218
220
|
itemType?: boolean;
|
package/dist/checks.d.ts
CHANGED
|
@@ -204,7 +204,9 @@ type Resolved<D extends SurfaceDeclaration> = Omit<D, "values" | "groups"> & {
|
|
|
204
204
|
* `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every
|
|
205
205
|
* sync path (the SQL emitter and the admin sync service) executes this plan and
|
|
206
206
|
* builds no rows of its own. Governance columns (`organization_id`,
|
|
207
|
-
* `
|
|
207
|
+
* `published_to_web`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.
|
|
208
|
+
* Mirror rows are published to the web on insert, written explicitly (access ladder
|
|
209
|
+
* T-13: never left to a column default); the database stamps when and by whom.
|
|
208
210
|
*
|
|
209
211
|
* Schema generations: the columns added by the ALC-14 migration (item_type,
|
|
210
212
|
* sensitivity, live slots, content_hash) are emitted only when the caller says
|
|
@@ -212,7 +214,7 @@ type Resolved<D extends SurfaceDeclaration> = Omit<D, "values" | "groups"> & {
|
|
|
212
214
|
* upserts against a key — the database does not have yet.
|
|
213
215
|
*/
|
|
214
216
|
|
|
215
|
-
declare const GOVERNANCE_COLUMNS: readonly ["organization_id", "
|
|
217
|
+
declare const GOVERNANCE_COLUMNS: readonly ["organization_id", "published_to_web"];
|
|
216
218
|
interface SyncSchema {
|
|
217
219
|
/** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */
|
|
218
220
|
itemType?: boolean;
|
package/dist/checks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
function surfaceClientOf(surfaceName){return surfaceName.split("/")[0]??""}var DEFAULT_SENSITIVITY=Object.freeze({exportable:!0,classification:"ordinary",includedByDefault:!0});function resolveSensitivity(value){return{exportable:value.sensitivity?.exportable??DEFAULT_SENSITIVITY.exportable,classification:value.sensitivity?.classification??DEFAULT_SENSITIVITY.classification,includedByDefault:value.sensitivity?.includedByDefault??DEFAULT_SENSITIVITY.includedByDefault}}var GOVERNANCE_COLUMNS=["organization_id","
|
|
1
|
+
function surfaceClientOf(surfaceName){return surfaceName.split("/")[0]??""}var DEFAULT_SENSITIVITY=Object.freeze({exportable:!0,classification:"ordinary",includedByDefault:!0});function resolveSensitivity(value){return{exportable:value.sensitivity?.exportable??DEFAULT_SENSITIVITY.exportable,classification:value.sensitivity?.classification??DEFAULT_SENSITIVITY.classification,includedByDefault:value.sensitivity?.includedByDefault??DEFAULT_SENSITIVITY.includedByDefault}}var GOVERNANCE_COLUMNS=["organization_id","published_to_web"];function childKey(schema){return schema?.itemType?["surface_name","item_type","name"]:["surface_name","name"]}function planSurfaceSync(declarations,options){let{organizationId,syncedFrom,schema}=options,provenance={synced_by:options.syncedBy??null,synced_from:syncedFrom},governance={organization_id:organizationId,published_to_web:!0},surfaces=[],values=[],targets=[],itemTypes=[],extensionTables=new Map;for(let d of declarations){let urlPattern=d.urlPattern?.trim()||options.resolveUrlPattern?.(d)||null,intro=d.intro?.trim()||null,parent=d.inheritsFrom??null,owned={client_name:surfaceClientOf(d.surfaceName),execution_mode:d.executionMode,description:d.description,label:d.label,value_groups:d.groups??[],readiness:d.readiness,readiness_note:d.readinessNote?.trim()||null,...schema?.contentHash?{content_hash:d.contentHash}:{}},claimed={...d.overlayId?{overlay_id:d.overlayId}:{},...urlPattern?{url_pattern:urlPattern}:{},...intro?{intro}:{},...parent?{parent_surface_name:parent}:{}};surfaces.push({name:d.surfaceName,insert:{name:d.surfaceName,...owned,overlay_id:d.overlayId??null,url_pattern:urlPattern,intro,parent_surface_name:parent},update:{...owned,...claimed}});for(let v of d.values)values.push({...governance,surface_name:d.surfaceName,...schema?.itemType?{item_type:""}:{},name:v.name,label:v.label,description:v.description,value_type:v.valueType,always_available:v.alwaysAvailable,typical_char_count:v.typicalCharCount,sort_order:v.sortOrder??1e3,auto_context:v.autoContext??!0,group_key:v.groupKey??v.group??"general",...schema?.valueContract?{exportable:resolveSensitivity(v).exportable,classification:resolveSensitivity(v).classification,included_by_default:resolveSensitivity(v).includedByDefault,kind_key:v.valueKind??null,live_slot:v.liveSlot??null,role:v.role??"data"}:{},...provenance});for(let t of d.writeTargets??[])targets.push({...governance,surface_name:d.surfaceName,...schema?.itemType?{item_type:t.itemType??""}:{},name:t.name,label:t.label,description:t.description,value_type:t.valueType,mode:t.mode,updates_value:t.updatesValue??null,group_key:t.group??"general",sort_order:t.sortOrder??1e3,apply_policy:t.applyPolicy??"manual",kind_key:t.valueKind??null,...schema?.valueContract?{patchable:t.patchable??!1,approval_comparison:t.approvalComparison??null,stream_ops:t.streamOps?[...t.streamOps]:null,destination:t.destination??!1}:{},...provenance});if(schema?.itemType)for(let[position,item]of(d.itemTypes??[]).entries()){itemTypes.push({...governance,surface_name:d.surfaceName,name:item.name,label:item.label,description:item.description,identity:[...item.identity],levels:item.levels?[...item.levels]:null,entity_type:item.entityType??null,sort_order:position,...provenance});for(let v of item.values)values.push({...governance,surface_name:d.surfaceName,item_type:item.name,name:v.name,label:v.label,description:v.description,value_type:v.valueType,always_available:v.alwaysAvailable,typical_char_count:v.typicalCharCount,sort_order:v.sortOrder??1e3,auto_context:v.autoContext??!0,group_key:v.group??"general",...provenance})}for(let extension of options.extensions??[])for(let entry of extension.toRows?.(d)??[]){let plan=extensionTables.get(entry.table)??{table:entry.table,conflict:entry.conflict,insertOnly:GOVERNANCE_COLUMNS,rows:[]};plan.rows.push({...governance,...entry.row,...provenance}),extensionTables.set(entry.table,plan)}}let keys={"ui.ui_surface_value":childKey(schema),"ui.ui_surface_write_target":childKey(schema),...schema?.itemType?{"ui.ui_surface_item_type":["surface_name","name"]}:{}};for(let extension of options.extensions??[])extension.mirror&&(keys[extension.mirror.table]=extension.mirror.conflict);for(let plan of extensionTables.values())keys[plan.table]??=plan.conflict;let tables=[{table:"ui.ui_surface_value",conflict:childKey(schema),insertOnly:GOVERNANCE_COLUMNS,rows:values},{table:"ui.ui_surface_write_target",conflict:childKey(schema),insertOnly:GOVERNANCE_COLUMNS,rows:targets},...schema?.itemType?[{table:"ui.ui_surface_item_type",conflict:["surface_name","name"],insertOnly:GOVERNANCE_COLUMNS,rows:itemTypes}]:[],...extensionTables.values()];return{surfaces,tables:tables.filter(table=>table.rows.length>0),keys}}function mirrorRowKey(row,columns){return columns.map(column=>String(row[column]??"")).join("\0")}function findStaleRows(plan,table,existing){let key=plan.keys[table];if(!key)throw new Error(`Sync plan declares no key for "${table}"; give the extension that owns it a \`mirror\` entry.`);let managed=new Set(plan.surfaces.map(surface=>surface.name)),declared=new Set((plan.tables.find(entry=>entry.table===table)?.rows??[]).map(row=>mirrorRowKey(row,key)));return existing.filter(row=>managed.has(String(row.surface_name))&&!declared.has(mirrorRowKey(row,key)))}function planColumns(table){let columns=[];for(let row of table.rows)for(let key of Object.keys(row))columns.includes(key)||columns.push(key);return columns}function conflictUpdateColumns(table){return planColumns(table).filter(c=>!table.conflict.includes(c)&&!table.insertOnly.includes(c))}function sqlLiteral(value){return value==null?"NULL":typeof value=="boolean"||typeof value=="number"?String(value):typeof value=="string"?`'${value.replace(/'/g,"''")}'`:Array.isArray(value)&&value.every(entry=>typeof entry=="string")?`ARRAY[${value.map(entry=>sqlLiteral(entry)).join(", ")}]::text[]`:`'${JSON.stringify(value).replace(/'/g,"''")}'::jsonb`}function renderSurfaceSyncStatements(plan){let statements=[];if(plan.surfaces.length>0){let insertColumns=Object.keys(plan.surfaces[0].insert);statements.push(`INSERT INTO ui.ui_surface (${insertColumns.join(", ")}) VALUES
|
|
2
2
|
${plan.surfaces.map(s=>`(${insertColumns.map(c=>sqlLiteral(s.insert[c])).join(", ")})`).join(`,
|
|
3
3
|
`)}
|
|
4
4
|
ON CONFLICT (name) DO NOTHING`);for(let s of plan.surfaces)statements.push(`UPDATE ui.ui_surface SET ${Object.entries(s.update).map(([c,v])=>`${c} = ${sqlLiteral(v)}`).join(", ")}, updated_at = now() WHERE name = ${sqlLiteral(s.name)}`)}for(let table of plan.tables){let columns=planColumns(table),sets=conflictUpdateColumns(table).map(c=>`${c} = EXCLUDED.${c}`);statements.push(`INSERT INTO ${table.table} (${columns.join(", ")}) VALUES
|
package/dist/checks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/declare/keys.ts","../src/declare/sensitivity.ts","../src/checks/sync.ts","../src/checks/routes.ts"],"sourcesContent":["import type { QualifiedValueKey } from \"./types\";\n\n/** Every value, write-target, item-type and group name (the DB CHECK rule). */\nexport const DECLARATION_NAME_RE = /^[a-z][a-z0-9_]*$/;\n\n/** \"<client>/<local>\" — the surface naming convention. */\nexport const SURFACE_NAME_RE = /^[a-z][a-z0-9-]*\\/[a-z0-9-/]+$/;\n\n/** A bare screen value (`status`) or an item value (`table_row.status`). */\nexport const QUALIFIED_VALUE_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)?$/;\n\nexport interface ParsedValueKey {\n /** \"\" for a screen-level value. */\n itemType: string;\n name: string;\n}\n\n/** Split a qualified key; null when it is not a valid bare or qualified key. */\nexport function parseQualifiedValueKey(key: string): ParsedValueKey | null {\n if (!QUALIFIED_VALUE_KEY_RE.test(key)) return null;\n const dot = key.indexOf(\".\");\n if (dot === -1) return { itemType: \"\", name: key };\n return { itemType: key.slice(0, dot), name: key.slice(dot + 1) };\n}\n\nexport function qualifyValueKey(itemType: string, name: string): QualifiedValueKey {\n return itemType ? `${itemType}.${name}` : name;\n}\n\n/** The client that owns a surface: its name prefix. */\nexport function surfaceClientOf(surfaceName: string): string {\n return surfaceName.split(\"/\")[0] ?? \"\";\n}\n","/**\n * Sensitivity defaults and resolution (CONTRACT §2.1). Its own module so the\n * lightweight entries (`/actions`, the T0 React host) can project without\n * pulling the declaration registry.\n */\nimport type { DeclaredValue, Sensitivity } from \"./types\";\n\nexport const DEFAULT_SENSITIVITY: Sensitivity = Object.freeze({\n exportable: true,\n classification: \"ordinary\",\n includedByDefault: true,\n});\n\nexport function resolveSensitivity(value: Pick<DeclaredValue, \"sensitivity\">): Sensitivity {\n return {\n exportable: value.sensitivity?.exportable ?? DEFAULT_SENSITIVITY.exportable,\n classification: value.sensitivity?.classification ?? DEFAULT_SENSITIVITY.classification,\n includedByDefault: value.sensitivity?.includedByDefault ?? DEFAULT_SENSITIVITY.includedByDefault,\n };\n}\n","/**\n * THE ONE SYNC (CONTRACT §2.7 \"One sync path\", chair ruling B1).\n *\n * `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every\n * sync path (the SQL emitter and the admin sync service) executes this plan and\n * builds no rows of its own. Governance columns (`organization_id`,\n * `visibility`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.\n *\n * Schema generations: the columns added by the ALC-14 migration (item_type,\n * sensitivity, live slots, content_hash) are emitted only when the caller says\n * that migration is live (`schema`), so no emitter ever writes a column — or\n * upserts against a key — the database does not have yet.\n */\nimport { resolveSensitivity, type Resolved } from \"../declare/registry\";\nimport { surfaceClientOf } from \"../declare/keys\";\nimport type { DeclarationExtension, Json, SurfaceDeclaration } from \"../declare/types\";\n\nexport const GOVERNANCE_COLUMNS = [\"organization_id\", \"visibility\"] as const;\n\nexport interface SyncSchema {\n /** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */\n itemType?: boolean;\n /** Sensitivity, kind, live slot and role columns exist on `ui_surface_value`. */\n valueContract?: boolean;\n /** `ui_surface.content_hash` exists. */\n contentHash?: boolean;\n}\n\nexport interface SurfaceSyncPlanOptions<D extends SurfaceDeclaration> {\n organizationId: string;\n syncedFrom: string;\n syncedBy?: string | null;\n /** The address the host derives for a declaration without `urlPattern`. */\n resolveUrlPattern?: (declaration: Resolved<D>) => string | undefined;\n extensions?: readonly DeclarationExtension<D>[];\n schema?: SyncSchema;\n}\n\nexport interface SurfaceRowPlan {\n name: string;\n /** Written only when the row does not exist yet. */\n insert: Record<string, Json>;\n /** Written on every sync (code owns these columns). */\n update: Record<string, Json>;\n}\n\nexport interface SyncTablePlan {\n table: string;\n /** The ON CONFLICT target (a unique index or primary key on exactly these columns). */\n conflict: readonly string[];\n /** Columns set on insert and never on conflict. */\n insertOnly: readonly string[];\n rows: Record<string, Json>[];\n}\n\nexport interface SurfaceSyncPlan {\n surfaces: SurfaceRowPlan[];\n tables: SyncTablePlan[];\n /**\n * The key (ON CONFLICT target) of every mirror table this plan owns,\n * including tables it emits no rows for. Stale detection and deletes use\n * exactly these columns (`findStaleRows`).\n */\n keys: Record<string, readonly string[]>;\n}\n\nfunction childKey(schema: SyncSchema | undefined): readonly string[] {\n return schema?.itemType ? [\"surface_name\", \"item_type\", \"name\"] : [\"surface_name\", \"name\"];\n}\n\nexport function planSurfaceSync<D extends SurfaceDeclaration>(\n declarations: readonly Resolved<D>[],\n options: SurfaceSyncPlanOptions<D>,\n): SurfaceSyncPlan {\n const { organizationId, syncedFrom, schema } = options;\n const syncedBy = options.syncedBy ?? null;\n const provenance = { synced_by: syncedBy, synced_from: syncedFrom };\n const governance = { organization_id: organizationId, visibility: \"public\" };\n const surfaces: SurfaceRowPlan[] = [];\n const values: Record<string, Json>[] = [];\n const targets: Record<string, Json>[] = [];\n const itemTypes: Record<string, Json>[] = [];\n const extensionTables = new Map<string, SyncTablePlan>();\n\n for (const d of declarations) {\n const urlPattern = d.urlPattern?.trim() || options.resolveUrlPattern?.(d) || null;\n const intro = d.intro?.trim() || null;\n const parent = d.inheritsFrom ?? null;\n const client = surfaceClientOf(d.surfaceName);\n const owned: Record<string, Json> = {\n client_name: client,\n execution_mode: d.executionMode,\n description: d.description,\n label: d.label,\n value_groups: (d.groups ?? []) as unknown as Json,\n readiness: d.readiness,\n readiness_note: d.readinessNote?.trim() || null,\n ...(schema?.contentHash ? { content_hash: d.contentHash } : {}),\n };\n // A declaration that does not claim these never clears a DB-authored value.\n const claimed: Record<string, Json> = {\n ...(d.overlayId ? { overlay_id: d.overlayId } : {}),\n ...(urlPattern ? { url_pattern: urlPattern } : {}),\n ...(intro ? { intro } : {}),\n ...(parent ? { parent_surface_name: parent } : {}),\n };\n surfaces.push({\n name: d.surfaceName,\n insert: {\n name: d.surfaceName,\n ...owned,\n overlay_id: d.overlayId ?? null,\n url_pattern: urlPattern,\n intro,\n parent_surface_name: parent,\n },\n update: { ...owned, ...claimed },\n });\n\n for (const v of d.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: \"\" } : {}),\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.groupKey ?? v.group ?? \"general\",\n ...(schema?.valueContract\n ? {\n exportable: resolveSensitivity(v).exportable,\n classification: resolveSensitivity(v).classification,\n included_by_default: resolveSensitivity(v).includedByDefault,\n kind_key: v.valueKind ?? null,\n live_slot: v.liveSlot ?? null,\n role: v.role ?? \"data\",\n }\n : {}),\n ...provenance,\n });\n }\n\n for (const t of d.writeTargets ?? []) {\n targets.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: t.itemType ?? \"\" } : {}),\n name: t.name,\n label: t.label,\n description: t.description,\n value_type: t.valueType,\n mode: t.mode,\n updates_value: t.updatesValue ?? null,\n group_key: t.group ?? \"general\",\n sort_order: t.sortOrder ?? 1000,\n apply_policy: t.applyPolicy ?? \"manual\",\n kind_key: t.valueKind ?? null,\n ...(schema?.valueContract\n ? {\n patchable: t.patchable ?? false,\n approval_comparison: t.approvalComparison ?? null,\n stream_ops: t.streamOps ? [...t.streamOps] : null,\n destination: t.destination ?? false,\n }\n : {}),\n ...provenance,\n });\n }\n\n if (schema?.itemType) {\n for (const [position, item] of (d.itemTypes ?? []).entries()) {\n itemTypes.push({\n ...governance,\n surface_name: d.surfaceName,\n name: item.name,\n label: item.label,\n description: item.description,\n identity: [...item.identity],\n levels: item.levels ? [...item.levels] : null,\n entity_type: item.entityType ?? null,\n sort_order: position,\n ...provenance,\n });\n for (const v of item.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n item_type: item.name,\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.group ?? \"general\",\n ...provenance,\n });\n }\n }\n }\n\n for (const extension of options.extensions ?? []) {\n for (const entry of extension.toRows?.(d as unknown as D) ?? []) {\n const plan = extensionTables.get(entry.table) ?? {\n table: entry.table,\n conflict: entry.conflict,\n insertOnly: GOVERNANCE_COLUMNS,\n rows: [],\n };\n plan.rows.push({ ...governance, ...entry.row, ...provenance });\n extensionTables.set(entry.table, plan);\n }\n }\n }\n\n const keys: Record<string, readonly string[]> = {\n \"ui.ui_surface_value\": childKey(schema),\n \"ui.ui_surface_write_target\": childKey(schema),\n ...(schema?.itemType ? { \"ui.ui_surface_item_type\": [\"surface_name\", \"name\"] } : {}),\n };\n for (const extension of options.extensions ?? []) {\n if (extension.mirror) keys[extension.mirror.table] = extension.mirror.conflict;\n }\n for (const plan of extensionTables.values()) keys[plan.table] ??= plan.conflict;\n\n const tables: SyncTablePlan[] = [\n { table: \"ui.ui_surface_value\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: values },\n { table: \"ui.ui_surface_write_target\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: targets },\n ...(schema?.itemType\n ? [{ table: \"ui.ui_surface_item_type\", conflict: [\"surface_name\", \"name\"], insertOnly: GOVERNANCE_COLUMNS, rows: itemTypes }]\n : []),\n ...extensionTables.values(),\n ];\n return { surfaces, tables: tables.filter((table) => table.rows.length > 0), keys };\n}\n\n/** One row's key under `columns` (a missing column reads as ''). */\nexport function mirrorRowKey(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {\n return columns.map((column) => String(row[column] ?? \"\")).join(\"\\u0000\");\n}\n\n/**\n * Rows of `table` that belong to a surface this plan manages but that the\n * plan no longer declares, judged by the table's full key (`plan.keys`) — so\n * a screen value and an item value that share a name are distinct rows. The\n * caller deletes each returned row filtered by every column of that key.\n */\nexport function findStaleRows<R extends Readonly<Record<string, unknown>>>(\n plan: SurfaceSyncPlan,\n table: string,\n existing: readonly R[],\n): R[] {\n const key = plan.keys[table];\n if (!key) {\n throw new Error(\n `Sync plan declares no key for \"${table}\"; give the extension that owns it a \\`mirror\\` entry.`,\n );\n }\n const managed = new Set(plan.surfaces.map((surface) => surface.name));\n const declared = new Set(\n (plan.tables.find((entry) => entry.table === table)?.rows ?? []).map((row) => mirrorRowKey(row, key)),\n );\n return existing.filter(\n (row) => managed.has(String(row.surface_name)) && !declared.has(mirrorRowKey(row, key)),\n );\n}\n\n/** Every column a table's rows carry, in first-seen order. */\nexport function planColumns(table: SyncTablePlan): string[] {\n const columns: string[] = [];\n for (const row of table.rows) for (const key of Object.keys(row)) if (!columns.includes(key)) columns.push(key);\n return columns;\n}\n\n/** Columns a conflict rewrites: everything except the key and the insert-only set. */\nexport function conflictUpdateColumns(table: SyncTablePlan): string[] {\n return planColumns(table).filter((c) => !table.conflict.includes(c) && !table.insertOnly.includes(c));\n}\n\nexport function sqlLiteral(value: Json | undefined): string {\n if (value === null || value === undefined) return \"NULL\";\n if (typeof value === \"boolean\" || typeof value === \"number\") return String(value);\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (Array.isArray(value) && value.every((entry) => typeof entry === \"string\")) {\n return `ARRAY[${value.map((entry) => sqlLiteral(entry)).join(\", \")}]::text[]`;\n }\n return `'${JSON.stringify(value).replace(/'/g, \"''\")}'::jsonb`;\n}\n\n/**\n * The plan as SQL statements, one per entry: surface insert, one UPDATE per\n * surface, one upsert per table. Callers may run them, or EXPLAIN each one in a\n * read-only transaction to prove the plan against a live schema with no write.\n */\nexport function renderSurfaceSyncStatements(plan: SurfaceSyncPlan): string[] {\n const statements: string[] = [];\n if (plan.surfaces.length > 0) {\n const insertColumns = Object.keys(plan.surfaces[0]!.insert);\n statements.push(\n `INSERT INTO ui.ui_surface (${insertColumns.join(\", \")}) VALUES\\n${plan.surfaces\n .map((s) => `(${insertColumns.map((c) => sqlLiteral(s.insert[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (name) DO NOTHING`,\n );\n for (const s of plan.surfaces) {\n statements.push(\n `UPDATE ui.ui_surface SET ${Object.entries(s.update)\n .map(([c, v]) => `${c} = ${sqlLiteral(v)}`)\n .join(\", \")}, updated_at = now() WHERE name = ${sqlLiteral(s.name)}`,\n );\n }\n }\n for (const table of plan.tables) {\n const columns = planColumns(table);\n const sets = conflictUpdateColumns(table).map((c) => `${c} = EXCLUDED.${c}`);\n statements.push(\n `INSERT INTO ${table.table} (${columns.join(\", \")}) VALUES\\n${table.rows\n .map((row) => `(${columns.map((c) => sqlLiteral(row[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (${table.conflict.join(\", \")}) DO UPDATE SET ${[...sets, \"updated_at = now()\"].join(\", \")}`,\n );\n }\n return statements;\n}\n\n/** The plan as one SQL script (governance columns insert-only). */\nexport function renderSurfaceSyncSql(plan: SurfaceSyncPlan): string {\n return renderSurfaceSyncStatements(plan)\n .map((statement) => `${statement};`)\n .join(\"\\n\\n\");\n}\n","/**\n * The declaration half of the route check (CONTRACT §2.6 `/checks`): does each\n * declared `urlPattern` address a live route? The host supplies its route list\n * (it owns the filesystem); the matching rule lives here, once. Ported from\n * matrx-frontend `scripts/check-surface-routes.ts` (ALC-14).\n */\n\n/** Route or pattern → segments; `[x]` / `:x` → \"*\", `[...x]` / `[[...x]]` → \"**\". */\nexport function routeSegments(path: string): string[] {\n return (path.split(\"?\")[0] ?? \"\")\n .split(\"#\")[0]!\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => {\n if (/^\\[\\[?\\.\\.\\..+\\]\\]?$/.test(segment)) return \"**\";\n if (segment.startsWith(\"[\") || segment.startsWith(\":\")) return \"*\";\n return segment;\n });\n}\n\n/**\n * Does a declared `urlPattern` address a live route? A literal pattern segment\n * must be a literal route segment — a dynamic route segment does NOT satisfy it,\n * or `/marketing/[brandId]/…` would \"cover\" a retired `/marketing/brands/…`.\n * A trailing `*` makes the pattern a prefix.\n */\nexport function urlPatternMatchesRoute(pattern: string, route: readonly string[]): boolean {\n const isPrefix = pattern.trimEnd().endsWith(\"*\") && !pattern.endsWith(\"]*\");\n const segments = routeSegments(pattern.replace(/\\*+\\s*$/, \"\"));\n for (let i = 0; i < segments.length; i += 1) {\n const routeSegment = route[i];\n if (routeSegment === \"**\") return true;\n if (routeSegment === undefined) return false;\n const declared = segments[i];\n if (declared === \"**\") return true;\n if (declared === \"*\" ? routeSegment !== \"*\" : routeSegment !== declared) return false;\n }\n return isPrefix || route.length === segments.length;\n}\n\n/** Every declaration whose `urlPattern` no live route can produce. */\nexport function findDeadUrlPatterns(\n declarations: readonly { surfaceName: string; urlPattern?: string | undefined }[],\n liveRoutes: readonly string[],\n): { surfaceName: string; urlPattern: string }[] {\n const routes = liveRoutes.map(routeSegments);\n const dead: { surfaceName: string; urlPattern: string }[] = [];\n for (const declaration of declarations) {\n const pattern = declaration.urlPattern;\n if (!pattern) continue;\n if (!routes.some((route) => urlPatternMatchesRoute(pattern, route))) {\n dead.push({ surfaceName: declaration.surfaceName, urlPattern: pattern });\n }\n }\n return dead;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/declare/keys.ts","../src/declare/sensitivity.ts","../src/checks/sync.ts","../src/checks/routes.ts"],"sourcesContent":["import type { QualifiedValueKey } from \"./types\";\n\n/** Every value, write-target, item-type and group name (the DB CHECK rule). */\nexport const DECLARATION_NAME_RE = /^[a-z][a-z0-9_]*$/;\n\n/** \"<client>/<local>\" — the surface naming convention. */\nexport const SURFACE_NAME_RE = /^[a-z][a-z0-9-]*\\/[a-z0-9-/]+$/;\n\n/** A bare screen value (`status`) or an item value (`table_row.status`). */\nexport const QUALIFIED_VALUE_KEY_RE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)?$/;\n\nexport interface ParsedValueKey {\n /** \"\" for a screen-level value. */\n itemType: string;\n name: string;\n}\n\n/** Split a qualified key; null when it is not a valid bare or qualified key. */\nexport function parseQualifiedValueKey(key: string): ParsedValueKey | null {\n if (!QUALIFIED_VALUE_KEY_RE.test(key)) return null;\n const dot = key.indexOf(\".\");\n if (dot === -1) return { itemType: \"\", name: key };\n return { itemType: key.slice(0, dot), name: key.slice(dot + 1) };\n}\n\nexport function qualifyValueKey(itemType: string, name: string): QualifiedValueKey {\n return itemType ? `${itemType}.${name}` : name;\n}\n\n/** The client that owns a surface: its name prefix. */\nexport function surfaceClientOf(surfaceName: string): string {\n return surfaceName.split(\"/\")[0] ?? \"\";\n}\n","/**\n * Sensitivity defaults and resolution (CONTRACT §2.1). Its own module so the\n * lightweight entries (`/actions`, the T0 React host) can project without\n * pulling the declaration registry.\n */\nimport type { DeclaredValue, Sensitivity } from \"./types\";\n\nexport const DEFAULT_SENSITIVITY: Sensitivity = Object.freeze({\n exportable: true,\n classification: \"ordinary\",\n includedByDefault: true,\n});\n\nexport function resolveSensitivity(value: Pick<DeclaredValue, \"sensitivity\">): Sensitivity {\n return {\n exportable: value.sensitivity?.exportable ?? DEFAULT_SENSITIVITY.exportable,\n classification: value.sensitivity?.classification ?? DEFAULT_SENSITIVITY.classification,\n includedByDefault: value.sensitivity?.includedByDefault ?? DEFAULT_SENSITIVITY.includedByDefault,\n };\n}\n","/**\n * THE ONE SYNC (CONTRACT §2.7 \"One sync path\", chair ruling B1).\n *\n * `planSurfaceSync` maps resolved declarations onto the `ui` mirror rows; every\n * sync path (the SQL emitter and the admin sync service) executes this plan and\n * builds no rows of its own. Governance columns (`organization_id`,\n * `published_to_web`) are INSERT-ONLY (chair ruling N5): a conflict never rewrites them.\n * Mirror rows are published to the web on insert, written explicitly (access ladder\n * T-13: never left to a column default); the database stamps when and by whom.\n *\n * Schema generations: the columns added by the ALC-14 migration (item_type,\n * sensitivity, live slots, content_hash) are emitted only when the caller says\n * that migration is live (`schema`), so no emitter ever writes a column — or\n * upserts against a key — the database does not have yet.\n */\nimport { resolveSensitivity, type Resolved } from \"../declare/registry\";\nimport { surfaceClientOf } from \"../declare/keys\";\nimport type { DeclarationExtension, Json, SurfaceDeclaration } from \"../declare/types\";\n\nexport const GOVERNANCE_COLUMNS = [\"organization_id\", \"published_to_web\"] as const;\n\nexport interface SyncSchema {\n /** `ui_surface_value/write_target.item_type` exists and the conflict key includes it (re-key step 1). */\n itemType?: boolean;\n /** Sensitivity, kind, live slot and role columns exist on `ui_surface_value`. */\n valueContract?: boolean;\n /** `ui_surface.content_hash` exists. */\n contentHash?: boolean;\n}\n\nexport interface SurfaceSyncPlanOptions<D extends SurfaceDeclaration> {\n organizationId: string;\n syncedFrom: string;\n syncedBy?: string | null;\n /** The address the host derives for a declaration without `urlPattern`. */\n resolveUrlPattern?: (declaration: Resolved<D>) => string | undefined;\n extensions?: readonly DeclarationExtension<D>[];\n schema?: SyncSchema;\n}\n\nexport interface SurfaceRowPlan {\n name: string;\n /** Written only when the row does not exist yet. */\n insert: Record<string, Json>;\n /** Written on every sync (code owns these columns). */\n update: Record<string, Json>;\n}\n\nexport interface SyncTablePlan {\n table: string;\n /** The ON CONFLICT target (a unique index or primary key on exactly these columns). */\n conflict: readonly string[];\n /** Columns set on insert and never on conflict. */\n insertOnly: readonly string[];\n rows: Record<string, Json>[];\n}\n\nexport interface SurfaceSyncPlan {\n surfaces: SurfaceRowPlan[];\n tables: SyncTablePlan[];\n /**\n * The key (ON CONFLICT target) of every mirror table this plan owns,\n * including tables it emits no rows for. Stale detection and deletes use\n * exactly these columns (`findStaleRows`).\n */\n keys: Record<string, readonly string[]>;\n}\n\nfunction childKey(schema: SyncSchema | undefined): readonly string[] {\n return schema?.itemType ? [\"surface_name\", \"item_type\", \"name\"] : [\"surface_name\", \"name\"];\n}\n\nexport function planSurfaceSync<D extends SurfaceDeclaration>(\n declarations: readonly Resolved<D>[],\n options: SurfaceSyncPlanOptions<D>,\n): SurfaceSyncPlan {\n const { organizationId, syncedFrom, schema } = options;\n const syncedBy = options.syncedBy ?? null;\n const provenance = { synced_by: syncedBy, synced_from: syncedFrom };\n const governance = { organization_id: organizationId, published_to_web: true };\n const surfaces: SurfaceRowPlan[] = [];\n const values: Record<string, Json>[] = [];\n const targets: Record<string, Json>[] = [];\n const itemTypes: Record<string, Json>[] = [];\n const extensionTables = new Map<string, SyncTablePlan>();\n\n for (const d of declarations) {\n const urlPattern = d.urlPattern?.trim() || options.resolveUrlPattern?.(d) || null;\n const intro = d.intro?.trim() || null;\n const parent = d.inheritsFrom ?? null;\n const client = surfaceClientOf(d.surfaceName);\n const owned: Record<string, Json> = {\n client_name: client,\n execution_mode: d.executionMode,\n description: d.description,\n label: d.label,\n value_groups: (d.groups ?? []) as unknown as Json,\n readiness: d.readiness,\n readiness_note: d.readinessNote?.trim() || null,\n ...(schema?.contentHash ? { content_hash: d.contentHash } : {}),\n };\n // A declaration that does not claim these never clears a DB-authored value.\n const claimed: Record<string, Json> = {\n ...(d.overlayId ? { overlay_id: d.overlayId } : {}),\n ...(urlPattern ? { url_pattern: urlPattern } : {}),\n ...(intro ? { intro } : {}),\n ...(parent ? { parent_surface_name: parent } : {}),\n };\n surfaces.push({\n name: d.surfaceName,\n insert: {\n name: d.surfaceName,\n ...owned,\n overlay_id: d.overlayId ?? null,\n url_pattern: urlPattern,\n intro,\n parent_surface_name: parent,\n },\n update: { ...owned, ...claimed },\n });\n\n for (const v of d.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: \"\" } : {}),\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.groupKey ?? v.group ?? \"general\",\n ...(schema?.valueContract\n ? {\n exportable: resolveSensitivity(v).exportable,\n classification: resolveSensitivity(v).classification,\n included_by_default: resolveSensitivity(v).includedByDefault,\n kind_key: v.valueKind ?? null,\n live_slot: v.liveSlot ?? null,\n role: v.role ?? \"data\",\n }\n : {}),\n ...provenance,\n });\n }\n\n for (const t of d.writeTargets ?? []) {\n targets.push({\n ...governance,\n surface_name: d.surfaceName,\n ...(schema?.itemType ? { item_type: t.itemType ?? \"\" } : {}),\n name: t.name,\n label: t.label,\n description: t.description,\n value_type: t.valueType,\n mode: t.mode,\n updates_value: t.updatesValue ?? null,\n group_key: t.group ?? \"general\",\n sort_order: t.sortOrder ?? 1000,\n apply_policy: t.applyPolicy ?? \"manual\",\n kind_key: t.valueKind ?? null,\n ...(schema?.valueContract\n ? {\n patchable: t.patchable ?? false,\n approval_comparison: t.approvalComparison ?? null,\n stream_ops: t.streamOps ? [...t.streamOps] : null,\n destination: t.destination ?? false,\n }\n : {}),\n ...provenance,\n });\n }\n\n if (schema?.itemType) {\n for (const [position, item] of (d.itemTypes ?? []).entries()) {\n itemTypes.push({\n ...governance,\n surface_name: d.surfaceName,\n name: item.name,\n label: item.label,\n description: item.description,\n identity: [...item.identity],\n levels: item.levels ? [...item.levels] : null,\n entity_type: item.entityType ?? null,\n sort_order: position,\n ...provenance,\n });\n for (const v of item.values) {\n values.push({\n ...governance,\n surface_name: d.surfaceName,\n item_type: item.name,\n name: v.name,\n label: v.label,\n description: v.description,\n value_type: v.valueType,\n always_available: v.alwaysAvailable,\n typical_char_count: v.typicalCharCount,\n sort_order: v.sortOrder ?? 1000,\n auto_context: v.autoContext ?? true,\n group_key: v.group ?? \"general\",\n ...provenance,\n });\n }\n }\n }\n\n for (const extension of options.extensions ?? []) {\n for (const entry of extension.toRows?.(d as unknown as D) ?? []) {\n const plan = extensionTables.get(entry.table) ?? {\n table: entry.table,\n conflict: entry.conflict,\n insertOnly: GOVERNANCE_COLUMNS,\n rows: [],\n };\n plan.rows.push({ ...governance, ...entry.row, ...provenance });\n extensionTables.set(entry.table, plan);\n }\n }\n }\n\n const keys: Record<string, readonly string[]> = {\n \"ui.ui_surface_value\": childKey(schema),\n \"ui.ui_surface_write_target\": childKey(schema),\n ...(schema?.itemType ? { \"ui.ui_surface_item_type\": [\"surface_name\", \"name\"] } : {}),\n };\n for (const extension of options.extensions ?? []) {\n if (extension.mirror) keys[extension.mirror.table] = extension.mirror.conflict;\n }\n for (const plan of extensionTables.values()) keys[plan.table] ??= plan.conflict;\n\n const tables: SyncTablePlan[] = [\n { table: \"ui.ui_surface_value\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: values },\n { table: \"ui.ui_surface_write_target\", conflict: childKey(schema), insertOnly: GOVERNANCE_COLUMNS, rows: targets },\n ...(schema?.itemType\n ? [{ table: \"ui.ui_surface_item_type\", conflict: [\"surface_name\", \"name\"], insertOnly: GOVERNANCE_COLUMNS, rows: itemTypes }]\n : []),\n ...extensionTables.values(),\n ];\n return { surfaces, tables: tables.filter((table) => table.rows.length > 0), keys };\n}\n\n/** One row's key under `columns` (a missing column reads as ''). */\nexport function mirrorRowKey(row: Readonly<Record<string, unknown>>, columns: readonly string[]): string {\n return columns.map((column) => String(row[column] ?? \"\")).join(\"\\u0000\");\n}\n\n/**\n * Rows of `table` that belong to a surface this plan manages but that the\n * plan no longer declares, judged by the table's full key (`plan.keys`) — so\n * a screen value and an item value that share a name are distinct rows. The\n * caller deletes each returned row filtered by every column of that key.\n */\nexport function findStaleRows<R extends Readonly<Record<string, unknown>>>(\n plan: SurfaceSyncPlan,\n table: string,\n existing: readonly R[],\n): R[] {\n const key = plan.keys[table];\n if (!key) {\n throw new Error(\n `Sync plan declares no key for \"${table}\"; give the extension that owns it a \\`mirror\\` entry.`,\n );\n }\n const managed = new Set(plan.surfaces.map((surface) => surface.name));\n const declared = new Set(\n (plan.tables.find((entry) => entry.table === table)?.rows ?? []).map((row) => mirrorRowKey(row, key)),\n );\n return existing.filter(\n (row) => managed.has(String(row.surface_name)) && !declared.has(mirrorRowKey(row, key)),\n );\n}\n\n/** Every column a table's rows carry, in first-seen order. */\nexport function planColumns(table: SyncTablePlan): string[] {\n const columns: string[] = [];\n for (const row of table.rows) for (const key of Object.keys(row)) if (!columns.includes(key)) columns.push(key);\n return columns;\n}\n\n/** Columns a conflict rewrites: everything except the key and the insert-only set. */\nexport function conflictUpdateColumns(table: SyncTablePlan): string[] {\n return planColumns(table).filter((c) => !table.conflict.includes(c) && !table.insertOnly.includes(c));\n}\n\nexport function sqlLiteral(value: Json | undefined): string {\n if (value === null || value === undefined) return \"NULL\";\n if (typeof value === \"boolean\" || typeof value === \"number\") return String(value);\n if (typeof value === \"string\") return `'${value.replace(/'/g, \"''\")}'`;\n if (Array.isArray(value) && value.every((entry) => typeof entry === \"string\")) {\n return `ARRAY[${value.map((entry) => sqlLiteral(entry)).join(\", \")}]::text[]`;\n }\n return `'${JSON.stringify(value).replace(/'/g, \"''\")}'::jsonb`;\n}\n\n/**\n * The plan as SQL statements, one per entry: surface insert, one UPDATE per\n * surface, one upsert per table. Callers may run them, or EXPLAIN each one in a\n * read-only transaction to prove the plan against a live schema with no write.\n */\nexport function renderSurfaceSyncStatements(plan: SurfaceSyncPlan): string[] {\n const statements: string[] = [];\n if (plan.surfaces.length > 0) {\n const insertColumns = Object.keys(plan.surfaces[0]!.insert);\n statements.push(\n `INSERT INTO ui.ui_surface (${insertColumns.join(\", \")}) VALUES\\n${plan.surfaces\n .map((s) => `(${insertColumns.map((c) => sqlLiteral(s.insert[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (name) DO NOTHING`,\n );\n for (const s of plan.surfaces) {\n statements.push(\n `UPDATE ui.ui_surface SET ${Object.entries(s.update)\n .map(([c, v]) => `${c} = ${sqlLiteral(v)}`)\n .join(\", \")}, updated_at = now() WHERE name = ${sqlLiteral(s.name)}`,\n );\n }\n }\n for (const table of plan.tables) {\n const columns = planColumns(table);\n const sets = conflictUpdateColumns(table).map((c) => `${c} = EXCLUDED.${c}`);\n statements.push(\n `INSERT INTO ${table.table} (${columns.join(\", \")}) VALUES\\n${table.rows\n .map((row) => `(${columns.map((c) => sqlLiteral(row[c])).join(\", \")})`)\n .join(\",\\n\")}\\nON CONFLICT (${table.conflict.join(\", \")}) DO UPDATE SET ${[...sets, \"updated_at = now()\"].join(\", \")}`,\n );\n }\n return statements;\n}\n\n/** The plan as one SQL script (governance columns insert-only). */\nexport function renderSurfaceSyncSql(plan: SurfaceSyncPlan): string {\n return renderSurfaceSyncStatements(plan)\n .map((statement) => `${statement};`)\n .join(\"\\n\\n\");\n}\n","/**\n * The declaration half of the route check (CONTRACT §2.6 `/checks`): does each\n * declared `urlPattern` address a live route? The host supplies its route list\n * (it owns the filesystem); the matching rule lives here, once. Ported from\n * matrx-frontend `scripts/check-surface-routes.ts` (ALC-14).\n */\n\n/** Route or pattern → segments; `[x]` / `:x` → \"*\", `[...x]` / `[[...x]]` → \"**\". */\nexport function routeSegments(path: string): string[] {\n return (path.split(\"?\")[0] ?? \"\")\n .split(\"#\")[0]!\n .split(\"/\")\n .filter(Boolean)\n .map((segment) => {\n if (/^\\[\\[?\\.\\.\\..+\\]\\]?$/.test(segment)) return \"**\";\n if (segment.startsWith(\"[\") || segment.startsWith(\":\")) return \"*\";\n return segment;\n });\n}\n\n/**\n * Does a declared `urlPattern` address a live route? A literal pattern segment\n * must be a literal route segment — a dynamic route segment does NOT satisfy it,\n * or `/marketing/[brandId]/…` would \"cover\" a retired `/marketing/brands/…`.\n * A trailing `*` makes the pattern a prefix.\n */\nexport function urlPatternMatchesRoute(pattern: string, route: readonly string[]): boolean {\n const isPrefix = pattern.trimEnd().endsWith(\"*\") && !pattern.endsWith(\"]*\");\n const segments = routeSegments(pattern.replace(/\\*+\\s*$/, \"\"));\n for (let i = 0; i < segments.length; i += 1) {\n const routeSegment = route[i];\n if (routeSegment === \"**\") return true;\n if (routeSegment === undefined) return false;\n const declared = segments[i];\n if (declared === \"**\") return true;\n if (declared === \"*\" ? routeSegment !== \"*\" : routeSegment !== declared) return false;\n }\n return isPrefix || route.length === segments.length;\n}\n\n/** Every declaration whose `urlPattern` no live route can produce. */\nexport function findDeadUrlPatterns(\n declarations: readonly { surfaceName: string; urlPattern?: string | undefined }[],\n liveRoutes: readonly string[],\n): { surfaceName: string; urlPattern: string }[] {\n const routes = liveRoutes.map(routeSegments);\n const dead: { surfaceName: string; urlPattern: string }[] = [];\n for (const declaration of declarations) {\n const pattern = declaration.urlPattern;\n if (!pattern) continue;\n if (!routes.some((route) => urlPatternMatchesRoute(pattern, route))) {\n dead.push({ surfaceName: declaration.surfaceName, urlPattern: pattern });\n }\n }\n return dead;\n}\n"],"mappings":"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","names":[]}
|
package/dist/menu.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var menu_exports={};__export(menu_exports,{APPROVED_HEADINGS:()=>APPROVED_HEADINGS,CATEGORY_GROUP:()=>CATEGORY_GROUP,GROUP_ORDER:()=>GROUP_ORDER,INLINE_SECTION_MAX:()=>INLINE_SECTION_MAX,MENU_LAYOUT_SETTING:()=>MENU_LAYOUT_SETTING,PLATFORM_MENU_LAYOUT:()=>PLATFORM_MENU_LAYOUT,UnapprovedHeadingError:()=>UnapprovedHeadingError,actionToNode:()=>actionToNode,bar:()=>bar,buildMenuModel:()=>buildMenuModel,context:()=>context,defaultHeadingPolicy:()=>defaultHeadingPolicy,filterPalette:()=>filterPalette,findNode:()=>findNode,foldUnderSubject:()=>foldUnderSubject,isApprovedHeading:()=>isApprovedHeading,isContainerWord:()=>isContainerWord,layouts:()=>layouts,leafIds:()=>leafIds,menuHeadings:()=>menuHeadings,overflow:()=>overflow,palette:()=>palette,resolveMenuLayout:()=>resolveMenuLayout,sectionLabel:()=>sectionLabel,selectionToolbar:()=>selectionToolbar,sheet:()=>sheet,unapprovedHeadings:()=>unapprovedHeadings,withExpandedChildren:()=>withExpandedChildren});module.exports=__toCommonJS(menu_exports);var APPROVED_HEADINGS=new Set(["Save","Copy as","Export","Edit","Creator tools","Admin","App","Compare","History"]),CONTAINER_WORD=/^(more|more…|more\.\.\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i,TARGET_SHAPE=/^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;function isContainerWord(label){return CONTAINER_WORD.test(label.trim())}function isApprovedHeading(label,kind="group",approved=APPROVED_HEADINGS){let l=label.trim();return!l||isContainerWord(l)?!1:approved.has(l)?!0:kind==="target"||TARGET_SHAPE.test(l)}function defaultHeadingPolicy(){return globalThis.process?.env?.NODE_ENV==="production"?"inline":"throw"}var UnapprovedHeadingError=class extends Error{heading;constructor(heading,where){super(`Menu heading "${heading}" (${where}) is not an approved heading. Remedy: show these rows inline \u2014 declare no section (or kind "target" for the clicked thing's own label) \u2014 or ask Arman to add the name to APPROVED_HEADINGS in @ai-matrx/alchemy/menu. A generic container word ("More\u2026", "Other") is never approved.`),this.name="UnapprovedHeadingError",this.heading=heading}};function menuHeadings(model){let out=[],walk=nodes=>{for(let n of nodes)n.kind==="submenu"&&(out.push(n.label),walk(n.children))};for(let s of model.sections)s.label&&out.push(s.label),walk(s.nodes);return[...new Set(out)]}function unapprovedHeadings(model,approved=APPROVED_HEADINGS){let out=new Set;for(let s of model.sections){s.label&&!isApprovedHeading(s.label,s.primary||s.target?"target":"group",approved)&&out.add(s.label);let walk=nodes=>{for(let n of nodes)n.kind==="submenu"&&(isContainerWord(n.label)&&out.add(n.label),walk(n.children))};walk(s.nodes)}return[...out]}var CATEGORY_GROUP={clipboard:"clipboard",copy:"transfer",export:"transfer",save:"document",share:"document",edit:"editable",history:"history",ai:"ai",ask:"ai",listen:"tools",study:"tools",app:"tools",feedback:"quick",creator:"admin",admin:"admin","surface-info":"surface-info"},GROUP_ORDER=["clipboard","editable","transfer","document","history","ai","tools","quick","surface","admin","surface-info"],CATEGORY_ORDER=["clipboard","edit","copy","export","save","share","history","ask","ai","listen","study","app","feedback","creator","admin","surface-info"];function translate(i18n,key,fallback){if(!i18n)return fallback;try{return i18n.t(key,fallback)}catch{return fallback}}function actionToNode(resolved,target,options={}){let{action,eligibility}=resolved,rawLabel=typeof action.label=="function"?action.label(target):action.label,label=translate(options.i18n,action.labelKey??action.id,rawLabel),base={id:action.id,actionId:action.id,...action.icon!==void 0?{icon:action.icon}:{},...action.placement!==void 0?{placement:action.placement}:{}};if(action.expand)return{...base,kind:"submenu",label,lazy:!0,loading:!1,...action.pending?.(target)?{pending:!0}:{},children:[],...action.emptyLabel!==void 0?{emptyLabel:action.emptyLabel}:{}};if(action.href!==void 0){let href=typeof action.href=="function"?action.href(target):action.href;return{...base,kind:"link",label,href}}if(action.toggle)return{...base,kind:"checkbox",label,checked:action.pressed?.(target)??!1,...action.description!==void 0?{description:action.description}:{}};let state=action.stateIcon?.(target)??null,pressed=action.pressed?.(target),hint=action.hint!==void 0?translate(options.i18n,action.hintKey??`${action.id}.hint`,action.hint):void 0;return{...base,kind:"item",label,...action.iconTone!==void 0?{iconTone:action.iconTone}:{},...state?{stateIcon:state}:{},...action.description!==void 0?{description:action.description}:{},...hint!==void 0?{hint}:{},...pressed!==void 0?{pressed}:{},...action.destructive?{destructive:!0}:{},...action.preserveSelection?{preserveSelection:!0}:{},...eligibility.status==="unavailable-verb"||eligibility.status==="unavailable"?{unavailable:{sentence:eligibility.sentence}}:{}}}function buildMenuModel(target,resolved,options={}){let policy=options.headingPolicy??defaultHeadingPolicy();resolved=resolved.map(r=>{let sec=r.action.section;if(r.action.expand&&isContainerWord(typeof r.action.label=="string"?r.action.label:"")&&policy==="throw")throw new UnapprovedHeadingError(r.action.label,`submenu of ${r.action.id}`);if(!sec)return r;let kind=sec.kind??(sec.primary?"target":"group");if(isApprovedHeading(sec.label,kind))return r;if(policy==="throw")throw new UnapprovedHeadingError(sec.label,`section of ${r.action.id}`);let{section:_dropped,...rest}=r.action;return{...r,action:rest}});let ordered=resolved.map((r,index)=>({r,index})).sort((a,b)=>{let ca=CATEGORY_ORDER.indexOf(a.r.action.category),cb=CATEGORY_ORDER.indexOf(b.r.action.category);if(ca!==cb)return ca-cb;let oa=a.r.action.order??0,ob=b.r.action.order??0;return oa!==ob?oa-ob:a.index-b.index}).map(x=>x.r),sections=new Map,roles={},explicit=new Map;resolved.forEach((r,index)=>{let sec=r.action.section;if(!sec)return;let entry=explicit.get(sec.id)??{group:CATEGORY_GROUP[r.action.category],rows:[]};entry.rows.push({r,index}),explicit.set(sec.id,entry)});let placed=new Set;for(let r of ordered){let sec=r.action.section;if(sec&&placed.has(sec.id))continue;let group=sec?explicit.get(sec.id)?.group??CATEGORY_GROUP[r.action.category]:CATEGORY_GROUP[r.action.category],sectionId=sec?`section:${sec.id}`:group,section=sections.get(sectionId);section||(section={id:sectionId,group,nodes:[],...sec?{label:translate(options.i18n,sec.labelKey??`section.${sec.id}`,sec.label)}:{},...sec?.icon!==void 0?{icon:sec.icon}:{},...sec?.primary?{primary:!0}:{},...sec&&(sec.kind??(sec.primary?"target":"group"))==="target"?{target:!0}:{}},sections.set(sectionId,section));let rows=sec?(explicit.get(sec.id)?.rows??[]).sort((x,y)=>(x.r.action.order??0)-(y.r.action.order??0)||x.index-y.index).map(x=>x.r):[r];sec&&placed.add(sec.id);for(let row of rows)section.nodes.push(actionToNode(row,target,options)),row.action.verb&&(roles[row.action.verb]=row.action.id)}let list=[...sections.values()].sort((a,b)=>!!a.primary!=!!b.primary?a.primary?-1:1:GROUP_ORDER.indexOf(a.group)-GROUP_ORDER.indexOf(b.group)),selected=target.selection?.text?.trim(),content=options.content?.trim(),fieldName=options.contentLabel?.trim();return{header:selected?{label:"Selected",text:target.selection?.text??""}:fieldName?{label:fieldName,text:options.content??""}:content?{label:"Content",text:options.content??""}:null,sections:list,roles}}function withExpandedChildren(model,nodeId,children){let visit=nodes=>nodes.map(n=>n.kind!=="submenu"?n:n.id===nodeId?children==="loading"?{...n,loading:!0}:{...n,lazy:!1,loading:!1,pending:!1,children}:{...n,children:visit(n.children)});return{...model,sections:model.sections.map(s=>({...s,nodes:visit(s.nodes)}))}}function findNode(nodes,id){for(let n of nodes){if(n.id===id)return n;if(n.kind==="submenu"){let hit=findNode(n.children,id);if(hit)return hit}}return null}var INLINE_SECTION_MAX=3;function sectionLabel(section){return section.label??null}function isLeaf(node){return node.kind==="item"||node.kind==="checkbox"||node.kind==="link"}function collect(nodes,out){for(let n of nodes)isLeaf(n)?out.push(n.id):n.kind==="submenu"&&(n.lazy?out.push(n.id):collect(n.children,out))}function leafIds(arranged){let out=[];if(Array.isArray(arranged))for(let s of arranged)collect(s.nodes,out);else if("leaves"in arranged)for(let l of arranged.leaves)out.push(l.node.id);else if("primary"in arranged&&"overflow"in arranged){collect(arranged.primary,out);for(let s of arranged.overflow)collect(s.nodes,out)}else if("root"in arranged)collect(arranged.strip,out),collect(arranged.root,out);else if("more"in arranged){collect(arranged.strip,out);for(let s of arranged.more)collect(s.nodes,out)}else if("filterable"in arranged){collect(arranged.strip,out);for(let s of arranged.sections)collect(s.nodes,out)}else for(let s of arranged.sections)collect(s.nodes,out);return[...new Set(out)].sort()}function fold(section){return{kind:"submenu",id:`${section.id}:fold`,label:section.label,...section.icon!==void 0?{icon:section.icon}:{},children:section.nodes}}function rowCount(nodes){return nodes.filter(n=>n.kind!=="separator"&&n.kind!=="label").length}function stripOf(model){let clipboard=model.sections.find(s=>s.group==="clipboard"&&!s.primary);return clipboard?clipboard.nodes.filter(isLeaf):[]}function clipboardRest(model){let clipboard=model.sections.find(s=>s.group==="clipboard"&&!s.primary);return clipboard?clipboard.nodes.filter(n=>!isLeaf(n)&&n.kind==="submenu"):[]}function placementOf(node){return"placement"in node&&node.placement?node.placement:"overflow"}function classic(model){return{strip:[],sections:model.sections,filterable:!1}}function tiered(model,filterable){let strip=stripOf(model),sections=[],rest=clipboardRest(model);for(let s of model.sections){if(s.group==="clipboard"&&!s.primary){rest.length&§ions.push({...s,nodes:rest});continue}if(s.primary){sections.push(s);continue}if(!s.label||s.group==="surface-info"||rowCount(s.nodes)<=3){sections.push(s);continue}sections.push({id:s.id,group:s.group,nodes:[fold({...s,label:s.label})]})}return{strip,sections,filterable}}function context(model,arrangement){return arrangement==="classic"?classic(model):tiered(model,arrangement==="command")}function foldUnderSubject(arranged,subject){let label=subject.trim(),primary=arranged.sections.filter(s=>s.primary),rest=arranged.sections.filter(s=>!s.primary);if(!label||primary.length===0||rest.length===0&&arranged.strip.length===0)return arranged;let children=[];arranged.strip.length&&children.push(...arranged.strip);for(let s of rest)rowCount(s.nodes)!==0&&(children.length&&children.push({kind:"separator",id:`${s.id}:subject-sep`}),s.label&&children.push({kind:"label",id:`${s.id}:subject-label`,label:s.label}),children.push(...s.nodes));return rowCount(children)===0?{...arranged,strip:[]}:{strip:[],filterable:!1,sections:[...primary,{id:"subject",group:rest[0]?.group??primary[0].group,nodes:[{kind:"submenu",id:"subject:fold",label,children}]}]}}function foldNamed(sections){return sections.map(s=>s.label&&!s.primary&&rowCount(s.nodes)>3?{id:s.id,group:s.group,nodes:[fold({...s,label:s.label})]}:s)}function overflow(model){return foldNamed(model.sections)}function bar(model,_options={}){let primary=[],overflowSections=[];for(let s of model.sections){let rest=[];for(let n of s.nodes){if(n.kind==="separator"||n.kind==="label"){rest.push(n);continue}let p=placementOf(n);(p==="primary"||p==="both")&&primary.push(n),(p==="overflow"||p==="both")&&rest.push(n)}rowCount(rest)>0&&overflowSections.push({...s,nodes:rest})}return{primary,overflow:foldNamed(overflowSections)}}function selectionToolbar(model){let strip=[],more=[];for(let s of model.sections){let rest=[];for(let n of s.nodes)s.group==="clipboard"&&!s.primary&&isLeaf(n)||placementOf(n)!=="overflow"?strip.push(n):rest.push(n);rowCount(rest)>0&&more.push({...s,nodes:rest})}return{strip,more}}function sheet(model){let strip=stripOf(model),root=[],rest=clipboardRest(model);for(let s of model.sections){let nodes=s.group==="clipboard"&&!s.primary?rest:s.nodes;rowCount(nodes)!==0&&(root.length&&root.push({kind:"separator",id:`${s.id}:sep`}),s.primary||!s.label||rowCount(nodes)<=3?(s.label&&root.push({kind:"label",id:`${s.id}:label`,label:s.label}),root.push(...nodes)):root.push(fold({...s,label:s.label,nodes})))}return{strip,root}}function palette(model){let leaves=[],walk=(nodes,trail)=>{for(let n of nodes)isLeaf(n)?leaves.push({node:n,breadcrumb:trail}):n.kind==="submenu"&&(n.lazy?leaves.push({node:n,breadcrumb:trail}):walk(n.children,[...trail,n.label]))};for(let s of model.sections)walk(s.nodes,s.label?[s.label]:[]);return{leaves}}function filterPalette(arranged,query){let words=query.toLowerCase().split(/\s+/).filter(Boolean);return words.length?{leaves:arranged.leaves.filter(l=>{let hay=`${l.node.label} ${l.breadcrumb.join(" ")}`.toLowerCase();return words.every(w=>hay.includes(w))})}:arranged}var layouts={bar,overflow,context,selectionToolbar,sheet,palette};var PLATFORM_MENU_LAYOUT={context:"command",density:"comfortable"},MENU_LAYOUT_SETTING="alchemy.menu.layout",ARRANGEMENTS=new Set(["classic","tiered","command"]),DENSITIES=new Set(["comfortable","compact"]);async function resolveMenuLayout(ports,organizationId){let org=ports.knobs?.layout(organizationId)??null,base={...PLATFORM_MENU_LAYOUT,...org??{}},own=null;try{own=await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)??null}catch{own=null}if(own&&typeof own=="object"&&!Array.isArray(own)){let o=own;return{context:typeof o.context=="string"&&ARRANGEMENTS.has(o.context)?o.context:base.context,density:typeof o.density=="string"&&DENSITIES.has(o.density)?o.density:base.density}}return base}
|
|
1
|
+
"use strict";var __defProp=Object.defineProperty;var __getOwnPropDesc=Object.getOwnPropertyDescriptor;var __getOwnPropNames=Object.getOwnPropertyNames;var __hasOwnProp=Object.prototype.hasOwnProperty;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:!0})},__copyProps=(to,from,except,desc)=>{if(from&&typeof from=="object"||typeof from=="function")for(let key of __getOwnPropNames(from))!__hasOwnProp.call(to,key)&&key!==except&&__defProp(to,key,{get:()=>from[key],enumerable:!(desc=__getOwnPropDesc(from,key))||desc.enumerable});return to};var __toCommonJS=mod=>__copyProps(__defProp({},"__esModule",{value:!0}),mod);var menu_exports={};__export(menu_exports,{APPROVED_HEADINGS:()=>APPROVED_HEADINGS,CATEGORY_GROUP:()=>CATEGORY_GROUP,GROUP_ORDER:()=>GROUP_ORDER,INLINE_SECTION_MAX:()=>INLINE_SECTION_MAX,MENU_LAYOUT_SETTING:()=>MENU_LAYOUT_SETTING,PLATFORM_MENU_LAYOUT:()=>PLATFORM_MENU_LAYOUT,UnapprovedHeadingError:()=>UnapprovedHeadingError,actionToNode:()=>actionToNode,bar:()=>bar,buildMenuModel:()=>buildMenuModel,context:()=>context,defaultHeadingPolicy:()=>defaultHeadingPolicy,filterPalette:()=>filterPalette,findNode:()=>findNode,foldUnderSubject:()=>foldUnderSubject,isApprovedHeading:()=>isApprovedHeading,isContainerWord:()=>isContainerWord,layouts:()=>layouts,leafIds:()=>leafIds,menuHeadings:()=>menuHeadings,overflow:()=>overflow,palette:()=>palette,resolveMenuLayout:()=>resolveMenuLayout,sectionLabel:()=>sectionLabel,selectionToolbar:()=>selectionToolbar,sheet:()=>sheet,unapprovedHeadings:()=>unapprovedHeadings,withExpandedChildren:()=>withExpandedChildren});module.exports=__toCommonJS(menu_exports);var APPROVED_HEADINGS=new Set(["Save","Copy as","Export","Edit","Creator tools","Admin","App","Compare","History"]),CONTAINER_WORD=/^(more|more…|more\.\.\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i,TARGET_SHAPE=/^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;function isContainerWord(label){return CONTAINER_WORD.test(label.trim())}function isApprovedHeading(label,kind="group",approved=APPROVED_HEADINGS){let l=label.trim();return!l||isContainerWord(l)?!1:approved.has(l)?!0:kind==="target"||TARGET_SHAPE.test(l)}function defaultHeadingPolicy(){return globalThis.process?.env?.NODE_ENV==="production"?"inline":"throw"}var UnapprovedHeadingError=class extends Error{heading;constructor(heading,where){super(`Menu heading "${heading}" (${where}) is not an approved heading. Remedy: show these rows inline \u2014 declare no section (or kind "target" for the clicked thing's own label) \u2014 or ask Arman to add the name to APPROVED_HEADINGS in @ai-matrx/alchemy/menu. A generic container word ("More\u2026", "Other") is never approved.`),this.name="UnapprovedHeadingError",this.heading=heading}};function menuHeadings(model){let out=[],walk=nodes=>{for(let n of nodes)n.kind==="submenu"&&(out.push(n.label),walk(n.children))};for(let s of model.sections)s.label&&out.push(s.label),walk(s.nodes);return[...new Set(out)]}function unapprovedHeadings(model,approved=APPROVED_HEADINGS){let out=new Set;for(let s of model.sections){s.label&&!isApprovedHeading(s.label,s.primary||s.target?"target":"group",approved)&&out.add(s.label);let walk=nodes=>{for(let n of nodes)n.kind==="submenu"&&(isContainerWord(n.label)&&out.add(n.label),walk(n.children))};walk(s.nodes)}return[...out]}var CATEGORY_GROUP={clipboard:"clipboard",copy:"transfer",export:"transfer",save:"document",share:"document",edit:"editable",history:"history",ai:"ai",ask:"ai",listen:"tools",study:"tools",app:"tools",feedback:"quick",creator:"admin",admin:"admin","surface-info":"surface-info"},GROUP_ORDER=["clipboard","editable","transfer","document","history","ai","tools","quick","surface","admin","surface-info"],CATEGORY_ORDER=["clipboard","edit","copy","export","save","share","history","ask","ai","listen","study","app","feedback","creator","admin","surface-info"];function translate(i18n,key,fallback){if(!i18n)return fallback;try{return i18n.t(key,fallback)}catch{return fallback}}function actionToNode(resolved,target,options={}){let{action,eligibility}=resolved,rawLabel=typeof action.label=="function"?action.label(target):action.label,label=translate(options.i18n,action.labelKey??action.id,rawLabel),base={id:action.id,actionId:action.id,...action.icon!==void 0?{icon:action.icon}:{},...action.placement!==void 0?{placement:action.placement}:{}};if(action.expand)return{...base,kind:"submenu",label,lazy:!0,loading:!1,...action.pending?.(target)?{pending:!0}:{},children:[],...action.emptyLabel!==void 0?{emptyLabel:action.emptyLabel}:{}};if(action.href!==void 0){let href=typeof action.href=="function"?action.href(target):action.href;return{...base,kind:"link",label,href}}if(action.toggle)return{...base,kind:"checkbox",label,checked:action.pressed?.(target)??!1,...action.description!==void 0?{description:action.description}:{}};let state=action.stateIcon?.(target)??null,pressed=action.pressed?.(target),hint=action.hint!==void 0?translate(options.i18n,action.hintKey??`${action.id}.hint`,action.hint):void 0;return{...base,kind:"item",label,...action.iconTone!==void 0?{iconTone:action.iconTone}:{},...state?{stateIcon:state}:{},...action.description!==void 0?{description:action.description}:{},...hint!==void 0?{hint}:{},...pressed!==void 0?{pressed}:{},...action.destructive?{destructive:!0}:{},...action.preserveSelection?{preserveSelection:!0}:{},...eligibility.status==="unavailable-verb"||eligibility.status==="unavailable"?{unavailable:{sentence:eligibility.sentence}}:{}}}function buildMenuModel(target,resolved,options={}){let policy=options.headingPolicy??defaultHeadingPolicy();resolved=resolved.map(r=>{let sec=r.action.section;if(r.action.expand&&isContainerWord(typeof r.action.label=="string"?r.action.label:"")&&policy==="throw")throw new UnapprovedHeadingError(r.action.label,`submenu of ${r.action.id}`);if(!sec)return r;let kind=sec.kind??(sec.primary?"target":"group");if(isApprovedHeading(sec.label,kind))return r;if(policy==="throw")throw new UnapprovedHeadingError(sec.label,`section of ${r.action.id}`);let{section:_dropped,...rest}=r.action;return{...r,action:rest}});let ordered=resolved.map((r,index)=>({r,index})).sort((a,b)=>{let ca=CATEGORY_ORDER.indexOf(a.r.action.category),cb=CATEGORY_ORDER.indexOf(b.r.action.category);if(ca!==cb)return ca-cb;let oa=a.r.action.order??0,ob=b.r.action.order??0;return oa!==ob?oa-ob:a.index-b.index}).map(x=>x.r),sections=new Map,roles={},explicit=new Map;resolved.forEach((r,index)=>{let sec=r.action.section;if(!sec)return;let entry=explicit.get(sec.id)??{group:CATEGORY_GROUP[r.action.category],rows:[]};entry.rows.push({r,index}),explicit.set(sec.id,entry)});let placed=new Set;for(let r of ordered){let sec=r.action.section;if(sec&&placed.has(sec.id))continue;let group=sec?explicit.get(sec.id)?.group??CATEGORY_GROUP[r.action.category]:CATEGORY_GROUP[r.action.category],sectionId=sec?`section:${sec.id}`:group,section=sections.get(sectionId);section||(section={id:sectionId,group,nodes:[],...sec?{label:translate(options.i18n,sec.labelKey??`section.${sec.id}`,sec.label)}:{},...sec?.icon!==void 0?{icon:sec.icon}:{},...sec?.primary?{primary:!0}:{},...sec&&(sec.kind??(sec.primary?"target":"group"))==="target"?{target:!0}:{}},sections.set(sectionId,section));let rows=sec?(explicit.get(sec.id)?.rows??[]).sort((x,y)=>(x.r.action.order??0)-(y.r.action.order??0)||x.index-y.index).map(x=>x.r):[r];sec&&placed.add(sec.id);for(let row of rows)row.action.startsGroup&§ion.nodes.length>0&§ion.nodes.push({kind:"separator",id:`${row.action.id}:group`}),section.nodes.push(actionToNode(row,target,options)),row.action.verb&&(roles[row.action.verb]=row.action.id)}let list=[...sections.values()].sort((a,b)=>!!a.primary!=!!b.primary?a.primary?-1:1:GROUP_ORDER.indexOf(a.group)-GROUP_ORDER.indexOf(b.group)),selected=target.selection?.text?.trim(),content=options.content?.trim(),fieldName=options.contentLabel?.trim();return{header:selected?{label:"Selected",text:target.selection?.text??""}:fieldName?{label:fieldName,text:options.content??""}:content?{label:"Content",text:options.content??""}:null,sections:list,roles}}function withExpandedChildren(model,nodeId,children){let visit=nodes=>nodes.map(n=>n.kind!=="submenu"?n:n.id===nodeId?children==="loading"?{...n,loading:!0}:{...n,lazy:!1,loading:!1,pending:!1,children}:{...n,children:visit(n.children)});return{...model,sections:model.sections.map(s=>({...s,nodes:visit(s.nodes)}))}}function findNode(nodes,id){for(let n of nodes){if(n.id===id)return n;if(n.kind==="submenu"){let hit=findNode(n.children,id);if(hit)return hit}}return null}var INLINE_SECTION_MAX=3;function sectionLabel(section){return section.label??null}function isLeaf(node){return node.kind==="item"||node.kind==="checkbox"||node.kind==="link"}function collect(nodes,out){for(let n of nodes)isLeaf(n)?out.push(n.id):n.kind==="submenu"&&(n.lazy?out.push(n.id):collect(n.children,out))}function leafIds(arranged){let out=[];if(Array.isArray(arranged))for(let s of arranged)collect(s.nodes,out);else if("leaves"in arranged)for(let l of arranged.leaves)out.push(l.node.id);else if("primary"in arranged&&"overflow"in arranged){collect(arranged.primary,out);for(let s of arranged.overflow)collect(s.nodes,out)}else if("root"in arranged)collect(arranged.strip,out),collect(arranged.root,out);else if("more"in arranged){collect(arranged.strip,out);for(let s of arranged.more)collect(s.nodes,out)}else if("filterable"in arranged){collect(arranged.strip,out);for(let s of arranged.sections)collect(s.nodes,out)}else for(let s of arranged.sections)collect(s.nodes,out);return[...new Set(out)].sort()}function fold(section){return{kind:"submenu",id:`${section.id}:fold`,label:section.label,...section.icon!==void 0?{icon:section.icon}:{},children:section.nodes}}function rowCount(nodes){return nodes.filter(n=>n.kind!=="separator"&&n.kind!=="label").length}function stripOf(model){let clipboard=model.sections.find(s=>s.group==="clipboard"&&!s.primary);return clipboard?clipboard.nodes.filter(isLeaf):[]}function clipboardRest(model){let clipboard=model.sections.find(s=>s.group==="clipboard"&&!s.primary);return clipboard?clipboard.nodes.filter(n=>!isLeaf(n)&&n.kind==="submenu"):[]}function placementOf(node){return"placement"in node&&node.placement?node.placement:"overflow"}function classic(model){return{strip:[],sections:model.sections,filterable:!1}}function tiered(model,filterable){let strip=stripOf(model),sections=[],rest=clipboardRest(model);for(let s of model.sections){if(s.group==="clipboard"&&!s.primary){rest.length&§ions.push({...s,nodes:rest});continue}if(s.primary){sections.push(s);continue}if(!s.label||s.group==="surface-info"||rowCount(s.nodes)<=3){sections.push(s);continue}sections.push({id:s.id,group:s.group,nodes:[fold({...s,label:s.label})]})}return{strip,sections,filterable}}function context(model,arrangement){return arrangement==="classic"?classic(model):tiered(model,arrangement==="command")}function foldUnderSubject(arranged,subject){let label=subject.trim(),primary=arranged.sections.filter(s=>s.primary),rest=arranged.sections.filter(s=>!s.primary);if(!label||primary.length===0||rest.length===0&&arranged.strip.length===0)return arranged;let children=[];arranged.strip.length&&children.push(...arranged.strip);for(let s of rest)rowCount(s.nodes)!==0&&(children.length&&children.push({kind:"separator",id:`${s.id}:subject-sep`}),s.label&&children.push({kind:"label",id:`${s.id}:subject-label`,label:s.label}),children.push(...s.nodes));return rowCount(children)===0?{...arranged,strip:[]}:{strip:[],filterable:!1,sections:[...primary,{id:"subject",group:rest[0]?.group??primary[0].group,nodes:[{kind:"submenu",id:"subject:fold",label,children}]}]}}function foldNamed(sections){return sections.map(s=>s.label&&!s.primary&&rowCount(s.nodes)>3?{id:s.id,group:s.group,nodes:[fold({...s,label:s.label})]}:s)}function overflow(model){return foldNamed(model.sections)}function bar(model,_options={}){let primary=[],overflowSections=[];for(let s of model.sections){let rest=[];for(let n of s.nodes){if(n.kind==="separator"||n.kind==="label"){rest.push(n);continue}let p=placementOf(n);(p==="primary"||p==="both")&&primary.push(n),(p==="overflow"||p==="both")&&rest.push(n)}rowCount(rest)>0&&overflowSections.push({...s,nodes:rest})}return{primary,overflow:foldNamed(overflowSections)}}function selectionToolbar(model){let strip=[],more=[];for(let s of model.sections){let rest=[];for(let n of s.nodes)s.group==="clipboard"&&!s.primary&&isLeaf(n)||placementOf(n)!=="overflow"?strip.push(n):rest.push(n);rowCount(rest)>0&&more.push({...s,nodes:rest})}return{strip,more}}function sheet(model){let strip=stripOf(model),root=[],rest=clipboardRest(model);for(let s of model.sections){let nodes=s.group==="clipboard"&&!s.primary?rest:s.nodes;rowCount(nodes)!==0&&(root.length&&root.push({kind:"separator",id:`${s.id}:sep`}),s.primary||!s.label||rowCount(nodes)<=3?(s.label&&root.push({kind:"label",id:`${s.id}:label`,label:s.label}),root.push(...nodes)):root.push(fold({...s,label:s.label,nodes})))}return{strip,root}}function palette(model){let leaves=[],walk=(nodes,trail)=>{for(let n of nodes)isLeaf(n)?leaves.push({node:n,breadcrumb:trail}):n.kind==="submenu"&&(n.lazy?leaves.push({node:n,breadcrumb:trail}):walk(n.children,[...trail,n.label]))};for(let s of model.sections)walk(s.nodes,s.label?[s.label]:[]);return{leaves}}function filterPalette(arranged,query){let words=query.toLowerCase().split(/\s+/).filter(Boolean);return words.length?{leaves:arranged.leaves.filter(l=>{let hay=`${l.node.label} ${l.breadcrumb.join(" ")}`.toLowerCase();return words.every(w=>hay.includes(w))})}:arranged}var layouts={bar,overflow,context,selectionToolbar,sheet,palette};var PLATFORM_MENU_LAYOUT={context:"command",density:"comfortable"},MENU_LAYOUT_SETTING="alchemy.menu.layout",ARRANGEMENTS=new Set(["classic","tiered","command"]),DENSITIES=new Set(["comfortable","compact"]);async function resolveMenuLayout(ports,organizationId){let org=ports.knobs?.layout(organizationId)??null,base={...PLATFORM_MENU_LAYOUT,...org??{}},own=null;try{own=await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)??null}catch{own=null}if(own&&typeof own=="object"&&!Array.isArray(own)){let o=own;return{context:typeof o.context=="string"&&ARRANGEMENTS.has(o.context)?o.context:base.context,density:typeof o.density=="string"&&DENSITIES.has(o.density)?o.density:base.density}}return base}
|
|
2
2
|
//# sourceMappingURL=menu.cjs.map
|