@ai-matrx/alchemy 0.8.32 → 0.8.35

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/dist/menu.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/menu.ts","../src/menu/headings.ts","../src/menu/model.ts","../src/menu/layouts.ts","../src/menu/knobs.ts"],"sourcesContent":["/**\n * `@ai-matrx/alchemy/menu` — the one menu model and its pure layouts\n * (Matrx Alchemy CONTRACT §2.5). Framework-neutral: icons are keys, no React.\n */\nexport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n Density,\n LayoutName,\n MenuCheckboxNode,\n MenuGroup,\n MenuItemNode,\n MenuLabelNode,\n MenuLeafNode,\n MenuLinkNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSeparatorNode,\n MenuSubmenuNode,\n} from \"./menu/types\";\nexport {\n CATEGORY_GROUP,\n GROUP_ORDER,\n actionToNode,\n buildMenuModel,\n findNode,\n withExpandedChildren,\n type BuildMenuModelOptions,\n} from \"./menu/model\";\nexport {\n INLINE_SECTION_MAX,\n bar,\n context,\n foldUnderSubject,\n filterPalette,\n layouts,\n leafIds,\n overflow,\n palette,\n sectionLabel,\n selectionToolbar,\n sheet,\n} from \"./menu/layouts\";\nexport { MENU_LAYOUT_SETTING, PLATFORM_MENU_LAYOUT, resolveMenuLayout } from \"./menu/knobs\";\nexport {\n APPROVED_HEADINGS,\n UnapprovedHeadingError,\n defaultHeadingPolicy,\n isApprovedHeading,\n isContainerWord,\n menuHeadings,\n unapprovedHeadings,\n type HeadingKind,\n type HeadingPolicy,\n} from \"./menu/headings\";\n","/**\n * THE ONE APPROVED-HEADING LIST (ALC-15, chair rulings 7 and D1; the Lossless\n * Law and the vocabulary law). A heading a person sees above or around rows\n * must be a name the classic menus used before agents started coining names.\n *\n * 🚨 ADDING A HEADING TO THIS LIST IS ARMAN'S CALL — never an agent's.\n *\n * Two kinds of section heading exist:\n * - a GROUP heading (\"Copy as\", \"History\"): must be in APPROVED_HEADINGS;\n * - a TARGET label, the thing the person clicked, supplied by the surface at\n * run time (\"Cell · Visit date\", \"Row · Widget A\", \"This finding\", the\n * surface's own label): allowed as declared (`section.kind: \"target\"`, or\n * the \"<Target> · <name>\" shape), except a generic container word.\n * A generic container word (\"More…\", \"Other\", \"Actions\") is never a heading.\n * An unapproved heading renders its rows INLINE in production; in development\n * and tests `buildMenuModel` throws with the remedy.\n */\nimport type { MenuModel, MenuNode } from \"./types\";\n\n/** Names with their provenance (the commit where each classic menu used them). */\nexport const APPROVED_HEADINGS: ReadonlySet<string> = new Set([\n \"Save\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Copy as\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Export\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Edit\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Creator tools\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Admin\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"App\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Compare\", // rich-document menuStructure, 2e3a8a9fdf (2026-06-11)\n \"History\", // context-menu-v3 tiered layout — the one grouping Arman approved, 2026-08-22\n]);\n\n// Ruling (chair, 2026-09-26, per R26): \"Conversation\" is a TARGET label (`kind: \"target\"`) — it names the thing its rows act on (the whole conversation), like \"Cell · …\" and \"Table · …\"; it is not a group heading and is not added above.\n\nconst CONTAINER_WORD =\n /^(more|more…|more\\.\\.\\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i;\n\n/** \"Cell · Visit date\", \"Row · Widget A\", \"Column · Price\", \"Table · Route sheet\". */\nconst TARGET_SHAPE = /^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;\n\nexport function isContainerWord(label: string): boolean {\n return CONTAINER_WORD.test(label.trim());\n}\n\nexport type HeadingKind = \"group\" | \"target\";\n\n/** Is this heading allowed? */\nexport function isApprovedHeading(label: string, kind: HeadingKind = \"group\", approved: ReadonlySet<string> = APPROVED_HEADINGS): boolean {\n const l = label.trim();\n if (!l || isContainerWord(l)) return false;\n if (approved.has(l)) return true;\n return kind === \"target\" || TARGET_SHAPE.test(l);\n}\n\nexport type HeadingPolicy = \"throw\" | \"inline\";\n\n/** Development and tests throw; production shows the rows inline. */\nexport function defaultHeadingPolicy(): HeadingPolicy {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.NODE_ENV;\n return env === \"production\" ? \"inline\" : \"throw\";\n}\n\nexport class UnapprovedHeadingError extends Error {\n readonly heading: string;\n constructor(heading: string, where: string) {\n super(\n `Menu heading \"${heading}\" (${where}) is not an approved heading. Remedy: show these rows inline — declare no section ` +\n `(or kind \"target\" for the clicked thing's own label) — or ask Arman to add the name to APPROVED_HEADINGS in ` +\n `@ai-matrx/alchemy/menu. A generic container word (\"More…\", \"Other\") is never approved.`,\n );\n this.name = \"UnapprovedHeadingError\";\n this.heading = heading;\n }\n}\n\n/** Every heading in a model: section labels, folds, and submenu rows at any depth. */\nexport function menuHeadings(model: Pick<MenuModel, \"sections\">): string[] {\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind === \"submenu\") {\n out.push(n.label);\n walk(n.children);\n }\n }\n };\n for (const s of model.sections) {\n if (s.label) out.push(s.label);\n walk(s.nodes);\n }\n return [...new Set(out)];\n}\n\n/** Section headings not in the approved set (container words are never approved). */\nexport function unapprovedHeadings(model: Pick<MenuModel, \"sections\">, approved: ReadonlySet<string> = APPROVED_HEADINGS): string[] {\n const out = new Set<string>();\n for (const s of model.sections) {\n if (s.label && !isApprovedHeading(s.label, s.primary || s.target ? \"target\" : \"group\", approved)) out.add(s.label);\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) if (n.kind === \"submenu\") {\n if (isContainerWord(n.label)) out.add(n.label);\n walk(n.children);\n }\n };\n walk(s.nodes);\n }\n return [...out];\n}\n","/**\n * buildMenuModel — WHAT exists at a click target, as one framework-neutral\n * tree (CONTRACT §2.5). Categories GROUP, never gate: eligibility already ran\n * in the registry; this only arranges the resolved set into sections.\n */\nimport type { Action, ActionCategory, ClickTarget, ResolvedAction } from \"../actions/types\";\nimport type { I18nPort } from \"../ports\";\nimport type { MenuGroup, MenuModel, MenuNode, MenuSection, MenuSubmenuNode } from \"./types\";\nimport { UnapprovedHeadingError, defaultHeadingPolicy, isApprovedHeading, isContainerWord, type HeadingPolicy } from \"./headings\";\n\n/** THE category → menu group table (CONTRACT §2.5). */\nexport const CATEGORY_GROUP: Readonly<Record<ActionCategory, MenuGroup>> = {\n clipboard: \"clipboard\",\n copy: \"transfer\",\n export: \"transfer\",\n save: \"document\",\n share: \"document\",\n edit: \"editable\",\n history: \"history\",\n ai: \"ai\",\n ask: \"ai\",\n listen: \"tools\",\n study: \"tools\",\n app: \"tools\",\n feedback: \"quick\",\n creator: \"admin\",\n admin: \"admin\",\n \"surface-info\": \"surface-info\",\n};\n\n/** Classic order of the groups. `surface-info` is engine-built and always last. */\nexport const GROUP_ORDER: readonly MenuGroup[] = [\n \"clipboard\",\n \"editable\",\n \"transfer\",\n \"document\",\n \"history\",\n \"ai\",\n \"tools\",\n \"quick\",\n \"surface\",\n \"admin\",\n \"surface-info\",\n];\n\nconst CATEGORY_ORDER: readonly ActionCategory[] = [\n \"clipboard\",\n \"edit\",\n \"copy\",\n \"export\",\n \"save\",\n \"share\",\n \"history\",\n \"ask\",\n \"ai\",\n \"listen\",\n \"study\",\n \"app\",\n \"feedback\",\n \"creator\",\n \"admin\",\n \"surface-info\",\n];\n\nexport interface BuildMenuModelOptions {\n /** What an unapproved heading does: \"throw\" (development/tests) or \"inline\" (production). */\n headingPolicy?: HeadingPolicy;\n i18n?: I18nPort;\n /** Content the menu acts on when nothing is selected. */\n content?: string | null;\n /**\n * The name of the field the content is (a text box's label, \"Body\"). The\n * header then names the field — never \"Content: {{reply.body}}\", its raw text.\n */\n contentLabel?: string | null;\n}\n\nfunction translate(i18n: I18nPort | undefined, key: string, fallback: string): string {\n if (!i18n) return fallback;\n try {\n return i18n.t(key, fallback);\n } catch {\n return fallback;\n }\n}\n\n/** One resolved action → one model node (item, checkbox, link or lazy submenu). */\nexport function actionToNode(\n resolved: ResolvedAction,\n target: ClickTarget,\n options: BuildMenuModelOptions = {},\n): MenuNode {\n const { action, eligibility } = resolved;\n const rawLabel = typeof action.label === \"function\" ? action.label(target) : action.label;\n const label = translate(options.i18n, action.labelKey ?? action.id, rawLabel);\n const base = {\n id: action.id,\n actionId: action.id,\n ...(action.icon !== undefined ? { icon: action.icon } : {}),\n ...(action.placement !== undefined ? { placement: action.placement } : {}),\n };\n if (action.expand) {\n const node: MenuSubmenuNode = {\n ...base,\n kind: \"submenu\",\n label,\n lazy: true,\n loading: false,\n ...(action.pending?.(target) ? { pending: true } : {}),\n children: [],\n ...(action.emptyLabel !== undefined ? { emptyLabel: action.emptyLabel } : {}),\n };\n return node;\n }\n if (action.href !== undefined) {\n const href = typeof action.href === \"function\" ? action.href(target) : action.href;\n return { ...base, kind: \"link\", label, href };\n }\n if (action.toggle) {\n return {\n ...base,\n kind: \"checkbox\",\n label,\n checked: action.pressed?.(target) ?? false,\n ...(action.description !== undefined ? { description: action.description } : {}),\n };\n }\n const state = action.stateIcon?.(target) ?? null;\n const pressed = action.pressed?.(target);\n const hint = action.hint !== undefined ? translate(options.i18n, action.hintKey ?? `${action.id}.hint`, action.hint) : undefined;\n return {\n ...base,\n kind: \"item\",\n label,\n ...(action.iconTone !== undefined ? { iconTone: action.iconTone } : {}),\n ...(state ? { stateIcon: state } : {}),\n ...(action.description !== undefined ? { description: action.description } : {}),\n ...(hint !== undefined ? { hint } : {}),\n ...(pressed !== undefined ? { pressed } : {}),\n ...(action.destructive ? { destructive: true } : {}),\n ...(action.preserveSelection ? { preserveSelection: true } : {}),\n ...(eligibility.status === \"unavailable-verb\" || eligibility.status === \"unavailable\"\n ? { unavailable: { sentence: eligibility.sentence } }\n : {}),\n };\n}\n\nexport function buildMenuModel(\n target: ClickTarget,\n resolved: readonly ResolvedAction[],\n options: BuildMenuModelOptions = {},\n): MenuModel {\n // THE ONE HEADING CHECK, for every host: an unapproved heading's rows go\n // inline (production) or the build throws with the remedy (dev/tests).\n const policy = options.headingPolicy ?? defaultHeadingPolicy();\n resolved = resolved.map((r) => {\n const sec = r.action.section;\n if (r.action.expand && isContainerWord(typeof r.action.label === \"string\" ? r.action.label : \"\") && policy === \"throw\") {\n throw new UnapprovedHeadingError(r.action.label as string, `submenu of ${r.action.id}`);\n }\n if (!sec) return r;\n const kind = sec.kind ?? (sec.primary ? \"target\" : \"group\");\n if (isApprovedHeading(sec.label, kind)) return r;\n if (policy === \"throw\") throw new UnapprovedHeadingError(sec.label, `section of ${r.action.id}`);\n const { section: _dropped, ...rest } = r.action;\n return { ...r, action: rest };\n });\n const ordered = resolved\n .map((r, index) => ({ r, index }))\n .sort((a, b) => {\n const ca = CATEGORY_ORDER.indexOf(a.r.action.category);\n const cb = CATEGORY_ORDER.indexOf(b.r.action.category);\n if (ca !== cb) return ca - cb;\n const oa = a.r.action.order ?? 0;\n const ob = b.r.action.order ?? 0;\n if (oa !== ob) return oa - ob;\n return a.index - b.index;\n })\n .map((x) => x.r);\n\n const sections = new Map<string, MenuSection>();\n const roles: Record<string, string> = {};\n // An explicit section sits in the group of its first-registered action and\n // keeps its own row order (order, then registration) across categories.\n const explicit = new Map<string, { group: MenuGroup; rows: { r: ResolvedAction; index: number }[] }>();\n resolved.forEach((r, index) => {\n const sec = r.action.section;\n if (!sec) return;\n const entry = explicit.get(sec.id) ?? { group: CATEGORY_GROUP[r.action.category], rows: [] };\n entry.rows.push({ r, index });\n explicit.set(sec.id, entry);\n });\n const placed = new Set<string>();\n for (const r of ordered) {\n const sec = r.action.section;\n if (sec && placed.has(sec.id)) continue;\n const group = sec ? (explicit.get(sec.id)?.group ?? CATEGORY_GROUP[r.action.category]) : CATEGORY_GROUP[r.action.category];\n const sectionId = sec ? `section:${sec.id}` : group;\n let section = sections.get(sectionId);\n if (!section) {\n section = {\n id: sectionId,\n group,\n nodes: [],\n ...(sec ? { label: translate(options.i18n, sec.labelKey ?? `section.${sec.id}`, sec.label) } : {}),\n ...(sec?.icon !== undefined ? { icon: sec.icon } : {}),\n ...(sec?.primary ? { primary: true } : {}),\n ...(sec && (sec.kind ?? (sec.primary ? \"target\" : \"group\")) === \"target\" ? { target: true } : {}),\n };\n sections.set(sectionId, section);\n }\n const rows = sec\n ? (explicit.get(sec.id)?.rows ?? [])\n .sort((x, y) => (x.r.action.order ?? 0) - (y.r.action.order ?? 0) || x.index - y.index)\n .map((x) => x.r)\n : [r];\n if (sec) placed.add(sec.id);\n for (const row of rows) {\n if (row.action.startsGroup && section.nodes.length > 0) {\n section.nodes.push({ kind: \"separator\", id: `${row.action.id}:group` });\n }\n section.nodes.push(actionToNode(row, target, options));\n if (row.action.verb) roles[row.action.verb] = row.action.id;\n }\n }\n\n const list = [...sections.values()].sort((a, b) => {\n if (Boolean(a.primary) !== Boolean(b.primary)) return a.primary ? -1 : 1;\n return GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group);\n });\n\n const selected = target.selection?.text?.trim();\n const content = options.content?.trim();\n const fieldName = options.contentLabel?.trim();\n const header = selected\n ? { label: \"Selected\", text: target.selection?.text ?? \"\" }\n : fieldName\n ? { label: fieldName, text: options.content ?? \"\" }\n : content\n ? { label: \"Content\", text: options.content ?? \"\" }\n : null;\n\n return { header, sections: list, roles };\n}\n\n/** Fill a lazy submenu's rows once they load (pure: returns a new model). */\n/**\n * AN EXPANDED SUBMENU KEEPS ITS GROUPS (DATA-V2-BASICS-2, 2026-09-28). A provider's rows arrive flat,\n * so a submenu drew \"Highlight row\" and \"Delete row…\" as neighbours with no divider, while the same\n * rows in a top-level menu sit in their sections. A divider now goes wherever the rows' `section`\n * changes — a row with no section and a row with one are different groups too.\n */\nexport function withGroupDividers(children: readonly Action[], nodes: readonly MenuNode[]): MenuNode[] {\n const out: MenuNode[] = [];\n children.forEach((child, i) => {\n const node = nodes[i];\n if (!node) return;\n if (i > 0 && (child.startsGroup || (children[i - 1]!.section?.id ?? null) !== (child.section?.id ?? null))) {\n out.push({ kind: \"separator\", id: `${child.id}:group` });\n }\n out.push(node);\n });\n return out;\n}\n\nexport function withExpandedChildren(model: MenuModel, nodeId: string, children: MenuNode[] | \"loading\"): MenuModel {\n const visit = (nodes: MenuNode[]): MenuNode[] =>\n nodes.map((n) => {\n if (n.kind !== \"submenu\") return n;\n if (n.id === nodeId) {\n return children === \"loading\" ? { ...n, loading: true } : { ...n, lazy: false, loading: false, pending: false, children };\n }\n return { ...n, children: visit(n.children) };\n });\n return { ...model, sections: model.sections.map((s) => ({ ...s, nodes: visit(s.nodes) })) };\n}\n\n/** Find a node anywhere in a tree. */\nexport function findNode(nodes: readonly MenuNode[], id: string): MenuNode | null {\n for (const n of nodes) {\n if (n.id === id) return n;\n if (n.kind === \"submenu\") {\n const hit = findNode(n.children, id);\n if (hit) return hit;\n }\n }\n return null;\n}\n","/**\n * Layouts — pure arrangements of one MenuModel (CONTRACT §2.5).\n *\n * Every layout is a function over the SAME model: it decides which nodes sit\n * at the top level, which fold under a named submenu and which go into a\n * strip. None adds, drops or renames a leaf — `leafIds(arranged)` equals\n * `leafIds(model)` for every layout (THE LOSSLESS LAW, proven by test).\n *\n * The clipboard group IS the universal verb strip: in the strip layouts it is\n * drawn icon-only; elsewhere its rows are drawn in place. Unavailable verbs\n * (the only greyed nodes, R1 c) appear in every layout identically.\n */\nimport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n MenuLeafNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSubmenuNode,\n} from \"./types\";\n\n/** A section with more rows than this folds under its label in tiered/command/sheet. */\nexport const INLINE_SECTION_MAX = 3;\n\n/**\n * A section's heading is ONLY the name its actions declare (an approved\n * heading the classic menu already uses: \"Save as\", \"Copy as\", \"History\").\n * A group with no declared name has no heading and is never folded — its rows\n * show inline. The package never coins a heading (chair ruling, ALC-15).\n */\nexport function sectionLabel(section: MenuSection): string | null {\n return section.label ?? null;\n}\n\nfunction isLeaf(node: MenuNode): node is MenuLeafNode {\n return node.kind === \"item\" || node.kind === \"checkbox\" || node.kind === \"link\";\n}\n\n/** Every actionable id in a node list, depth-first. A lazy submenu counts as one leaf. */\nfunction collect(nodes: readonly MenuNode[], out: string[]): void {\n for (const n of nodes) {\n if (isLeaf(n)) out.push(n.id);\n else if (n.kind === \"submenu\") {\n if (n.lazy) out.push(n.id);\n else collect(n.children, out);\n }\n }\n}\n\ntype Arranged =\n | MenuModel\n | ArrangedBar\n | ArrangedContext\n | ArrangedSheet\n | ArrangedPalette\n | ArrangedSelectionToolbar\n | MenuSection[];\n\n/** Proof obligation for every layout: its leaf set equals the model's (sorted, de-duplicated). */\nexport function leafIds(arranged: Arranged): readonly string[] {\n const out: string[] = [];\n if (Array.isArray(arranged)) {\n for (const s of arranged) collect(s.nodes, out);\n } else if (\"leaves\" in arranged) {\n for (const l of arranged.leaves) out.push(l.node.id);\n } else if (\"primary\" in arranged && \"overflow\" in arranged) {\n collect(arranged.primary, out);\n for (const s of arranged.overflow) collect(s.nodes, out);\n } else if (\"root\" in arranged) {\n collect(arranged.strip, out);\n collect(arranged.root, out);\n } else if (\"more\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.more) collect(s.nodes, out);\n } else if (\"filterable\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.sections) collect(s.nodes, out);\n } else {\n for (const s of arranged.sections) collect(s.nodes, out);\n }\n return [...new Set(out)].sort();\n}\n\n/** Only a NAMED section folds; callers check `section.label` first. */\nfunction fold(section: MenuSection & { label: string }): MenuSubmenuNode {\n return {\n kind: \"submenu\",\n id: `${section.id}:fold`,\n label: section.label,\n ...(section.icon !== undefined ? { icon: section.icon } : {}),\n children: section.nodes,\n };\n}\n\nfunction rowCount(nodes: readonly MenuNode[]): number {\n return nodes.filter((n) => n.kind !== \"separator\" && n.kind !== \"label\").length;\n}\n\nfunction stripOf(model: MenuModel): MenuLeafNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter(isLeaf) : [];\n}\n\nfunction clipboardRest(model: MenuModel): MenuNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter((n) => !isLeaf(n) && n.kind === \"submenu\") : [];\n}\n\nfunction placementOf(node: MenuNode): \"primary\" | \"overflow\" | \"both\" {\n return \"placement\" in node && node.placement ? node.placement : \"overflow\";\n}\n\n/** The historical flat column: every section top-level. */\nfunction classic(model: MenuModel): ArrangedContext {\n return { strip: [], sections: model.sections, filterable: false };\n}\n\n/**\n * Tiered / command: the universal verbs as an icon strip; every other row by\n * its own name; the clicked target's (primary) section first, inline, heading\n * kept; a long non-primary section folds under ITS OWN label (never a coined\n * heading); History is the one approved grouping and already a group.\n */\nfunction tiered(model: MenuModel, filterable: boolean): ArrangedContext {\n const strip = stripOf(model);\n const sections: MenuSection[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n if (s.group === \"clipboard\" && !s.primary) {\n if (rest.length) sections.push({ ...s, nodes: rest });\n continue;\n }\n if (s.primary) {\n sections.push(s);\n continue;\n }\n if (!s.label || s.group === \"surface-info\" || rowCount(s.nodes) <= INLINE_SECTION_MAX) {\n // A section keeps its declared heading in EVERY layout (the Lossless\n // Law covers headings: ⋯, the bar's ⋯ and the phone sheet show it too).\n sections.push(s);\n continue;\n }\n sections.push({ id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] });\n }\n return { strip, sections, filterable };\n}\n\nexport function context(model: MenuModel, arrangement: ContextArrangement): ArrangedContext {\n if (arrangement === \"classic\") return classic(model);\n return tiered(model, arrangement === \"command\");\n}\n\n/**\n * FOLD UNDER THE SUBJECT — a control that belongs to a CONTAINER of the thing\n * (a tab's \"…\" over the note it shows) draws the container's own rows (the\n * primary sections) on top, and EVERY other row — the verb strip included —\n * under ONE submenu named for the subject (\"Note ▸\"). The label is the\n * subject's name the host supplies (the menu header's label), never a coined\n * group heading. Lossless: rows move, none is dropped or renamed. With no\n * primary section there is nothing to put first, so the arrangement is left\n * as it was. (page-pass /notes, 2026-09-28, Arman's ruling for the tab \"…\".)\n */\nexport function foldUnderSubject(arranged: ArrangedContext, subject: string): ArrangedContext {\n const label = subject.trim();\n const primary = arranged.sections.filter((s) => s.primary);\n const rest = arranged.sections.filter((s) => !s.primary);\n if (!label || primary.length === 0 || (rest.length === 0 && arranged.strip.length === 0)) return arranged;\n const children: MenuNode[] = [];\n if (arranged.strip.length) children.push(...arranged.strip);\n for (const s of rest) {\n if (rowCount(s.nodes) === 0) continue;\n if (children.length) children.push({ kind: \"separator\", id: `${s.id}:subject-sep` });\n if (s.label) children.push({ kind: \"label\", id: `${s.id}:subject-label`, label: s.label });\n children.push(...s.nodes);\n }\n if (rowCount(children) === 0) return { ...arranged, strip: [] };\n return {\n strip: [],\n filterable: false,\n sections: [\n ...primary,\n { id: \"subject\", group: rest[0]?.group ?? primary[0]!.group, nodes: [{ kind: \"submenu\", id: \"subject:fold\", label, children }] },\n ],\n };\n}\n\n/**\n * A long NAMED section folds under its own name (\"Save as ▸\", \"Copy as ▸\");\n * primary sections and unnamed groups stay inline. Folding moves rows, never\n * renames or drops them, and never under a coined heading.\n */\nfunction foldNamed(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map((s) =>\n s.label && !s.primary && rowCount(s.nodes) > INLINE_SECTION_MAX ? { id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] } : s,\n );\n}\n\n/** ⋯ overflow: every section in classic order; long named sections fold under their own name. */\nexport function overflow(model: MenuModel): MenuSection[] {\n return foldNamed(model.sections);\n}\n\n/**\n * The action bar: primary buttons inline, EVERYTHING else under ⋯. A \"both\"\n * action is mirrored in both halves (the leaf set is still the model's).\n * `mini` changes presentation only (icon-only primaries), never membership.\n */\nexport function bar(model: MenuModel, _options: { mini?: boolean } = {}): ArrangedBar {\n const primary: MenuNode[] = [];\n const overflowSections: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n if (n.kind === \"separator\" || n.kind === \"label\") {\n rest.push(n);\n continue;\n }\n const p = placementOf(n);\n if (p === \"primary\" || p === \"both\") primary.push(n);\n if (p === \"overflow\" || p === \"both\") rest.push(n);\n }\n if (rowCount(rest) > 0) overflowSections.push({ ...s, nodes: rest });\n }\n return { primary, overflow: foldNamed(overflowSections) };\n}\n\n/** Selection toolbar: the verbs plus primary actions as a strip; the rest under More. */\nexport function selectionToolbar(model: MenuModel): ArrangedSelectionToolbar {\n const strip: MenuNode[] = [];\n const more: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n const inStrip = (s.group === \"clipboard\" && !s.primary && isLeaf(n)) || placementOf(n) !== \"overflow\";\n if (inStrip) strip.push(n);\n else rest.push(n);\n }\n if (rowCount(rest) > 0) more.push({ ...s, nodes: rest });\n }\n return { strip, more };\n}\n\n/**\n * Mobile bottom sheet: the verb strip on top; primary and short sections\n * inline; longer sections become one drill-down row named by the section.\n * Consumes the model directly, so it can never drift from desktop.\n */\nexport function sheet(model: MenuModel): ArrangedSheet {\n const strip = stripOf(model);\n const root: MenuNode[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n const nodes = s.group === \"clipboard\" && !s.primary ? rest : s.nodes;\n if (rowCount(nodes) === 0) continue;\n if (root.length) root.push({ kind: \"separator\", id: `${s.id}:sep` });\n if (s.primary || !s.label || rowCount(nodes) <= INLINE_SECTION_MAX) {\n if (s.label) root.push({ kind: \"label\", id: `${s.id}:label`, label: s.label });\n root.push(...nodes);\n } else {\n root.push(fold({ ...s, label: s.label, nodes }));\n }\n }\n return { strip, root };\n}\n\n/** Command palette: every leaf flattened with its breadcrumb (Linear). */\nexport function palette(model: MenuModel): ArrangedPalette {\n const leaves: ArrangedPalette[\"leaves\"] = [];\n const walk = (nodes: readonly MenuNode[], trail: string[]) => {\n for (const n of nodes) {\n if (isLeaf(n)) leaves.push({ node: n, breadcrumb: trail });\n else if (n.kind === \"submenu\") {\n if (n.lazy) leaves.push({ node: n, breadcrumb: trail });\n else walk(n.children, [...trail, n.label]);\n }\n }\n };\n for (const s of model.sections) walk(s.nodes, s.label ? [s.label] : []);\n return { leaves };\n}\n\n/** Filter the palette by a query (every word must match label or breadcrumb). */\nexport function filterPalette(arranged: ArrangedPalette, query: string): ArrangedPalette {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return arranged;\n return {\n leaves: arranged.leaves.filter((l) => {\n const hay = `${l.node.label} ${l.breadcrumb.join(\" \")}`.toLowerCase();\n return words.every((w) => hay.includes(w));\n }),\n };\n}\n\nexport const layouts = { bar, overflow, context, selectionToolbar, sheet, palette };\n","/**\n * N9: layout and density are a PERSON's choice layered over the organization\n * default: persistence (\"alchemy.menu.layout\") → knobs.layout(org) → platform\n * default (command / comfortable, Arman 2026-08-22).\n */\nimport type { AlchemyHostPorts, ContextArrangementKnob, DensityKnob } from \"../ports\";\n\nexport const PLATFORM_MENU_LAYOUT: { context: ContextArrangementKnob; density: DensityKnob } = {\n context: \"command\",\n density: \"comfortable\",\n};\n\nexport const MENU_LAYOUT_SETTING = \"alchemy.menu.layout\";\n\nconst ARRANGEMENTS = new Set<string>([\"classic\", \"tiered\", \"command\"]);\nconst DENSITIES = new Set<string>([\"comfortable\", \"compact\"]);\n\nexport async function resolveMenuLayout(\n ports: Pick<AlchemyHostPorts, \"persistence\" | \"knobs\">,\n organizationId: string | null,\n): Promise<{ context: ContextArrangementKnob; density: DensityKnob }> {\n const org = ports.knobs?.layout(organizationId) ?? null;\n const base = { ...PLATFORM_MENU_LAYOUT, ...(org ?? {}) };\n let own: unknown = null;\n try {\n own = (await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)) ?? null;\n } catch {\n own = null;\n }\n if (own && typeof own === \"object\" && !Array.isArray(own)) {\n const o = own as { context?: unknown; density?: unknown };\n return {\n context: typeof o.context === \"string\" && ARRANGEMENTS.has(o.context) ? (o.context as ContextArrangementKnob) : base.context,\n density: typeof o.density === \"string\" && DENSITIES.has(o.density) ? (o.density as DensityKnob) : base.density,\n };\n }\n return base;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/menu.ts","../src/menu/headings.ts","../src/menu/model.ts","../src/menu/layouts.ts","../src/menu/knobs.ts"],"sourcesContent":["/**\n * `@ai-matrx/alchemy/menu` — the one menu model and its pure layouts\n * (Matrx Alchemy CONTRACT §2.5). Framework-neutral: icons are keys, no React.\n */\nexport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n Density,\n LayoutName,\n MenuCheckboxNode,\n MenuGroup,\n MenuItemNode,\n MenuLabelNode,\n MenuLeafNode,\n MenuLinkNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSeparatorNode,\n MenuSubmenuNode,\n} from \"./menu/types\";\nexport {\n CATEGORY_GROUP,\n GROUP_ORDER,\n actionToNode,\n buildMenuModel,\n findNode,\n withExpandedChildren,\n type BuildMenuModelOptions,\n} from \"./menu/model\";\nexport {\n INLINE_SECTION_MAX,\n bar,\n context,\n foldUnderSubject,\n filterPalette,\n layouts,\n leafIds,\n overflow,\n palette,\n sectionLabel,\n selectionToolbar,\n sheet,\n} from \"./menu/layouts\";\nexport { MENU_LAYOUT_SETTING, PLATFORM_MENU_LAYOUT, resolveMenuLayout } from \"./menu/knobs\";\nexport {\n APPROVED_HEADINGS,\n UnapprovedHeadingError,\n defaultHeadingPolicy,\n isApprovedHeading,\n isContainerWord,\n menuHeadings,\n unapprovedHeadings,\n type HeadingKind,\n type HeadingPolicy,\n} from \"./menu/headings\";\n","/**\n * THE ONE APPROVED-HEADING LIST (ALC-15, chair rulings 7 and D1; the Lossless\n * Law and the vocabulary law). A heading a person sees above or around rows\n * must be a name the classic menus used before agents started coining names.\n *\n * 🚨 ADDING A HEADING TO THIS LIST IS ARMAN'S CALL — never an agent's.\n *\n * Two kinds of section heading exist:\n * - a GROUP heading (\"Copy as\", \"History\"): must be in APPROVED_HEADINGS;\n * - a TARGET label, the thing the person clicked, supplied by the surface at\n * run time (\"Cell · Visit date\", \"Row · Widget A\", \"This finding\", the\n * surface's own label): allowed as declared (`section.kind: \"target\"`, or\n * the \"<Target> · <name>\" shape), except a generic container word.\n * A generic container word (\"More…\", \"Other\", \"Actions\") is never a heading.\n * An unapproved heading renders its rows INLINE in production; in development\n * and tests `buildMenuModel` throws with the remedy.\n */\nimport type { MenuModel, MenuNode } from \"./types\";\n\n/** Names with their provenance (the commit where each classic menu used them). */\nexport const APPROVED_HEADINGS: ReadonlySet<string> = new Set([\n \"Save\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Copy as\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Export\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Edit\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Creator tools\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Admin\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"App\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Compare\", // rich-document menuStructure, 2e3a8a9fdf (2026-06-11)\n \"History\", // context-menu-v3 tiered layout — the one grouping Arman approved, 2026-08-22\n]);\n\n// Ruling (chair, 2026-09-26, per R26): \"Conversation\" is a TARGET label (`kind: \"target\"`) — it names the thing its rows act on (the whole conversation), like \"Cell · …\" and \"Table · …\"; it is not a group heading and is not added above.\n\nconst CONTAINER_WORD =\n /^(more|more…|more\\.\\.\\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i;\n\n/** \"Cell · Visit date\", \"Row · Widget A\", \"Column · Price\", \"Table · Route sheet\". */\nconst TARGET_SHAPE = /^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;\n\nexport function isContainerWord(label: string): boolean {\n return CONTAINER_WORD.test(label.trim());\n}\n\nexport type HeadingKind = \"group\" | \"target\";\n\n/** Is this heading allowed? */\nexport function isApprovedHeading(label: string, kind: HeadingKind = \"group\", approved: ReadonlySet<string> = APPROVED_HEADINGS): boolean {\n const l = label.trim();\n if (!l || isContainerWord(l)) return false;\n if (approved.has(l)) return true;\n return kind === \"target\" || TARGET_SHAPE.test(l);\n}\n\nexport type HeadingPolicy = \"throw\" | \"inline\";\n\n/** Development and tests throw; production shows the rows inline. */\nexport function defaultHeadingPolicy(): HeadingPolicy {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.NODE_ENV;\n return env === \"production\" ? \"inline\" : \"throw\";\n}\n\nexport class UnapprovedHeadingError extends Error {\n readonly heading: string;\n constructor(heading: string, where: string) {\n super(\n `Menu heading \"${heading}\" (${where}) is not an approved heading. Remedy: show these rows inline — declare no section ` +\n `(or kind \"target\" for the clicked thing's own label) — or ask Arman to add the name to APPROVED_HEADINGS in ` +\n `@ai-matrx/alchemy/menu. A generic container word (\"More…\", \"Other\") is never approved.`,\n );\n this.name = \"UnapprovedHeadingError\";\n this.heading = heading;\n }\n}\n\n/** Every heading in a model: section labels, folds, and submenu rows at any depth. */\nexport function menuHeadings(model: Pick<MenuModel, \"sections\">): string[] {\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind === \"submenu\") {\n out.push(n.label);\n walk(n.children);\n }\n }\n };\n for (const s of model.sections) {\n if (s.label) out.push(s.label);\n walk(s.nodes);\n }\n return [...new Set(out)];\n}\n\n/** Section headings not in the approved set (container words are never approved). */\nexport function unapprovedHeadings(model: Pick<MenuModel, \"sections\">, approved: ReadonlySet<string> = APPROVED_HEADINGS): string[] {\n const out = new Set<string>();\n for (const s of model.sections) {\n if (s.label && !isApprovedHeading(s.label, s.primary || s.target ? \"target\" : \"group\", approved)) out.add(s.label);\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) if (n.kind === \"submenu\") {\n if (isContainerWord(n.label)) out.add(n.label);\n walk(n.children);\n }\n };\n walk(s.nodes);\n }\n return [...out];\n}\n","/**\n * buildMenuModel — WHAT exists at a click target, as one framework-neutral\n * tree (CONTRACT §2.5). Categories GROUP, never gate: eligibility already ran\n * in the registry; this only arranges the resolved set into sections.\n */\nimport type { Action, ActionCategory, ClickTarget, ResolvedAction } from \"../actions/types\";\nimport type { I18nPort } from \"../ports\";\nimport type { MenuGroup, MenuModel, MenuNode, MenuSection, MenuSubmenuNode } from \"./types\";\nimport { UnapprovedHeadingError, defaultHeadingPolicy, isApprovedHeading, isContainerWord, type HeadingPolicy } from \"./headings\";\n\n/** THE category → menu group table (CONTRACT §2.5). */\nexport const CATEGORY_GROUP: Readonly<Record<ActionCategory, MenuGroup>> = {\n clipboard: \"clipboard\",\n copy: \"transfer\",\n export: \"transfer\",\n save: \"document\",\n share: \"document\",\n edit: \"editable\",\n history: \"history\",\n ai: \"ai\",\n ask: \"ai\",\n listen: \"tools\",\n study: \"tools\",\n app: \"tools\",\n feedback: \"quick\",\n creator: \"admin\",\n admin: \"admin\",\n \"surface-info\": \"surface-info\",\n};\n\n/** Classic order of the groups. `surface-info` is engine-built and always last. */\nexport const GROUP_ORDER: readonly MenuGroup[] = [\n \"clipboard\",\n \"editable\",\n \"transfer\",\n \"document\",\n \"history\",\n \"ai\",\n \"tools\",\n \"quick\",\n \"surface\",\n \"admin\",\n \"surface-info\",\n];\n\nconst CATEGORY_ORDER: readonly ActionCategory[] = [\n \"clipboard\",\n \"edit\",\n \"copy\",\n \"export\",\n \"save\",\n \"share\",\n \"history\",\n \"ask\",\n \"ai\",\n \"listen\",\n \"study\",\n \"app\",\n \"feedback\",\n \"creator\",\n \"admin\",\n \"surface-info\",\n];\n\nexport interface BuildMenuModelOptions {\n /** What an unapproved heading does: \"throw\" (development/tests) or \"inline\" (production). */\n headingPolicy?: HeadingPolicy;\n i18n?: I18nPort;\n /** Content the menu acts on when nothing is selected. */\n content?: string | null;\n /**\n * The name of the field the content is (a text box's label, \"Body\"). The\n * header then names the field — never \"Content: {{reply.body}}\", its raw text.\n */\n contentLabel?: string | null;\n}\n\nfunction translate(i18n: I18nPort | undefined, key: string, fallback: string): string {\n if (!i18n) return fallback;\n try {\n return i18n.t(key, fallback);\n } catch {\n return fallback;\n }\n}\n\n/** One resolved action → one model node (item, checkbox, link or lazy submenu). */\nexport function actionToNode(\n resolved: ResolvedAction,\n target: ClickTarget,\n options: BuildMenuModelOptions = {},\n): MenuNode {\n const { action, eligibility } = resolved;\n const rawLabel = typeof action.label === \"function\" ? action.label(target) : action.label;\n const label = translate(options.i18n, action.labelKey ?? action.id, rawLabel);\n const base = {\n id: action.id,\n actionId: action.id,\n ...(action.icon !== undefined ? { icon: action.icon } : {}),\n ...(action.placement !== undefined ? { placement: action.placement } : {}),\n };\n if (action.expand) {\n const node: MenuSubmenuNode = {\n ...base,\n kind: \"submenu\",\n label,\n lazy: true,\n loading: false,\n ...(action.pending?.(target) ? { pending: true } : {}),\n children: [],\n ...(action.emptyLabel !== undefined ? { emptyLabel: action.emptyLabel } : {}),\n };\n return node;\n }\n if (action.href !== undefined) {\n const href = typeof action.href === \"function\" ? action.href(target) : action.href;\n return { ...base, kind: \"link\", label, href };\n }\n if (action.toggle) {\n return {\n ...base,\n kind: \"checkbox\",\n label,\n checked: action.pressed?.(target) ?? false,\n ...(action.description !== undefined ? { description: action.description } : {}),\n };\n }\n const state = action.stateIcon?.(target) ?? null;\n const pressed = action.pressed?.(target);\n const hint = action.hint !== undefined ? translate(options.i18n, action.hintKey ?? `${action.id}.hint`, action.hint) : undefined;\n return {\n ...base,\n kind: \"item\",\n label,\n ...(action.iconTone !== undefined ? { iconTone: action.iconTone } : {}),\n ...(state ? { stateIcon: state } : {}),\n ...(action.description !== undefined ? { description: action.description } : {}),\n ...(hint !== undefined ? { hint } : {}),\n ...(pressed !== undefined ? { pressed } : {}),\n ...(action.destructive ? { destructive: true } : {}),\n ...(action.preserveSelection ? { preserveSelection: true } : {}),\n ...(eligibility.status === \"unavailable-verb\" || eligibility.status === \"unavailable\"\n ? { unavailable: { sentence: eligibility.sentence } }\n : {}),\n };\n}\n\nexport function buildMenuModel(\n target: ClickTarget,\n resolved: readonly ResolvedAction[],\n options: BuildMenuModelOptions = {},\n): MenuModel {\n // THE ONE HEADING CHECK, for every host: an unapproved heading's rows go\n // inline (production) or the build throws with the remedy (dev/tests).\n const policy = options.headingPolicy ?? defaultHeadingPolicy();\n resolved = resolved.map((r) => {\n const sec = r.action.section;\n if (r.action.expand && isContainerWord(typeof r.action.label === \"string\" ? r.action.label : \"\") && policy === \"throw\") {\n throw new UnapprovedHeadingError(r.action.label as string, `submenu of ${r.action.id}`);\n }\n if (!sec) return r;\n const kind = sec.kind ?? (sec.primary ? \"target\" : \"group\");\n if (isApprovedHeading(sec.label, kind)) return r;\n if (policy === \"throw\") throw new UnapprovedHeadingError(sec.label, `section of ${r.action.id}`);\n const { section: _dropped, ...rest } = r.action;\n return { ...r, action: rest };\n });\n const ordered = resolved\n .map((r, index) => ({ r, index }))\n .sort((a, b) => {\n const ca = CATEGORY_ORDER.indexOf(a.r.action.category);\n const cb = CATEGORY_ORDER.indexOf(b.r.action.category);\n if (ca !== cb) return ca - cb;\n const oa = a.r.action.order ?? 0;\n const ob = b.r.action.order ?? 0;\n if (oa !== ob) return oa - ob;\n return a.index - b.index;\n })\n .map((x) => x.r);\n\n const sections = new Map<string, MenuSection>();\n const roles: Record<string, string> = {};\n // An explicit section sits in the group of its first-registered action and\n // keeps its own row order (order, then registration) across categories.\n const explicit = new Map<string, { group: MenuGroup; rows: { r: ResolvedAction; index: number }[] }>();\n resolved.forEach((r, index) => {\n const sec = r.action.section;\n if (!sec) return;\n const entry = explicit.get(sec.id) ?? { group: CATEGORY_GROUP[r.action.category], rows: [] };\n entry.rows.push({ r, index });\n explicit.set(sec.id, entry);\n });\n const placed = new Set<string>();\n for (const r of ordered) {\n const sec = r.action.section;\n if (sec && placed.has(sec.id)) continue;\n const group = sec ? (explicit.get(sec.id)?.group ?? CATEGORY_GROUP[r.action.category]) : CATEGORY_GROUP[r.action.category];\n const sectionId = sec ? `section:${sec.id}` : group;\n let section = sections.get(sectionId);\n if (!section) {\n section = {\n id: sectionId,\n group,\n nodes: [],\n ...(sec ? { label: translate(options.i18n, sec.labelKey ?? `section.${sec.id}`, sec.label) } : {}),\n ...(sec?.icon !== undefined ? { icon: sec.icon } : {}),\n ...(sec?.primary ? { primary: true } : {}),\n ...(sec && (sec.kind ?? (sec.primary ? \"target\" : \"group\")) === \"target\" ? { target: true } : {}),\n };\n sections.set(sectionId, section);\n }\n const rows = sec\n ? (explicit.get(sec.id)?.rows ?? [])\n .sort((x, y) => (x.r.action.order ?? 0) - (y.r.action.order ?? 0) || x.index - y.index)\n .map((x) => x.r)\n : [r];\n if (sec) placed.add(sec.id);\n for (const row of rows) {\n if (row.action.startsGroup && section.nodes.length > 0) {\n section.nodes.push({ kind: \"separator\", id: `${row.action.id}:group` });\n }\n section.nodes.push(actionToNode(row, target, options));\n if (row.action.verb) roles[row.action.verb] = row.action.id;\n }\n }\n\n const list = [...sections.values()].sort((a, b) => {\n if (Boolean(a.primary) !== Boolean(b.primary)) return a.primary ? -1 : 1;\n return GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group);\n });\n\n const selected = target.selection?.text?.trim();\n const content = options.content?.trim();\n const fieldName = options.contentLabel?.trim();\n const header = selected\n ? { label: \"Selected\", text: target.selection?.text ?? \"\" }\n : fieldName\n ? { label: fieldName, text: options.content ?? \"\" }\n : content\n ? { label: \"Content\", text: options.content ?? \"\" }\n : null;\n\n return { header, sections: list, roles };\n}\n\n/** Fill a lazy submenu's rows once they load (pure: returns a new model). */\n/**\n * AN EXPANDED SUBMENU KEEPS ITS GROUPS (DATA-V2-BASICS-2, 2026-09-28). A provider's rows arrive flat,\n * so a submenu drew \"Highlight row\" and \"Delete row…\" as neighbours with no divider, while the same\n * rows in a top-level menu sit in their sections. A divider now goes wherever the rows' `section`\n * changes — a row with no section and a row with one are different groups too.\n */\nexport function withGroupDividers(children: readonly Action[], nodes: readonly MenuNode[]): MenuNode[] {\n const out: MenuNode[] = [];\n children.forEach((child, i) => {\n const node = nodes[i];\n if (!node) return;\n if (i > 0 && (child.startsGroup || (children[i - 1]!.section?.id ?? null) !== (child.section?.id ?? null))) {\n out.push({ kind: \"separator\", id: `${child.id}:group` });\n }\n out.push(node);\n });\n return out;\n}\n\nexport function withExpandedChildren(model: MenuModel, nodeId: string, children: MenuNode[] | \"loading\"): MenuModel {\n const visit = (nodes: MenuNode[]): MenuNode[] =>\n nodes.map((n) => {\n if (n.kind !== \"submenu\") return n;\n if (n.id === nodeId) {\n return children === \"loading\" ? { ...n, loading: true } : { ...n, lazy: false, loading: false, pending: false, children };\n }\n return { ...n, children: visit(n.children) };\n });\n return { ...model, sections: model.sections.map((s) => ({ ...s, nodes: visit(s.nodes) })) };\n}\n\n/** Find a node anywhere in a tree. */\nexport function findNode(nodes: readonly MenuNode[], id: string): MenuNode | null {\n for (const n of nodes) {\n if (n.id === id) return n;\n if (n.kind === \"submenu\") {\n const hit = findNode(n.children, id);\n if (hit) return hit;\n }\n }\n return null;\n}\n","/**\n * Layouts — pure arrangements of one MenuModel (CONTRACT §2.5).\n *\n * Every layout is a function over the SAME model: it decides which nodes sit\n * at the top level, which fold under a named submenu and which go into a\n * strip. None adds, drops or renames a leaf — `leafIds(arranged)` equals\n * `leafIds(model)` for every layout (THE LOSSLESS LAW, proven by test).\n *\n * The clipboard group IS the universal verb strip: in the strip layouts it is\n * drawn icon-only; elsewhere its rows are drawn in place. Unavailable verbs\n * (the only greyed nodes, R1 c) appear in every layout identically.\n */\nimport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n MenuLeafNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSubmenuNode,\n} from \"./types\";\n\n/** A section with more rows than this folds under its label in tiered/command/sheet. */\nexport const INLINE_SECTION_MAX = 3;\n\n/**\n * A section's heading is ONLY the name its actions declare (an approved\n * heading the classic menu already uses: \"Save as\", \"Copy as\", \"History\").\n * A group with no declared name has no heading and is never folded — its rows\n * show inline. The package never coins a heading (chair ruling, ALC-15).\n */\nexport function sectionLabel(section: MenuSection): string | null {\n return section.label ?? null;\n}\n\nfunction isLeaf(node: MenuNode): node is MenuLeafNode {\n return node.kind === \"item\" || node.kind === \"checkbox\" || node.kind === \"link\";\n}\n\n/** Every actionable id in a node list, depth-first. A lazy submenu counts as one leaf. */\nfunction collect(nodes: readonly MenuNode[], out: string[]): void {\n for (const n of nodes) {\n if (isLeaf(n)) out.push(n.id);\n else if (n.kind === \"submenu\") {\n if (n.lazy) out.push(n.id);\n else collect(n.children, out);\n }\n }\n}\n\ntype Arranged =\n | MenuModel\n | ArrangedBar\n | ArrangedContext\n | ArrangedSheet\n | ArrangedPalette\n | ArrangedSelectionToolbar\n | MenuSection[];\n\n/** Proof obligation for every layout: its leaf set equals the model's (sorted, de-duplicated). */\nexport function leafIds(arranged: Arranged): readonly string[] {\n const out: string[] = [];\n if (Array.isArray(arranged)) {\n for (const s of arranged) collect(s.nodes, out);\n } else if (\"leaves\" in arranged) {\n for (const l of arranged.leaves) out.push(l.node.id);\n } else if (\"primary\" in arranged && \"overflow\" in arranged) {\n collect(arranged.primary, out);\n for (const s of arranged.overflow) collect(s.nodes, out);\n } else if (\"root\" in arranged) {\n collect(arranged.strip, out);\n collect(arranged.root, out);\n } else if (\"more\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.more) collect(s.nodes, out);\n } else if (\"filterable\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.sections) collect(s.nodes, out);\n } else {\n for (const s of arranged.sections) collect(s.nodes, out);\n }\n return [...new Set(out)].sort();\n}\n\n/** Only a NAMED section folds; callers check `section.label` first. */\nfunction fold(section: MenuSection & { label: string }): MenuSubmenuNode {\n return {\n kind: \"submenu\",\n id: `${section.id}:fold`,\n label: section.label,\n ...(section.icon !== undefined ? { icon: section.icon } : {}),\n children: section.nodes,\n };\n}\n\nfunction rowCount(nodes: readonly MenuNode[]): number {\n return nodes.filter((n) => n.kind !== \"separator\" && n.kind !== \"label\").length;\n}\n\nfunction stripOf(model: MenuModel): MenuLeafNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter(isLeaf) : [];\n}\n\nfunction clipboardRest(model: MenuModel): MenuNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter((n) => !isLeaf(n) && n.kind === \"submenu\") : [];\n}\n\nfunction placementOf(node: MenuNode): \"primary\" | \"overflow\" | \"both\" {\n return \"placement\" in node && node.placement ? node.placement : \"overflow\";\n}\n\n/** The historical flat column: every section top-level. */\nfunction classic(model: MenuModel): ArrangedContext {\n return { strip: [], sections: model.sections, filterable: false };\n}\n\n/**\n * Tiered / command: the universal verbs as an icon strip; every other row by\n * its own name; the clicked target's (primary) section first, inline, heading\n * kept; a long non-primary section folds under ITS OWN label (never a coined\n * heading); History is the one approved grouping and already a group.\n */\nfunction tiered(model: MenuModel, filterable: boolean): ArrangedContext {\n const strip = stripOf(model);\n const sections: MenuSection[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n if (s.group === \"clipboard\" && !s.primary) {\n if (rest.length) sections.push({ ...s, nodes: rest });\n continue;\n }\n if (s.primary) {\n sections.push(s);\n continue;\n }\n if (!s.label || s.group === \"surface-info\" || rowCount(s.nodes) <= INLINE_SECTION_MAX) {\n // A section keeps its declared heading in EVERY layout (the Lossless\n // Law covers headings: ⋯, the bar's ⋯ and the phone sheet show it too).\n sections.push(s);\n continue;\n }\n sections.push({ id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] });\n }\n return { strip, sections, filterable };\n}\n\nexport function context(model: MenuModel, arrangement: ContextArrangement): ArrangedContext {\n if (arrangement === \"classic\") return classic(model);\n return tiered(model, arrangement === \"command\");\n}\n\n/**\n * FOLD UNDER THE SUBJECT — a control that belongs to a CONTAINER of the thing\n * (a tab's \"…\" over the note it shows) draws the container's own rows (the\n * primary sections) on top, and EVERY other row — the verb strip included —\n * under ONE submenu named for the subject (\"Note ▸\"). The label is the\n * subject's name the host supplies (the menu header's label), never a coined\n * group heading. Lossless: rows move, none is dropped or renamed. With no\n * primary section there is nothing to put first, so the arrangement is left\n * as it was. (page-pass /notes, 2026-09-28, Arman's ruling for the tab \"…\".)\n */\nexport function foldUnderSubject(arranged: ArrangedContext, subject: string): ArrangedContext {\n const label = subject.trim();\n const primary = arranged.sections.filter((s) => s.primary);\n const rest = arranged.sections.filter((s) => !s.primary);\n if (!label || primary.length === 0 || (rest.length === 0 && arranged.strip.length === 0)) return arranged;\n const children: MenuNode[] = [];\n if (arranged.strip.length) children.push(...arranged.strip);\n for (const s of rest) {\n if (rowCount(s.nodes) === 0) continue;\n if (children.length) children.push({ kind: \"separator\", id: `${s.id}:subject-sep` });\n if (s.label) children.push({ kind: \"label\", id: `${s.id}:subject-label`, label: s.label });\n children.push(...s.nodes);\n }\n if (rowCount(children) === 0) return { ...arranged, strip: [] };\n return {\n strip: [],\n filterable: false,\n sections: [\n ...primary,\n { id: \"subject\", group: rest[0]?.group ?? primary[0]!.group, nodes: [{ kind: \"submenu\", id: \"subject:fold\", label, children }] },\n ],\n };\n}\n\n/**\n * A long NAMED section folds under its own name (\"Save as ▸\", \"Copy as ▸\");\n * primary sections and unnamed groups stay inline. Folding moves rows, never\n * renames or drops them, and never under a coined heading.\n */\nfunction foldNamed(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map((s) =>\n s.label && !s.primary && rowCount(s.nodes) > INLINE_SECTION_MAX ? { id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] } : s,\n );\n}\n\n/** ⋯ overflow: every section in classic order; long named sections fold under their own name. */\nexport function overflow(model: MenuModel): MenuSection[] {\n return foldNamed(model.sections);\n}\n\n/**\n * The action bar: primary buttons inline, EVERYTHING else under ⋯. A \"both\"\n * action is mirrored in both halves (the leaf set is still the model's).\n * `mini` changes presentation only (icon-only primaries), never membership.\n */\nexport function bar(model: MenuModel, _options: { mini?: boolean } = {}): ArrangedBar {\n const primary: MenuNode[] = [];\n const overflowSections: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n if (n.kind === \"separator\" || n.kind === \"label\") {\n rest.push(n);\n continue;\n }\n const p = placementOf(n);\n if (p === \"primary\" || p === \"both\") primary.push(n);\n if (p === \"overflow\" || p === \"both\") rest.push(n);\n }\n if (rowCount(rest) > 0) overflowSections.push({ ...s, nodes: rest });\n }\n return { primary, overflow: foldNamed(overflowSections) };\n}\n\n/** Selection toolbar: the verbs plus primary actions as a strip; the rest under More. */\nexport function selectionToolbar(model: MenuModel): ArrangedSelectionToolbar {\n const strip: MenuNode[] = [];\n const more: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n const inStrip = (s.group === \"clipboard\" && !s.primary && isLeaf(n)) || placementOf(n) !== \"overflow\";\n if (inStrip) strip.push(n);\n else rest.push(n);\n }\n if (rowCount(rest) > 0) more.push({ ...s, nodes: rest });\n }\n return { strip, more };\n}\n\n/**\n * Mobile bottom sheet: the verb strip on top; primary and short sections\n * inline; longer sections become one drill-down row named by the section.\n * Consumes the model directly, so it can never drift from desktop.\n */\nexport function sheet(model: MenuModel): ArrangedSheet {\n const strip = stripOf(model);\n const root: MenuNode[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n const nodes = s.group === \"clipboard\" && !s.primary ? rest : s.nodes;\n if (rowCount(nodes) === 0) continue;\n if (root.length) root.push({ kind: \"separator\", id: `${s.id}:sep` });\n if (s.primary || !s.label || rowCount(nodes) <= INLINE_SECTION_MAX) {\n if (s.label) root.push({ kind: \"label\", id: `${s.id}:label`, label: s.label });\n root.push(...nodes);\n } else {\n root.push(fold({ ...s, label: s.label, nodes }));\n }\n }\n return { strip, root };\n}\n\n/** Command palette: every leaf flattened with its breadcrumb (Linear). */\nexport function palette(model: MenuModel): ArrangedPalette {\n const leaves: ArrangedPalette[\"leaves\"] = [];\n const walk = (nodes: readonly MenuNode[], trail: string[]) => {\n for (const n of nodes) {\n if (isLeaf(n)) leaves.push({ node: n, breadcrumb: trail });\n else if (n.kind === \"submenu\") {\n if (n.lazy) leaves.push({ node: n, breadcrumb: trail });\n else walk(n.children, [...trail, n.label]);\n }\n }\n };\n for (const s of model.sections) walk(s.nodes, s.label ? [s.label] : []);\n return { leaves };\n}\n\n/**\n * A SEARCH REACHES EVERY ROW (clone 2026-10-01: \"Patient\" found nothing while\n * \"Patient reminder card\" sat under AI Actions ▸ My Personal). `palette` lists a\n * lazy submenu as ONE leaf — its label only — because its rows load on open.\n * A layout that searches opens every lazy submenu it can see: these are the\n * ones still to open (not already loading).\n */\nexport function lazySubmenuIds(model: MenuModel | null): string[] {\n if (!model) return [];\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind !== \"submenu\") continue;\n if (n.lazy) {\n if (!n.loading) out.push(n.id);\n } else walk(n.children);\n }\n };\n for (const s of model.sections) walk(s.nodes);\n return out;\n}\n\n/** True while some submenu's rows are not in the model yet (a search could still find more). */\nexport function paletteStillOpening(model: MenuModel | null): boolean {\n if (!model) return true;\n const walk = (nodes: readonly MenuNode[]): boolean =>\n nodes.some((n) => n.kind === \"submenu\" && (n.lazy || n.loading || n.pending || walk(n.children)));\n return model.sections.some((s) => walk(s.nodes));\n}\n\n/** Filter the palette by a query (every word must match label or breadcrumb). */\nexport function filterPalette(arranged: ArrangedPalette, query: string): ArrangedPalette {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return arranged;\n return {\n leaves: arranged.leaves.filter((l) => {\n const hay = `${l.node.label} ${l.breadcrumb.join(\" \")}`.toLowerCase();\n return words.every((w) => hay.includes(w));\n }),\n };\n}\n\nexport const layouts = { bar, overflow, context, selectionToolbar, sheet, palette };\n","/**\n * N9: layout and density are a PERSON's choice layered over the organization\n * default: persistence (\"alchemy.menu.layout\") → knobs.layout(org) → platform\n * default (command / comfortable, Arman 2026-08-22).\n */\nimport type { AlchemyHostPorts, ContextArrangementKnob, DensityKnob } from \"../ports\";\n\nexport const PLATFORM_MENU_LAYOUT: { context: ContextArrangementKnob; density: DensityKnob } = {\n context: \"command\",\n density: \"comfortable\",\n};\n\nexport const MENU_LAYOUT_SETTING = \"alchemy.menu.layout\";\n\nconst ARRANGEMENTS = new Set<string>([\"classic\", \"tiered\", \"command\"]);\nconst DENSITIES = new Set<string>([\"comfortable\", \"compact\"]);\n\nexport async function resolveMenuLayout(\n ports: Pick<AlchemyHostPorts, \"persistence\" | \"knobs\">,\n organizationId: string | null,\n): Promise<{ context: ContextArrangementKnob; density: DensityKnob }> {\n const org = ports.knobs?.layout(organizationId) ?? null;\n const base = { ...PLATFORM_MENU_LAYOUT, ...(org ?? {}) };\n let own: unknown = null;\n try {\n own = (await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)) ?? null;\n } catch {\n own = null;\n }\n if (own && typeof own === \"object\" && !Array.isArray(own)) {\n const o = own as { context?: unknown; density?: unknown };\n return {\n context: typeof o.context === \"string\" && ARRANGEMENTS.has(o.context) ? (o.context as ContextArrangementKnob) : base.context,\n density: typeof o.density === \"string\" && DENSITIES.has(o.density) ? (o.density as DensityKnob) : base.density,\n };\n }\n return base;\n}\n"],"mappings":"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","names":[]}
package/dist/menu.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/menu/headings.ts","../src/menu/model.ts","../src/menu/layouts.ts","../src/menu/knobs.ts"],"sourcesContent":["/**\n * THE ONE APPROVED-HEADING LIST (ALC-15, chair rulings 7 and D1; the Lossless\n * Law and the vocabulary law). A heading a person sees above or around rows\n * must be a name the classic menus used before agents started coining names.\n *\n * 🚨 ADDING A HEADING TO THIS LIST IS ARMAN'S CALL — never an agent's.\n *\n * Two kinds of section heading exist:\n * - a GROUP heading (\"Copy as\", \"History\"): must be in APPROVED_HEADINGS;\n * - a TARGET label, the thing the person clicked, supplied by the surface at\n * run time (\"Cell · Visit date\", \"Row · Widget A\", \"This finding\", the\n * surface's own label): allowed as declared (`section.kind: \"target\"`, or\n * the \"<Target> · <name>\" shape), except a generic container word.\n * A generic container word (\"More…\", \"Other\", \"Actions\") is never a heading.\n * An unapproved heading renders its rows INLINE in production; in development\n * and tests `buildMenuModel` throws with the remedy.\n */\nimport type { MenuModel, MenuNode } from \"./types\";\n\n/** Names with their provenance (the commit where each classic menu used them). */\nexport const APPROVED_HEADINGS: ReadonlySet<string> = new Set([\n \"Save\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Copy as\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Export\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Edit\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Creator tools\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Admin\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"App\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Compare\", // rich-document menuStructure, 2e3a8a9fdf (2026-06-11)\n \"History\", // context-menu-v3 tiered layout — the one grouping Arman approved, 2026-08-22\n]);\n\n// Ruling (chair, 2026-09-26, per R26): \"Conversation\" is a TARGET label (`kind: \"target\"`) — it names the thing its rows act on (the whole conversation), like \"Cell · …\" and \"Table · …\"; it is not a group heading and is not added above.\n\nconst CONTAINER_WORD =\n /^(more|more…|more\\.\\.\\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i;\n\n/** \"Cell · Visit date\", \"Row · Widget A\", \"Column · Price\", \"Table · Route sheet\". */\nconst TARGET_SHAPE = /^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;\n\nexport function isContainerWord(label: string): boolean {\n return CONTAINER_WORD.test(label.trim());\n}\n\nexport type HeadingKind = \"group\" | \"target\";\n\n/** Is this heading allowed? */\nexport function isApprovedHeading(label: string, kind: HeadingKind = \"group\", approved: ReadonlySet<string> = APPROVED_HEADINGS): boolean {\n const l = label.trim();\n if (!l || isContainerWord(l)) return false;\n if (approved.has(l)) return true;\n return kind === \"target\" || TARGET_SHAPE.test(l);\n}\n\nexport type HeadingPolicy = \"throw\" | \"inline\";\n\n/** Development and tests throw; production shows the rows inline. */\nexport function defaultHeadingPolicy(): HeadingPolicy {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.NODE_ENV;\n return env === \"production\" ? \"inline\" : \"throw\";\n}\n\nexport class UnapprovedHeadingError extends Error {\n readonly heading: string;\n constructor(heading: string, where: string) {\n super(\n `Menu heading \"${heading}\" (${where}) is not an approved heading. Remedy: show these rows inline — declare no section ` +\n `(or kind \"target\" for the clicked thing's own label) — or ask Arman to add the name to APPROVED_HEADINGS in ` +\n `@ai-matrx/alchemy/menu. A generic container word (\"More…\", \"Other\") is never approved.`,\n );\n this.name = \"UnapprovedHeadingError\";\n this.heading = heading;\n }\n}\n\n/** Every heading in a model: section labels, folds, and submenu rows at any depth. */\nexport function menuHeadings(model: Pick<MenuModel, \"sections\">): string[] {\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind === \"submenu\") {\n out.push(n.label);\n walk(n.children);\n }\n }\n };\n for (const s of model.sections) {\n if (s.label) out.push(s.label);\n walk(s.nodes);\n }\n return [...new Set(out)];\n}\n\n/** Section headings not in the approved set (container words are never approved). */\nexport function unapprovedHeadings(model: Pick<MenuModel, \"sections\">, approved: ReadonlySet<string> = APPROVED_HEADINGS): string[] {\n const out = new Set<string>();\n for (const s of model.sections) {\n if (s.label && !isApprovedHeading(s.label, s.primary || s.target ? \"target\" : \"group\", approved)) out.add(s.label);\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) if (n.kind === \"submenu\") {\n if (isContainerWord(n.label)) out.add(n.label);\n walk(n.children);\n }\n };\n walk(s.nodes);\n }\n return [...out];\n}\n","/**\n * buildMenuModel — WHAT exists at a click target, as one framework-neutral\n * tree (CONTRACT §2.5). Categories GROUP, never gate: eligibility already ran\n * in the registry; this only arranges the resolved set into sections.\n */\nimport type { Action, ActionCategory, ClickTarget, ResolvedAction } from \"../actions/types\";\nimport type { I18nPort } from \"../ports\";\nimport type { MenuGroup, MenuModel, MenuNode, MenuSection, MenuSubmenuNode } from \"./types\";\nimport { UnapprovedHeadingError, defaultHeadingPolicy, isApprovedHeading, isContainerWord, type HeadingPolicy } from \"./headings\";\n\n/** THE category → menu group table (CONTRACT §2.5). */\nexport const CATEGORY_GROUP: Readonly<Record<ActionCategory, MenuGroup>> = {\n clipboard: \"clipboard\",\n copy: \"transfer\",\n export: \"transfer\",\n save: \"document\",\n share: \"document\",\n edit: \"editable\",\n history: \"history\",\n ai: \"ai\",\n ask: \"ai\",\n listen: \"tools\",\n study: \"tools\",\n app: \"tools\",\n feedback: \"quick\",\n creator: \"admin\",\n admin: \"admin\",\n \"surface-info\": \"surface-info\",\n};\n\n/** Classic order of the groups. `surface-info` is engine-built and always last. */\nexport const GROUP_ORDER: readonly MenuGroup[] = [\n \"clipboard\",\n \"editable\",\n \"transfer\",\n \"document\",\n \"history\",\n \"ai\",\n \"tools\",\n \"quick\",\n \"surface\",\n \"admin\",\n \"surface-info\",\n];\n\nconst CATEGORY_ORDER: readonly ActionCategory[] = [\n \"clipboard\",\n \"edit\",\n \"copy\",\n \"export\",\n \"save\",\n \"share\",\n \"history\",\n \"ask\",\n \"ai\",\n \"listen\",\n \"study\",\n \"app\",\n \"feedback\",\n \"creator\",\n \"admin\",\n \"surface-info\",\n];\n\nexport interface BuildMenuModelOptions {\n /** What an unapproved heading does: \"throw\" (development/tests) or \"inline\" (production). */\n headingPolicy?: HeadingPolicy;\n i18n?: I18nPort;\n /** Content the menu acts on when nothing is selected. */\n content?: string | null;\n /**\n * The name of the field the content is (a text box's label, \"Body\"). The\n * header then names the field — never \"Content: {{reply.body}}\", its raw text.\n */\n contentLabel?: string | null;\n}\n\nfunction translate(i18n: I18nPort | undefined, key: string, fallback: string): string {\n if (!i18n) return fallback;\n try {\n return i18n.t(key, fallback);\n } catch {\n return fallback;\n }\n}\n\n/** One resolved action → one model node (item, checkbox, link or lazy submenu). */\nexport function actionToNode(\n resolved: ResolvedAction,\n target: ClickTarget,\n options: BuildMenuModelOptions = {},\n): MenuNode {\n const { action, eligibility } = resolved;\n const rawLabel = typeof action.label === \"function\" ? action.label(target) : action.label;\n const label = translate(options.i18n, action.labelKey ?? action.id, rawLabel);\n const base = {\n id: action.id,\n actionId: action.id,\n ...(action.icon !== undefined ? { icon: action.icon } : {}),\n ...(action.placement !== undefined ? { placement: action.placement } : {}),\n };\n if (action.expand) {\n const node: MenuSubmenuNode = {\n ...base,\n kind: \"submenu\",\n label,\n lazy: true,\n loading: false,\n ...(action.pending?.(target) ? { pending: true } : {}),\n children: [],\n ...(action.emptyLabel !== undefined ? { emptyLabel: action.emptyLabel } : {}),\n };\n return node;\n }\n if (action.href !== undefined) {\n const href = typeof action.href === \"function\" ? action.href(target) : action.href;\n return { ...base, kind: \"link\", label, href };\n }\n if (action.toggle) {\n return {\n ...base,\n kind: \"checkbox\",\n label,\n checked: action.pressed?.(target) ?? false,\n ...(action.description !== undefined ? { description: action.description } : {}),\n };\n }\n const state = action.stateIcon?.(target) ?? null;\n const pressed = action.pressed?.(target);\n const hint = action.hint !== undefined ? translate(options.i18n, action.hintKey ?? `${action.id}.hint`, action.hint) : undefined;\n return {\n ...base,\n kind: \"item\",\n label,\n ...(action.iconTone !== undefined ? { iconTone: action.iconTone } : {}),\n ...(state ? { stateIcon: state } : {}),\n ...(action.description !== undefined ? { description: action.description } : {}),\n ...(hint !== undefined ? { hint } : {}),\n ...(pressed !== undefined ? { pressed } : {}),\n ...(action.destructive ? { destructive: true } : {}),\n ...(action.preserveSelection ? { preserveSelection: true } : {}),\n ...(eligibility.status === \"unavailable-verb\" || eligibility.status === \"unavailable\"\n ? { unavailable: { sentence: eligibility.sentence } }\n : {}),\n };\n}\n\nexport function buildMenuModel(\n target: ClickTarget,\n resolved: readonly ResolvedAction[],\n options: BuildMenuModelOptions = {},\n): MenuModel {\n // THE ONE HEADING CHECK, for every host: an unapproved heading's rows go\n // inline (production) or the build throws with the remedy (dev/tests).\n const policy = options.headingPolicy ?? defaultHeadingPolicy();\n resolved = resolved.map((r) => {\n const sec = r.action.section;\n if (r.action.expand && isContainerWord(typeof r.action.label === \"string\" ? r.action.label : \"\") && policy === \"throw\") {\n throw new UnapprovedHeadingError(r.action.label as string, `submenu of ${r.action.id}`);\n }\n if (!sec) return r;\n const kind = sec.kind ?? (sec.primary ? \"target\" : \"group\");\n if (isApprovedHeading(sec.label, kind)) return r;\n if (policy === \"throw\") throw new UnapprovedHeadingError(sec.label, `section of ${r.action.id}`);\n const { section: _dropped, ...rest } = r.action;\n return { ...r, action: rest };\n });\n const ordered = resolved\n .map((r, index) => ({ r, index }))\n .sort((a, b) => {\n const ca = CATEGORY_ORDER.indexOf(a.r.action.category);\n const cb = CATEGORY_ORDER.indexOf(b.r.action.category);\n if (ca !== cb) return ca - cb;\n const oa = a.r.action.order ?? 0;\n const ob = b.r.action.order ?? 0;\n if (oa !== ob) return oa - ob;\n return a.index - b.index;\n })\n .map((x) => x.r);\n\n const sections = new Map<string, MenuSection>();\n const roles: Record<string, string> = {};\n // An explicit section sits in the group of its first-registered action and\n // keeps its own row order (order, then registration) across categories.\n const explicit = new Map<string, { group: MenuGroup; rows: { r: ResolvedAction; index: number }[] }>();\n resolved.forEach((r, index) => {\n const sec = r.action.section;\n if (!sec) return;\n const entry = explicit.get(sec.id) ?? { group: CATEGORY_GROUP[r.action.category], rows: [] };\n entry.rows.push({ r, index });\n explicit.set(sec.id, entry);\n });\n const placed = new Set<string>();\n for (const r of ordered) {\n const sec = r.action.section;\n if (sec && placed.has(sec.id)) continue;\n const group = sec ? (explicit.get(sec.id)?.group ?? CATEGORY_GROUP[r.action.category]) : CATEGORY_GROUP[r.action.category];\n const sectionId = sec ? `section:${sec.id}` : group;\n let section = sections.get(sectionId);\n if (!section) {\n section = {\n id: sectionId,\n group,\n nodes: [],\n ...(sec ? { label: translate(options.i18n, sec.labelKey ?? `section.${sec.id}`, sec.label) } : {}),\n ...(sec?.icon !== undefined ? { icon: sec.icon } : {}),\n ...(sec?.primary ? { primary: true } : {}),\n ...(sec && (sec.kind ?? (sec.primary ? \"target\" : \"group\")) === \"target\" ? { target: true } : {}),\n };\n sections.set(sectionId, section);\n }\n const rows = sec\n ? (explicit.get(sec.id)?.rows ?? [])\n .sort((x, y) => (x.r.action.order ?? 0) - (y.r.action.order ?? 0) || x.index - y.index)\n .map((x) => x.r)\n : [r];\n if (sec) placed.add(sec.id);\n for (const row of rows) {\n if (row.action.startsGroup && section.nodes.length > 0) {\n section.nodes.push({ kind: \"separator\", id: `${row.action.id}:group` });\n }\n section.nodes.push(actionToNode(row, target, options));\n if (row.action.verb) roles[row.action.verb] = row.action.id;\n }\n }\n\n const list = [...sections.values()].sort((a, b) => {\n if (Boolean(a.primary) !== Boolean(b.primary)) return a.primary ? -1 : 1;\n return GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group);\n });\n\n const selected = target.selection?.text?.trim();\n const content = options.content?.trim();\n const fieldName = options.contentLabel?.trim();\n const header = selected\n ? { label: \"Selected\", text: target.selection?.text ?? \"\" }\n : fieldName\n ? { label: fieldName, text: options.content ?? \"\" }\n : content\n ? { label: \"Content\", text: options.content ?? \"\" }\n : null;\n\n return { header, sections: list, roles };\n}\n\n/** Fill a lazy submenu's rows once they load (pure: returns a new model). */\n/**\n * AN EXPANDED SUBMENU KEEPS ITS GROUPS (DATA-V2-BASICS-2, 2026-09-28). A provider's rows arrive flat,\n * so a submenu drew \"Highlight row\" and \"Delete row…\" as neighbours with no divider, while the same\n * rows in a top-level menu sit in their sections. A divider now goes wherever the rows' `section`\n * changes — a row with no section and a row with one are different groups too.\n */\nexport function withGroupDividers(children: readonly Action[], nodes: readonly MenuNode[]): MenuNode[] {\n const out: MenuNode[] = [];\n children.forEach((child, i) => {\n const node = nodes[i];\n if (!node) return;\n if (i > 0 && (child.startsGroup || (children[i - 1]!.section?.id ?? null) !== (child.section?.id ?? null))) {\n out.push({ kind: \"separator\", id: `${child.id}:group` });\n }\n out.push(node);\n });\n return out;\n}\n\nexport function withExpandedChildren(model: MenuModel, nodeId: string, children: MenuNode[] | \"loading\"): MenuModel {\n const visit = (nodes: MenuNode[]): MenuNode[] =>\n nodes.map((n) => {\n if (n.kind !== \"submenu\") return n;\n if (n.id === nodeId) {\n return children === \"loading\" ? { ...n, loading: true } : { ...n, lazy: false, loading: false, pending: false, children };\n }\n return { ...n, children: visit(n.children) };\n });\n return { ...model, sections: model.sections.map((s) => ({ ...s, nodes: visit(s.nodes) })) };\n}\n\n/** Find a node anywhere in a tree. */\nexport function findNode(nodes: readonly MenuNode[], id: string): MenuNode | null {\n for (const n of nodes) {\n if (n.id === id) return n;\n if (n.kind === \"submenu\") {\n const hit = findNode(n.children, id);\n if (hit) return hit;\n }\n }\n return null;\n}\n","/**\n * Layouts — pure arrangements of one MenuModel (CONTRACT §2.5).\n *\n * Every layout is a function over the SAME model: it decides which nodes sit\n * at the top level, which fold under a named submenu and which go into a\n * strip. None adds, drops or renames a leaf — `leafIds(arranged)` equals\n * `leafIds(model)` for every layout (THE LOSSLESS LAW, proven by test).\n *\n * The clipboard group IS the universal verb strip: in the strip layouts it is\n * drawn icon-only; elsewhere its rows are drawn in place. Unavailable verbs\n * (the only greyed nodes, R1 c) appear in every layout identically.\n */\nimport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n MenuLeafNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSubmenuNode,\n} from \"./types\";\n\n/** A section with more rows than this folds under its label in tiered/command/sheet. */\nexport const INLINE_SECTION_MAX = 3;\n\n/**\n * A section's heading is ONLY the name its actions declare (an approved\n * heading the classic menu already uses: \"Save as\", \"Copy as\", \"History\").\n * A group with no declared name has no heading and is never folded — its rows\n * show inline. The package never coins a heading (chair ruling, ALC-15).\n */\nexport function sectionLabel(section: MenuSection): string | null {\n return section.label ?? null;\n}\n\nfunction isLeaf(node: MenuNode): node is MenuLeafNode {\n return node.kind === \"item\" || node.kind === \"checkbox\" || node.kind === \"link\";\n}\n\n/** Every actionable id in a node list, depth-first. A lazy submenu counts as one leaf. */\nfunction collect(nodes: readonly MenuNode[], out: string[]): void {\n for (const n of nodes) {\n if (isLeaf(n)) out.push(n.id);\n else if (n.kind === \"submenu\") {\n if (n.lazy) out.push(n.id);\n else collect(n.children, out);\n }\n }\n}\n\ntype Arranged =\n | MenuModel\n | ArrangedBar\n | ArrangedContext\n | ArrangedSheet\n | ArrangedPalette\n | ArrangedSelectionToolbar\n | MenuSection[];\n\n/** Proof obligation for every layout: its leaf set equals the model's (sorted, de-duplicated). */\nexport function leafIds(arranged: Arranged): readonly string[] {\n const out: string[] = [];\n if (Array.isArray(arranged)) {\n for (const s of arranged) collect(s.nodes, out);\n } else if (\"leaves\" in arranged) {\n for (const l of arranged.leaves) out.push(l.node.id);\n } else if (\"primary\" in arranged && \"overflow\" in arranged) {\n collect(arranged.primary, out);\n for (const s of arranged.overflow) collect(s.nodes, out);\n } else if (\"root\" in arranged) {\n collect(arranged.strip, out);\n collect(arranged.root, out);\n } else if (\"more\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.more) collect(s.nodes, out);\n } else if (\"filterable\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.sections) collect(s.nodes, out);\n } else {\n for (const s of arranged.sections) collect(s.nodes, out);\n }\n return [...new Set(out)].sort();\n}\n\n/** Only a NAMED section folds; callers check `section.label` first. */\nfunction fold(section: MenuSection & { label: string }): MenuSubmenuNode {\n return {\n kind: \"submenu\",\n id: `${section.id}:fold`,\n label: section.label,\n ...(section.icon !== undefined ? { icon: section.icon } : {}),\n children: section.nodes,\n };\n}\n\nfunction rowCount(nodes: readonly MenuNode[]): number {\n return nodes.filter((n) => n.kind !== \"separator\" && n.kind !== \"label\").length;\n}\n\nfunction stripOf(model: MenuModel): MenuLeafNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter(isLeaf) : [];\n}\n\nfunction clipboardRest(model: MenuModel): MenuNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter((n) => !isLeaf(n) && n.kind === \"submenu\") : [];\n}\n\nfunction placementOf(node: MenuNode): \"primary\" | \"overflow\" | \"both\" {\n return \"placement\" in node && node.placement ? node.placement : \"overflow\";\n}\n\n/** The historical flat column: every section top-level. */\nfunction classic(model: MenuModel): ArrangedContext {\n return { strip: [], sections: model.sections, filterable: false };\n}\n\n/**\n * Tiered / command: the universal verbs as an icon strip; every other row by\n * its own name; the clicked target's (primary) section first, inline, heading\n * kept; a long non-primary section folds under ITS OWN label (never a coined\n * heading); History is the one approved grouping and already a group.\n */\nfunction tiered(model: MenuModel, filterable: boolean): ArrangedContext {\n const strip = stripOf(model);\n const sections: MenuSection[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n if (s.group === \"clipboard\" && !s.primary) {\n if (rest.length) sections.push({ ...s, nodes: rest });\n continue;\n }\n if (s.primary) {\n sections.push(s);\n continue;\n }\n if (!s.label || s.group === \"surface-info\" || rowCount(s.nodes) <= INLINE_SECTION_MAX) {\n // A section keeps its declared heading in EVERY layout (the Lossless\n // Law covers headings: ⋯, the bar's ⋯ and the phone sheet show it too).\n sections.push(s);\n continue;\n }\n sections.push({ id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] });\n }\n return { strip, sections, filterable };\n}\n\nexport function context(model: MenuModel, arrangement: ContextArrangement): ArrangedContext {\n if (arrangement === \"classic\") return classic(model);\n return tiered(model, arrangement === \"command\");\n}\n\n/**\n * FOLD UNDER THE SUBJECT — a control that belongs to a CONTAINER of the thing\n * (a tab's \"…\" over the note it shows) draws the container's own rows (the\n * primary sections) on top, and EVERY other row — the verb strip included —\n * under ONE submenu named for the subject (\"Note ▸\"). The label is the\n * subject's name the host supplies (the menu header's label), never a coined\n * group heading. Lossless: rows move, none is dropped or renamed. With no\n * primary section there is nothing to put first, so the arrangement is left\n * as it was. (page-pass /notes, 2026-09-28, Arman's ruling for the tab \"…\".)\n */\nexport function foldUnderSubject(arranged: ArrangedContext, subject: string): ArrangedContext {\n const label = subject.trim();\n const primary = arranged.sections.filter((s) => s.primary);\n const rest = arranged.sections.filter((s) => !s.primary);\n if (!label || primary.length === 0 || (rest.length === 0 && arranged.strip.length === 0)) return arranged;\n const children: MenuNode[] = [];\n if (arranged.strip.length) children.push(...arranged.strip);\n for (const s of rest) {\n if (rowCount(s.nodes) === 0) continue;\n if (children.length) children.push({ kind: \"separator\", id: `${s.id}:subject-sep` });\n if (s.label) children.push({ kind: \"label\", id: `${s.id}:subject-label`, label: s.label });\n children.push(...s.nodes);\n }\n if (rowCount(children) === 0) return { ...arranged, strip: [] };\n return {\n strip: [],\n filterable: false,\n sections: [\n ...primary,\n { id: \"subject\", group: rest[0]?.group ?? primary[0]!.group, nodes: [{ kind: \"submenu\", id: \"subject:fold\", label, children }] },\n ],\n };\n}\n\n/**\n * A long NAMED section folds under its own name (\"Save as ▸\", \"Copy as ▸\");\n * primary sections and unnamed groups stay inline. Folding moves rows, never\n * renames or drops them, and never under a coined heading.\n */\nfunction foldNamed(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map((s) =>\n s.label && !s.primary && rowCount(s.nodes) > INLINE_SECTION_MAX ? { id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] } : s,\n );\n}\n\n/** ⋯ overflow: every section in classic order; long named sections fold under their own name. */\nexport function overflow(model: MenuModel): MenuSection[] {\n return foldNamed(model.sections);\n}\n\n/**\n * The action bar: primary buttons inline, EVERYTHING else under ⋯. A \"both\"\n * action is mirrored in both halves (the leaf set is still the model's).\n * `mini` changes presentation only (icon-only primaries), never membership.\n */\nexport function bar(model: MenuModel, _options: { mini?: boolean } = {}): ArrangedBar {\n const primary: MenuNode[] = [];\n const overflowSections: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n if (n.kind === \"separator\" || n.kind === \"label\") {\n rest.push(n);\n continue;\n }\n const p = placementOf(n);\n if (p === \"primary\" || p === \"both\") primary.push(n);\n if (p === \"overflow\" || p === \"both\") rest.push(n);\n }\n if (rowCount(rest) > 0) overflowSections.push({ ...s, nodes: rest });\n }\n return { primary, overflow: foldNamed(overflowSections) };\n}\n\n/** Selection toolbar: the verbs plus primary actions as a strip; the rest under More. */\nexport function selectionToolbar(model: MenuModel): ArrangedSelectionToolbar {\n const strip: MenuNode[] = [];\n const more: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n const inStrip = (s.group === \"clipboard\" && !s.primary && isLeaf(n)) || placementOf(n) !== \"overflow\";\n if (inStrip) strip.push(n);\n else rest.push(n);\n }\n if (rowCount(rest) > 0) more.push({ ...s, nodes: rest });\n }\n return { strip, more };\n}\n\n/**\n * Mobile bottom sheet: the verb strip on top; primary and short sections\n * inline; longer sections become one drill-down row named by the section.\n * Consumes the model directly, so it can never drift from desktop.\n */\nexport function sheet(model: MenuModel): ArrangedSheet {\n const strip = stripOf(model);\n const root: MenuNode[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n const nodes = s.group === \"clipboard\" && !s.primary ? rest : s.nodes;\n if (rowCount(nodes) === 0) continue;\n if (root.length) root.push({ kind: \"separator\", id: `${s.id}:sep` });\n if (s.primary || !s.label || rowCount(nodes) <= INLINE_SECTION_MAX) {\n if (s.label) root.push({ kind: \"label\", id: `${s.id}:label`, label: s.label });\n root.push(...nodes);\n } else {\n root.push(fold({ ...s, label: s.label, nodes }));\n }\n }\n return { strip, root };\n}\n\n/** Command palette: every leaf flattened with its breadcrumb (Linear). */\nexport function palette(model: MenuModel): ArrangedPalette {\n const leaves: ArrangedPalette[\"leaves\"] = [];\n const walk = (nodes: readonly MenuNode[], trail: string[]) => {\n for (const n of nodes) {\n if (isLeaf(n)) leaves.push({ node: n, breadcrumb: trail });\n else if (n.kind === \"submenu\") {\n if (n.lazy) leaves.push({ node: n, breadcrumb: trail });\n else walk(n.children, [...trail, n.label]);\n }\n }\n };\n for (const s of model.sections) walk(s.nodes, s.label ? [s.label] : []);\n return { leaves };\n}\n\n/** Filter the palette by a query (every word must match label or breadcrumb). */\nexport function filterPalette(arranged: ArrangedPalette, query: string): ArrangedPalette {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return arranged;\n return {\n leaves: arranged.leaves.filter((l) => {\n const hay = `${l.node.label} ${l.breadcrumb.join(\" \")}`.toLowerCase();\n return words.every((w) => hay.includes(w));\n }),\n };\n}\n\nexport const layouts = { bar, overflow, context, selectionToolbar, sheet, palette };\n","/**\n * N9: layout and density are a PERSON's choice layered over the organization\n * default: persistence (\"alchemy.menu.layout\") → knobs.layout(org) → platform\n * default (command / comfortable, Arman 2026-08-22).\n */\nimport type { AlchemyHostPorts, ContextArrangementKnob, DensityKnob } from \"../ports\";\n\nexport const PLATFORM_MENU_LAYOUT: { context: ContextArrangementKnob; density: DensityKnob } = {\n context: \"command\",\n density: \"comfortable\",\n};\n\nexport const MENU_LAYOUT_SETTING = \"alchemy.menu.layout\";\n\nconst ARRANGEMENTS = new Set<string>([\"classic\", \"tiered\", \"command\"]);\nconst DENSITIES = new Set<string>([\"comfortable\", \"compact\"]);\n\nexport async function resolveMenuLayout(\n ports: Pick<AlchemyHostPorts, \"persistence\" | \"knobs\">,\n organizationId: string | null,\n): Promise<{ context: ContextArrangementKnob; density: DensityKnob }> {\n const org = ports.knobs?.layout(organizationId) ?? null;\n const base = { ...PLATFORM_MENU_LAYOUT, ...(org ?? {}) };\n let own: unknown = null;\n try {\n own = (await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)) ?? null;\n } catch {\n own = null;\n }\n if (own && typeof own === \"object\" && !Array.isArray(own)) {\n const o = own as { context?: unknown; density?: unknown };\n return {\n context: typeof o.context === \"string\" && ARRANGEMENTS.has(o.context) ? (o.context as ContextArrangementKnob) : base.context,\n density: typeof o.density === \"string\" && DENSITIES.has(o.density) ? (o.density as DensityKnob) : base.density,\n };\n }\n return base;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/menu/headings.ts","../src/menu/model.ts","../src/menu/layouts.ts","../src/menu/knobs.ts"],"sourcesContent":["/**\n * THE ONE APPROVED-HEADING LIST (ALC-15, chair rulings 7 and D1; the Lossless\n * Law and the vocabulary law). A heading a person sees above or around rows\n * must be a name the classic menus used before agents started coining names.\n *\n * 🚨 ADDING A HEADING TO THIS LIST IS ARMAN'S CALL — never an agent's.\n *\n * Two kinds of section heading exist:\n * - a GROUP heading (\"Copy as\", \"History\"): must be in APPROVED_HEADINGS;\n * - a TARGET label, the thing the person clicked, supplied by the surface at\n * run time (\"Cell · Visit date\", \"Row · Widget A\", \"This finding\", the\n * surface's own label): allowed as declared (`section.kind: \"target\"`, or\n * the \"<Target> · <name>\" shape), except a generic container word.\n * A generic container word (\"More…\", \"Other\", \"Actions\") is never a heading.\n * An unapproved heading renders its rows INLINE in production; in development\n * and tests `buildMenuModel` throws with the remedy.\n */\nimport type { MenuModel, MenuNode } from \"./types\";\n\n/** Names with their provenance (the commit where each classic menu used them). */\nexport const APPROVED_HEADINGS: ReadonlySet<string> = new Set([\n \"Save\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Copy as\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Export\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Edit\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Creator tools\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Admin\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"App\", // rich-document menuStructure, e5b8d01229 (2026-05-19)\n \"Compare\", // rich-document menuStructure, 2e3a8a9fdf (2026-06-11)\n \"History\", // context-menu-v3 tiered layout — the one grouping Arman approved, 2026-08-22\n]);\n\n// Ruling (chair, 2026-09-26, per R26): \"Conversation\" is a TARGET label (`kind: \"target\"`) — it names the thing its rows act on (the whole conversation), like \"Cell · …\" and \"Table · …\"; it is not a group heading and is not added above.\n\nconst CONTAINER_WORD =\n /^(more|more…|more\\.\\.\\.|other|others|misc|miscellaneous|additional|extra|extras|actions|options|advanced|stuff|everything else)$/i;\n\n/** \"Cell · Visit date\", \"Row · Widget A\", \"Column · Price\", \"Table · Route sheet\". */\nconst TARGET_SHAPE = /^(Cell|Row|Column|Table|Range|Selection|Record) · .+$/;\n\nexport function isContainerWord(label: string): boolean {\n return CONTAINER_WORD.test(label.trim());\n}\n\nexport type HeadingKind = \"group\" | \"target\";\n\n/** Is this heading allowed? */\nexport function isApprovedHeading(label: string, kind: HeadingKind = \"group\", approved: ReadonlySet<string> = APPROVED_HEADINGS): boolean {\n const l = label.trim();\n if (!l || isContainerWord(l)) return false;\n if (approved.has(l)) return true;\n return kind === \"target\" || TARGET_SHAPE.test(l);\n}\n\nexport type HeadingPolicy = \"throw\" | \"inline\";\n\n/** Development and tests throw; production shows the rows inline. */\nexport function defaultHeadingPolicy(): HeadingPolicy {\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env?.NODE_ENV;\n return env === \"production\" ? \"inline\" : \"throw\";\n}\n\nexport class UnapprovedHeadingError extends Error {\n readonly heading: string;\n constructor(heading: string, where: string) {\n super(\n `Menu heading \"${heading}\" (${where}) is not an approved heading. Remedy: show these rows inline — declare no section ` +\n `(or kind \"target\" for the clicked thing's own label) — or ask Arman to add the name to APPROVED_HEADINGS in ` +\n `@ai-matrx/alchemy/menu. A generic container word (\"More…\", \"Other\") is never approved.`,\n );\n this.name = \"UnapprovedHeadingError\";\n this.heading = heading;\n }\n}\n\n/** Every heading in a model: section labels, folds, and submenu rows at any depth. */\nexport function menuHeadings(model: Pick<MenuModel, \"sections\">): string[] {\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind === \"submenu\") {\n out.push(n.label);\n walk(n.children);\n }\n }\n };\n for (const s of model.sections) {\n if (s.label) out.push(s.label);\n walk(s.nodes);\n }\n return [...new Set(out)];\n}\n\n/** Section headings not in the approved set (container words are never approved). */\nexport function unapprovedHeadings(model: Pick<MenuModel, \"sections\">, approved: ReadonlySet<string> = APPROVED_HEADINGS): string[] {\n const out = new Set<string>();\n for (const s of model.sections) {\n if (s.label && !isApprovedHeading(s.label, s.primary || s.target ? \"target\" : \"group\", approved)) out.add(s.label);\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) if (n.kind === \"submenu\") {\n if (isContainerWord(n.label)) out.add(n.label);\n walk(n.children);\n }\n };\n walk(s.nodes);\n }\n return [...out];\n}\n","/**\n * buildMenuModel — WHAT exists at a click target, as one framework-neutral\n * tree (CONTRACT §2.5). Categories GROUP, never gate: eligibility already ran\n * in the registry; this only arranges the resolved set into sections.\n */\nimport type { Action, ActionCategory, ClickTarget, ResolvedAction } from \"../actions/types\";\nimport type { I18nPort } from \"../ports\";\nimport type { MenuGroup, MenuModel, MenuNode, MenuSection, MenuSubmenuNode } from \"./types\";\nimport { UnapprovedHeadingError, defaultHeadingPolicy, isApprovedHeading, isContainerWord, type HeadingPolicy } from \"./headings\";\n\n/** THE category → menu group table (CONTRACT §2.5). */\nexport const CATEGORY_GROUP: Readonly<Record<ActionCategory, MenuGroup>> = {\n clipboard: \"clipboard\",\n copy: \"transfer\",\n export: \"transfer\",\n save: \"document\",\n share: \"document\",\n edit: \"editable\",\n history: \"history\",\n ai: \"ai\",\n ask: \"ai\",\n listen: \"tools\",\n study: \"tools\",\n app: \"tools\",\n feedback: \"quick\",\n creator: \"admin\",\n admin: \"admin\",\n \"surface-info\": \"surface-info\",\n};\n\n/** Classic order of the groups. `surface-info` is engine-built and always last. */\nexport const GROUP_ORDER: readonly MenuGroup[] = [\n \"clipboard\",\n \"editable\",\n \"transfer\",\n \"document\",\n \"history\",\n \"ai\",\n \"tools\",\n \"quick\",\n \"surface\",\n \"admin\",\n \"surface-info\",\n];\n\nconst CATEGORY_ORDER: readonly ActionCategory[] = [\n \"clipboard\",\n \"edit\",\n \"copy\",\n \"export\",\n \"save\",\n \"share\",\n \"history\",\n \"ask\",\n \"ai\",\n \"listen\",\n \"study\",\n \"app\",\n \"feedback\",\n \"creator\",\n \"admin\",\n \"surface-info\",\n];\n\nexport interface BuildMenuModelOptions {\n /** What an unapproved heading does: \"throw\" (development/tests) or \"inline\" (production). */\n headingPolicy?: HeadingPolicy;\n i18n?: I18nPort;\n /** Content the menu acts on when nothing is selected. */\n content?: string | null;\n /**\n * The name of the field the content is (a text box's label, \"Body\"). The\n * header then names the field — never \"Content: {{reply.body}}\", its raw text.\n */\n contentLabel?: string | null;\n}\n\nfunction translate(i18n: I18nPort | undefined, key: string, fallback: string): string {\n if (!i18n) return fallback;\n try {\n return i18n.t(key, fallback);\n } catch {\n return fallback;\n }\n}\n\n/** One resolved action → one model node (item, checkbox, link or lazy submenu). */\nexport function actionToNode(\n resolved: ResolvedAction,\n target: ClickTarget,\n options: BuildMenuModelOptions = {},\n): MenuNode {\n const { action, eligibility } = resolved;\n const rawLabel = typeof action.label === \"function\" ? action.label(target) : action.label;\n const label = translate(options.i18n, action.labelKey ?? action.id, rawLabel);\n const base = {\n id: action.id,\n actionId: action.id,\n ...(action.icon !== undefined ? { icon: action.icon } : {}),\n ...(action.placement !== undefined ? { placement: action.placement } : {}),\n };\n if (action.expand) {\n const node: MenuSubmenuNode = {\n ...base,\n kind: \"submenu\",\n label,\n lazy: true,\n loading: false,\n ...(action.pending?.(target) ? { pending: true } : {}),\n children: [],\n ...(action.emptyLabel !== undefined ? { emptyLabel: action.emptyLabel } : {}),\n };\n return node;\n }\n if (action.href !== undefined) {\n const href = typeof action.href === \"function\" ? action.href(target) : action.href;\n return { ...base, kind: \"link\", label, href };\n }\n if (action.toggle) {\n return {\n ...base,\n kind: \"checkbox\",\n label,\n checked: action.pressed?.(target) ?? false,\n ...(action.description !== undefined ? { description: action.description } : {}),\n };\n }\n const state = action.stateIcon?.(target) ?? null;\n const pressed = action.pressed?.(target);\n const hint = action.hint !== undefined ? translate(options.i18n, action.hintKey ?? `${action.id}.hint`, action.hint) : undefined;\n return {\n ...base,\n kind: \"item\",\n label,\n ...(action.iconTone !== undefined ? { iconTone: action.iconTone } : {}),\n ...(state ? { stateIcon: state } : {}),\n ...(action.description !== undefined ? { description: action.description } : {}),\n ...(hint !== undefined ? { hint } : {}),\n ...(pressed !== undefined ? { pressed } : {}),\n ...(action.destructive ? { destructive: true } : {}),\n ...(action.preserveSelection ? { preserveSelection: true } : {}),\n ...(eligibility.status === \"unavailable-verb\" || eligibility.status === \"unavailable\"\n ? { unavailable: { sentence: eligibility.sentence } }\n : {}),\n };\n}\n\nexport function buildMenuModel(\n target: ClickTarget,\n resolved: readonly ResolvedAction[],\n options: BuildMenuModelOptions = {},\n): MenuModel {\n // THE ONE HEADING CHECK, for every host: an unapproved heading's rows go\n // inline (production) or the build throws with the remedy (dev/tests).\n const policy = options.headingPolicy ?? defaultHeadingPolicy();\n resolved = resolved.map((r) => {\n const sec = r.action.section;\n if (r.action.expand && isContainerWord(typeof r.action.label === \"string\" ? r.action.label : \"\") && policy === \"throw\") {\n throw new UnapprovedHeadingError(r.action.label as string, `submenu of ${r.action.id}`);\n }\n if (!sec) return r;\n const kind = sec.kind ?? (sec.primary ? \"target\" : \"group\");\n if (isApprovedHeading(sec.label, kind)) return r;\n if (policy === \"throw\") throw new UnapprovedHeadingError(sec.label, `section of ${r.action.id}`);\n const { section: _dropped, ...rest } = r.action;\n return { ...r, action: rest };\n });\n const ordered = resolved\n .map((r, index) => ({ r, index }))\n .sort((a, b) => {\n const ca = CATEGORY_ORDER.indexOf(a.r.action.category);\n const cb = CATEGORY_ORDER.indexOf(b.r.action.category);\n if (ca !== cb) return ca - cb;\n const oa = a.r.action.order ?? 0;\n const ob = b.r.action.order ?? 0;\n if (oa !== ob) return oa - ob;\n return a.index - b.index;\n })\n .map((x) => x.r);\n\n const sections = new Map<string, MenuSection>();\n const roles: Record<string, string> = {};\n // An explicit section sits in the group of its first-registered action and\n // keeps its own row order (order, then registration) across categories.\n const explicit = new Map<string, { group: MenuGroup; rows: { r: ResolvedAction; index: number }[] }>();\n resolved.forEach((r, index) => {\n const sec = r.action.section;\n if (!sec) return;\n const entry = explicit.get(sec.id) ?? { group: CATEGORY_GROUP[r.action.category], rows: [] };\n entry.rows.push({ r, index });\n explicit.set(sec.id, entry);\n });\n const placed = new Set<string>();\n for (const r of ordered) {\n const sec = r.action.section;\n if (sec && placed.has(sec.id)) continue;\n const group = sec ? (explicit.get(sec.id)?.group ?? CATEGORY_GROUP[r.action.category]) : CATEGORY_GROUP[r.action.category];\n const sectionId = sec ? `section:${sec.id}` : group;\n let section = sections.get(sectionId);\n if (!section) {\n section = {\n id: sectionId,\n group,\n nodes: [],\n ...(sec ? { label: translate(options.i18n, sec.labelKey ?? `section.${sec.id}`, sec.label) } : {}),\n ...(sec?.icon !== undefined ? { icon: sec.icon } : {}),\n ...(sec?.primary ? { primary: true } : {}),\n ...(sec && (sec.kind ?? (sec.primary ? \"target\" : \"group\")) === \"target\" ? { target: true } : {}),\n };\n sections.set(sectionId, section);\n }\n const rows = sec\n ? (explicit.get(sec.id)?.rows ?? [])\n .sort((x, y) => (x.r.action.order ?? 0) - (y.r.action.order ?? 0) || x.index - y.index)\n .map((x) => x.r)\n : [r];\n if (sec) placed.add(sec.id);\n for (const row of rows) {\n if (row.action.startsGroup && section.nodes.length > 0) {\n section.nodes.push({ kind: \"separator\", id: `${row.action.id}:group` });\n }\n section.nodes.push(actionToNode(row, target, options));\n if (row.action.verb) roles[row.action.verb] = row.action.id;\n }\n }\n\n const list = [...sections.values()].sort((a, b) => {\n if (Boolean(a.primary) !== Boolean(b.primary)) return a.primary ? -1 : 1;\n return GROUP_ORDER.indexOf(a.group) - GROUP_ORDER.indexOf(b.group);\n });\n\n const selected = target.selection?.text?.trim();\n const content = options.content?.trim();\n const fieldName = options.contentLabel?.trim();\n const header = selected\n ? { label: \"Selected\", text: target.selection?.text ?? \"\" }\n : fieldName\n ? { label: fieldName, text: options.content ?? \"\" }\n : content\n ? { label: \"Content\", text: options.content ?? \"\" }\n : null;\n\n return { header, sections: list, roles };\n}\n\n/** Fill a lazy submenu's rows once they load (pure: returns a new model). */\n/**\n * AN EXPANDED SUBMENU KEEPS ITS GROUPS (DATA-V2-BASICS-2, 2026-09-28). A provider's rows arrive flat,\n * so a submenu drew \"Highlight row\" and \"Delete row…\" as neighbours with no divider, while the same\n * rows in a top-level menu sit in their sections. A divider now goes wherever the rows' `section`\n * changes — a row with no section and a row with one are different groups too.\n */\nexport function withGroupDividers(children: readonly Action[], nodes: readonly MenuNode[]): MenuNode[] {\n const out: MenuNode[] = [];\n children.forEach((child, i) => {\n const node = nodes[i];\n if (!node) return;\n if (i > 0 && (child.startsGroup || (children[i - 1]!.section?.id ?? null) !== (child.section?.id ?? null))) {\n out.push({ kind: \"separator\", id: `${child.id}:group` });\n }\n out.push(node);\n });\n return out;\n}\n\nexport function withExpandedChildren(model: MenuModel, nodeId: string, children: MenuNode[] | \"loading\"): MenuModel {\n const visit = (nodes: MenuNode[]): MenuNode[] =>\n nodes.map((n) => {\n if (n.kind !== \"submenu\") return n;\n if (n.id === nodeId) {\n return children === \"loading\" ? { ...n, loading: true } : { ...n, lazy: false, loading: false, pending: false, children };\n }\n return { ...n, children: visit(n.children) };\n });\n return { ...model, sections: model.sections.map((s) => ({ ...s, nodes: visit(s.nodes) })) };\n}\n\n/** Find a node anywhere in a tree. */\nexport function findNode(nodes: readonly MenuNode[], id: string): MenuNode | null {\n for (const n of nodes) {\n if (n.id === id) return n;\n if (n.kind === \"submenu\") {\n const hit = findNode(n.children, id);\n if (hit) return hit;\n }\n }\n return null;\n}\n","/**\n * Layouts — pure arrangements of one MenuModel (CONTRACT §2.5).\n *\n * Every layout is a function over the SAME model: it decides which nodes sit\n * at the top level, which fold under a named submenu and which go into a\n * strip. None adds, drops or renames a leaf — `leafIds(arranged)` equals\n * `leafIds(model)` for every layout (THE LOSSLESS LAW, proven by test).\n *\n * The clipboard group IS the universal verb strip: in the strip layouts it is\n * drawn icon-only; elsewhere its rows are drawn in place. Unavailable verbs\n * (the only greyed nodes, R1 c) appear in every layout identically.\n */\nimport type {\n ArrangedBar,\n ArrangedContext,\n ArrangedPalette,\n ArrangedSelectionToolbar,\n ArrangedSheet,\n ContextArrangement,\n MenuLeafNode,\n MenuModel,\n MenuNode,\n MenuSection,\n MenuSubmenuNode,\n} from \"./types\";\n\n/** A section with more rows than this folds under its label in tiered/command/sheet. */\nexport const INLINE_SECTION_MAX = 3;\n\n/**\n * A section's heading is ONLY the name its actions declare (an approved\n * heading the classic menu already uses: \"Save as\", \"Copy as\", \"History\").\n * A group with no declared name has no heading and is never folded — its rows\n * show inline. The package never coins a heading (chair ruling, ALC-15).\n */\nexport function sectionLabel(section: MenuSection): string | null {\n return section.label ?? null;\n}\n\nfunction isLeaf(node: MenuNode): node is MenuLeafNode {\n return node.kind === \"item\" || node.kind === \"checkbox\" || node.kind === \"link\";\n}\n\n/** Every actionable id in a node list, depth-first. A lazy submenu counts as one leaf. */\nfunction collect(nodes: readonly MenuNode[], out: string[]): void {\n for (const n of nodes) {\n if (isLeaf(n)) out.push(n.id);\n else if (n.kind === \"submenu\") {\n if (n.lazy) out.push(n.id);\n else collect(n.children, out);\n }\n }\n}\n\ntype Arranged =\n | MenuModel\n | ArrangedBar\n | ArrangedContext\n | ArrangedSheet\n | ArrangedPalette\n | ArrangedSelectionToolbar\n | MenuSection[];\n\n/** Proof obligation for every layout: its leaf set equals the model's (sorted, de-duplicated). */\nexport function leafIds(arranged: Arranged): readonly string[] {\n const out: string[] = [];\n if (Array.isArray(arranged)) {\n for (const s of arranged) collect(s.nodes, out);\n } else if (\"leaves\" in arranged) {\n for (const l of arranged.leaves) out.push(l.node.id);\n } else if (\"primary\" in arranged && \"overflow\" in arranged) {\n collect(arranged.primary, out);\n for (const s of arranged.overflow) collect(s.nodes, out);\n } else if (\"root\" in arranged) {\n collect(arranged.strip, out);\n collect(arranged.root, out);\n } else if (\"more\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.more) collect(s.nodes, out);\n } else if (\"filterable\" in arranged) {\n collect(arranged.strip, out);\n for (const s of arranged.sections) collect(s.nodes, out);\n } else {\n for (const s of arranged.sections) collect(s.nodes, out);\n }\n return [...new Set(out)].sort();\n}\n\n/** Only a NAMED section folds; callers check `section.label` first. */\nfunction fold(section: MenuSection & { label: string }): MenuSubmenuNode {\n return {\n kind: \"submenu\",\n id: `${section.id}:fold`,\n label: section.label,\n ...(section.icon !== undefined ? { icon: section.icon } : {}),\n children: section.nodes,\n };\n}\n\nfunction rowCount(nodes: readonly MenuNode[]): number {\n return nodes.filter((n) => n.kind !== \"separator\" && n.kind !== \"label\").length;\n}\n\nfunction stripOf(model: MenuModel): MenuLeafNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter(isLeaf) : [];\n}\n\nfunction clipboardRest(model: MenuModel): MenuNode[] {\n const clipboard = model.sections.find((s) => s.group === \"clipboard\" && !s.primary);\n return clipboard ? clipboard.nodes.filter((n) => !isLeaf(n) && n.kind === \"submenu\") : [];\n}\n\nfunction placementOf(node: MenuNode): \"primary\" | \"overflow\" | \"both\" {\n return \"placement\" in node && node.placement ? node.placement : \"overflow\";\n}\n\n/** The historical flat column: every section top-level. */\nfunction classic(model: MenuModel): ArrangedContext {\n return { strip: [], sections: model.sections, filterable: false };\n}\n\n/**\n * Tiered / command: the universal verbs as an icon strip; every other row by\n * its own name; the clicked target's (primary) section first, inline, heading\n * kept; a long non-primary section folds under ITS OWN label (never a coined\n * heading); History is the one approved grouping and already a group.\n */\nfunction tiered(model: MenuModel, filterable: boolean): ArrangedContext {\n const strip = stripOf(model);\n const sections: MenuSection[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n if (s.group === \"clipboard\" && !s.primary) {\n if (rest.length) sections.push({ ...s, nodes: rest });\n continue;\n }\n if (s.primary) {\n sections.push(s);\n continue;\n }\n if (!s.label || s.group === \"surface-info\" || rowCount(s.nodes) <= INLINE_SECTION_MAX) {\n // A section keeps its declared heading in EVERY layout (the Lossless\n // Law covers headings: ⋯, the bar's ⋯ and the phone sheet show it too).\n sections.push(s);\n continue;\n }\n sections.push({ id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] });\n }\n return { strip, sections, filterable };\n}\n\nexport function context(model: MenuModel, arrangement: ContextArrangement): ArrangedContext {\n if (arrangement === \"classic\") return classic(model);\n return tiered(model, arrangement === \"command\");\n}\n\n/**\n * FOLD UNDER THE SUBJECT — a control that belongs to a CONTAINER of the thing\n * (a tab's \"…\" over the note it shows) draws the container's own rows (the\n * primary sections) on top, and EVERY other row — the verb strip included —\n * under ONE submenu named for the subject (\"Note ▸\"). The label is the\n * subject's name the host supplies (the menu header's label), never a coined\n * group heading. Lossless: rows move, none is dropped or renamed. With no\n * primary section there is nothing to put first, so the arrangement is left\n * as it was. (page-pass /notes, 2026-09-28, Arman's ruling for the tab \"…\".)\n */\nexport function foldUnderSubject(arranged: ArrangedContext, subject: string): ArrangedContext {\n const label = subject.trim();\n const primary = arranged.sections.filter((s) => s.primary);\n const rest = arranged.sections.filter((s) => !s.primary);\n if (!label || primary.length === 0 || (rest.length === 0 && arranged.strip.length === 0)) return arranged;\n const children: MenuNode[] = [];\n if (arranged.strip.length) children.push(...arranged.strip);\n for (const s of rest) {\n if (rowCount(s.nodes) === 0) continue;\n if (children.length) children.push({ kind: \"separator\", id: `${s.id}:subject-sep` });\n if (s.label) children.push({ kind: \"label\", id: `${s.id}:subject-label`, label: s.label });\n children.push(...s.nodes);\n }\n if (rowCount(children) === 0) return { ...arranged, strip: [] };\n return {\n strip: [],\n filterable: false,\n sections: [\n ...primary,\n { id: \"subject\", group: rest[0]?.group ?? primary[0]!.group, nodes: [{ kind: \"submenu\", id: \"subject:fold\", label, children }] },\n ],\n };\n}\n\n/**\n * A long NAMED section folds under its own name (\"Save as ▸\", \"Copy as ▸\");\n * primary sections and unnamed groups stay inline. Folding moves rows, never\n * renames or drops them, and never under a coined heading.\n */\nfunction foldNamed(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map((s) =>\n s.label && !s.primary && rowCount(s.nodes) > INLINE_SECTION_MAX ? { id: s.id, group: s.group, nodes: [fold({ ...s, label: s.label })] } : s,\n );\n}\n\n/** ⋯ overflow: every section in classic order; long named sections fold under their own name. */\nexport function overflow(model: MenuModel): MenuSection[] {\n return foldNamed(model.sections);\n}\n\n/**\n * The action bar: primary buttons inline, EVERYTHING else under ⋯. A \"both\"\n * action is mirrored in both halves (the leaf set is still the model's).\n * `mini` changes presentation only (icon-only primaries), never membership.\n */\nexport function bar(model: MenuModel, _options: { mini?: boolean } = {}): ArrangedBar {\n const primary: MenuNode[] = [];\n const overflowSections: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n if (n.kind === \"separator\" || n.kind === \"label\") {\n rest.push(n);\n continue;\n }\n const p = placementOf(n);\n if (p === \"primary\" || p === \"both\") primary.push(n);\n if (p === \"overflow\" || p === \"both\") rest.push(n);\n }\n if (rowCount(rest) > 0) overflowSections.push({ ...s, nodes: rest });\n }\n return { primary, overflow: foldNamed(overflowSections) };\n}\n\n/** Selection toolbar: the verbs plus primary actions as a strip; the rest under More. */\nexport function selectionToolbar(model: MenuModel): ArrangedSelectionToolbar {\n const strip: MenuNode[] = [];\n const more: MenuSection[] = [];\n for (const s of model.sections) {\n const rest: MenuNode[] = [];\n for (const n of s.nodes) {\n const inStrip = (s.group === \"clipboard\" && !s.primary && isLeaf(n)) || placementOf(n) !== \"overflow\";\n if (inStrip) strip.push(n);\n else rest.push(n);\n }\n if (rowCount(rest) > 0) more.push({ ...s, nodes: rest });\n }\n return { strip, more };\n}\n\n/**\n * Mobile bottom sheet: the verb strip on top; primary and short sections\n * inline; longer sections become one drill-down row named by the section.\n * Consumes the model directly, so it can never drift from desktop.\n */\nexport function sheet(model: MenuModel): ArrangedSheet {\n const strip = stripOf(model);\n const root: MenuNode[] = [];\n const rest = clipboardRest(model);\n for (const s of model.sections) {\n const nodes = s.group === \"clipboard\" && !s.primary ? rest : s.nodes;\n if (rowCount(nodes) === 0) continue;\n if (root.length) root.push({ kind: \"separator\", id: `${s.id}:sep` });\n if (s.primary || !s.label || rowCount(nodes) <= INLINE_SECTION_MAX) {\n if (s.label) root.push({ kind: \"label\", id: `${s.id}:label`, label: s.label });\n root.push(...nodes);\n } else {\n root.push(fold({ ...s, label: s.label, nodes }));\n }\n }\n return { strip, root };\n}\n\n/** Command palette: every leaf flattened with its breadcrumb (Linear). */\nexport function palette(model: MenuModel): ArrangedPalette {\n const leaves: ArrangedPalette[\"leaves\"] = [];\n const walk = (nodes: readonly MenuNode[], trail: string[]) => {\n for (const n of nodes) {\n if (isLeaf(n)) leaves.push({ node: n, breadcrumb: trail });\n else if (n.kind === \"submenu\") {\n if (n.lazy) leaves.push({ node: n, breadcrumb: trail });\n else walk(n.children, [...trail, n.label]);\n }\n }\n };\n for (const s of model.sections) walk(s.nodes, s.label ? [s.label] : []);\n return { leaves };\n}\n\n/**\n * A SEARCH REACHES EVERY ROW (clone 2026-10-01: \"Patient\" found nothing while\n * \"Patient reminder card\" sat under AI Actions ▸ My Personal). `palette` lists a\n * lazy submenu as ONE leaf — its label only — because its rows load on open.\n * A layout that searches opens every lazy submenu it can see: these are the\n * ones still to open (not already loading).\n */\nexport function lazySubmenuIds(model: MenuModel | null): string[] {\n if (!model) return [];\n const out: string[] = [];\n const walk = (nodes: readonly MenuNode[]) => {\n for (const n of nodes) {\n if (n.kind !== \"submenu\") continue;\n if (n.lazy) {\n if (!n.loading) out.push(n.id);\n } else walk(n.children);\n }\n };\n for (const s of model.sections) walk(s.nodes);\n return out;\n}\n\n/** True while some submenu's rows are not in the model yet (a search could still find more). */\nexport function paletteStillOpening(model: MenuModel | null): boolean {\n if (!model) return true;\n const walk = (nodes: readonly MenuNode[]): boolean =>\n nodes.some((n) => n.kind === \"submenu\" && (n.lazy || n.loading || n.pending || walk(n.children)));\n return model.sections.some((s) => walk(s.nodes));\n}\n\n/** Filter the palette by a query (every word must match label or breadcrumb). */\nexport function filterPalette(arranged: ArrangedPalette, query: string): ArrangedPalette {\n const words = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (!words.length) return arranged;\n return {\n leaves: arranged.leaves.filter((l) => {\n const hay = `${l.node.label} ${l.breadcrumb.join(\" \")}`.toLowerCase();\n return words.every((w) => hay.includes(w));\n }),\n };\n}\n\nexport const layouts = { bar, overflow, context, selectionToolbar, sheet, palette };\n","/**\n * N9: layout and density are a PERSON's choice layered over the organization\n * default: persistence (\"alchemy.menu.layout\") → knobs.layout(org) → platform\n * default (command / comfortable, Arman 2026-08-22).\n */\nimport type { AlchemyHostPorts, ContextArrangementKnob, DensityKnob } from \"../ports\";\n\nexport const PLATFORM_MENU_LAYOUT: { context: ContextArrangementKnob; density: DensityKnob } = {\n context: \"command\",\n density: \"comfortable\",\n};\n\nexport const MENU_LAYOUT_SETTING = \"alchemy.menu.layout\";\n\nconst ARRANGEMENTS = new Set<string>([\"classic\", \"tiered\", \"command\"]);\nconst DENSITIES = new Set<string>([\"comfortable\", \"compact\"]);\n\nexport async function resolveMenuLayout(\n ports: Pick<AlchemyHostPorts, \"persistence\" | \"knobs\">,\n organizationId: string | null,\n): Promise<{ context: ContextArrangementKnob; density: DensityKnob }> {\n const org = ports.knobs?.layout(organizationId) ?? null;\n const base = { ...PLATFORM_MENU_LAYOUT, ...(org ?? {}) };\n let own: unknown = null;\n try {\n own = (await ports.persistence?.readSetting(MENU_LAYOUT_SETTING)) ?? null;\n } catch {\n own = null;\n }\n if (own && typeof own === \"object\" && !Array.isArray(own)) {\n const o = own as { context?: unknown; density?: unknown };\n return {\n context: typeof o.context === \"string\" && ARRANGEMENTS.has(o.context) ? (o.context as ContextArrangementKnob) : base.context,\n density: typeof o.density === \"string\" && DENSITIES.has(o.density) ? (o.density as DensityKnob) : base.density,\n };\n }\n return base;\n}\n"],"mappings":"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","names":[]}
package/dist/operate.cjs CHANGED
@@ -24,7 +24,7 @@ ${summary}<data format="${dataFormat}">
24
24
  `;)i++;spans.push({path,start,end:i});let last=index===paths.length-1,expected=last?`
25
25
  `:delimiter;if(!(last&&i===text.length)){if(text[i]!==expected)throw spanError(`Expected ${last?"a line break":"a delimiter"} at character ${i}.`);last||i++}}),linePath!==null&&(spans[lineSlot].end=i),i<text.length&&i++};readLine(columns.map(c=>`/columns/${pointerSegment(c.id)}`),null);for(let row=0;row<rowCount;row++)readLine(columns.map(c=>`/rows/${row}${c.path}`),`/rows/${row}`);if(i<text.length)throw spanError(`Text continues after the last row at character ${i}.`);return spans}function markdownTableSpans(text,columns,rowCount){let spans=[],lines=[],at2=0;for(let line of text.split(`
26
26
  `))lines.push({start:at2,end:at2+line.length}),at2+=line.length+1;if(lines.length!==rowCount+2)throw spanError(`A Markdown table of ${rowCount} rows has ${lines.length} lines.`);let cells=(line,paths)=>{let i=line.start;if(text.slice(i,i+2)!=="| ")throw spanError(`A table line must open with "| " at character ${i}.`);if(i+=2,paths.forEach(path=>{let start=i;for(;i<line.end&&text[i]!=="|";)i+=text[i]==="\\"?2:1;if(text[i]!=="|"||text[i-1]!==" ")throw spanError(`A table cell must close with " |" at character ${i}.`);spans.push({path,start,end:i-1}),i+=2}),i-1!==line.end)throw spanError(`A table line has more cells than columns at character ${i}.`)};cells(lines[0],columns.map(c=>`/columns/${pointerSegment(c.id)}`));for(let row=0;row<rowCount;row++){let line=lines[row+2];spans.push({path:`/rows/${row}`,start:line.start,end:line.end}),cells(line,columns.map(c=>`/rows/${row}${c.path}`))}return spans}function spanMap(draft,artifact){if(artifact.snapshotId!==draft.snapshotId||artifact.draftRevision!==draft.revision)throw spanError("This artifact was built from a different draft, so its spans cannot be read from this one. Seal the current draft again.");let text=artifact.plainText,payload=draft.payload,format=artifact.format;if(format==="json"||format==="compact-json")return jsonTextSpans(text,payload.kind==="rows"?"/rows":"");if(format==="csv"||format==="tsv"){if(payload.kind!=="rows")throw spanError(`${format.toUpperCase()} requires tabular data`);return delimitedSpans(text,format==="csv"?",":" ",exportedColumns(payload.columns),payload.rows.length)}if(format!=="plain"&&format!=="markdown"&&format!=="html")throw spanError(`The ${format} format has no text span map; spans cover plain, Markdown, JSON, CSV and TSV text.`);if(payload.kind==="rows"){let columns=exportedColumns(payload.columns);return format==="markdown"?markdownTableSpans(text,columns,payload.rows.length):delimitedSpans(text," ",columns,payload.rows.length)}if(payload.kind==="json"||payload.kind==="registered")return jsonTextSpans(text);if(payload.kind==="text"&&payload.language&&format==="markdown"){if(fencedCodeBlock(payload.text,payload.language)!==text)throw spanError("The Markdown code block does not hold this draft's text.");let start=text.indexOf(`
27
- `)+1;return[{path:"",start,end:start+payload.text.length}]}return[{path:"",start:0,end:text.length}]}function offsetSpans(spans,offset){return spans.map(s=>({path:s.path,start:s.start+offset,end:s.end+offset}))}function agentPayloadWithSpans(input,environment={}){let{text,body,bodyOffset}=assembleAgentPayload(input,environment);return(input.dataFormat??"json")!=="json"?{text,spans:[{path:"",start:bodyOffset,end:bodyOffset+body.length}]}:{text,spans:jsonTextSpans(body,"",bodyOffset)}}init_core();var EMPTY_STRUCTURE=Object.freeze({exclude:Object.freeze([]),limits:Object.freeze({})}),encode=segment=>segment.replace(/~/g,"~0").replace(/\//g,"~1"),decode=segment=>segment.replace(/~1/g,"/").replace(/~0/g,"~");function pointerSegments(pointer){if(pointer==="")return[];if(!pointer.startsWith("/"))throw new ContentTransferError("invalid-pointer",`"${pointer}" is not a JSON Pointer (it must start with "/").`);return pointer.slice(1).split("/").map(decode)}function joinPointer(segments){return segments.map(segment=>`/${encode(segment)}`).join("")}function patternMatches(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length===s.length&&p.every((segment,index)=>segment==="*"||segment===s[index])}function patternCovers(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length<=s.length&&p.every((segment,index)=>segment==="*"||segment===s[index])}function patternBelow(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length>s.length&&s.every((segment,index)=>p[index]==="*"||p[index]===segment)}function displayPath(pointer,root="data"){let out=root;for(let segment of pointerSegments(pointer))segment==="*"||/^(0|[1-9]\d*)$/.test(segment)?out+=`[${segment}]`:/^[A-Za-z_$][\w$]*$/.test(segment)?out+=`${out?".":""}${segment}`:out+=`[${JSON.stringify(segment)}]`;return out}var sizes=new WeakMap;function jsonSize(value){if(value===null)return 4;if(typeof value=="string")return JSON.stringify(value).length;if(typeof value=="number")return(JSON.stringify(value)??"null").length;if(typeof value=="boolean")return value?4:5;let cached=sizes.get(value);if(cached!==void 0)return cached;let total=2;if(Array.isArray(value))value.forEach((item,index)=>{total+=jsonSize(item)+(index?1:0)});else{let index=0;for(let[key,item]of Object.entries(value))total+=JSON.stringify(key).length+1+jsonSize(item)+(index++?1:0)}return sizes.set(value,total),total}function nodeChars(value){return typeof value=="string"?value.length:jsonSize(value)}var n=value=>value.toLocaleString("en-US");function stubText(value){let chars=n(nodeChars(value));if(typeof value=="string"){let first=(value.split(`
27
+ `)+1;return[{path:"",start,end:start+payload.text.length}]}return[{path:"",start:0,end:text.length}]}function offsetSpans(spans,offset){return spans.map(s=>({path:s.path,start:s.start+offset,end:s.end+offset}))}function agentPayloadWithSpans(input,environment={}){let{text,body,bodyOffset}=assembleAgentPayload(input,environment);return(input.dataFormat??"json")!=="json"?{text,spans:[{path:"",start:bodyOffset,end:bodyOffset+body.length}]}:{text,spans:jsonTextSpans(body,"",bodyOffset)}}var import_format=require("@ai-matrx/kit/format");init_core();var EMPTY_STRUCTURE=Object.freeze({exclude:Object.freeze([]),limits:Object.freeze({})}),encode=segment=>segment.replace(/~/g,"~0").replace(/\//g,"~1"),decode=segment=>segment.replace(/~1/g,"/").replace(/~0/g,"~");function pointerSegments(pointer){if(pointer==="")return[];if(!pointer.startsWith("/"))throw new ContentTransferError("invalid-pointer",`"${pointer}" is not a JSON Pointer (it must start with "/").`);return pointer.slice(1).split("/").map(decode)}function joinPointer(segments){return segments.map(segment=>`/${encode(segment)}`).join("")}function patternMatches(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length===s.length&&p.every((segment,index)=>segment==="*"||segment===s[index])}function patternCovers(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length<=s.length&&p.every((segment,index)=>segment==="*"||segment===s[index])}function patternBelow(pattern,pointer){let p=pointerSegments(pattern),s=pointerSegments(pointer);return p.length>s.length&&s.every((segment,index)=>p[index]==="*"||p[index]===segment)}function displayPath(pointer,root="data"){let out=root;for(let segment of pointerSegments(pointer))segment==="*"||/^(0|[1-9]\d*)$/.test(segment)?out+=`[${segment}]`:/^[A-Za-z_$][\w$]*$/.test(segment)?out+=`${out?".":""}${segment}`:out+=`[${JSON.stringify(segment)}]`;return out}var sizes=new WeakMap;function jsonSize(value){if(value===null)return 4;if(typeof value=="string")return JSON.stringify(value).length;if(typeof value=="number")return(JSON.stringify(value)??"null").length;if(typeof value=="boolean")return value?4:5;let cached=sizes.get(value);if(cached!==void 0)return cached;let total=2;if(Array.isArray(value))value.forEach((item,index)=>{total+=jsonSize(item)+(index?1:0)});else{let index=0;for(let[key,item]of Object.entries(value))total+=JSON.stringify(key).length+1+jsonSize(item)+(index++?1:0)}return sizes.set(value,total),total}function nodeChars(value){return typeof value=="string"?value.length:jsonSize(value)}var n=value=>(0,import_format.formatCount)(value,{locale:"en-US"});function stubText(value){let chars=n(nodeChars(value));if(typeof value=="string"){let first=(value.split(`
28
28
  `,1)[0]??"").trim(),head=first.length>120?`${first.slice(0,120)}\u2026`:first;return`${head?`${head} `:""}[stubbed \u2014 ${chars} chars]`}if(Array.isArray(value))return`[stubbed \u2014 ${n(value.length)} items, ${chars} chars]`;if(value&&typeof value=="object"){let header=Object.entries(value).filter(([,item])=>typeof item=="string"&&item.length<=60&&!item.includes(`
29
29
  `)||typeof item=="number"||typeof item=="boolean").slice(0,4).map(([key,item])=>`${key}: ${String(item)}`);return`[stubbed \u2014 ${header.length?`${header.join(", ")} \u2014 `:""}${n(Object.keys(value).length)} keys, ${chars} chars]`}return`[stubbed \u2014 ${chars} chars]`}function truncateText(text,max){return text.length>max?`${text.slice(0,max)} \u2026 [truncated \u2014 ${n(text.length)} chars]`:text}function assertLimit2(limit,pattern){if(limit.kind!=="stub"&&(!Number.isSafeInteger(limit.value)||limit.value<0))throw new ContentTransferError("invalid-limit",`The limit on ${displayPath(pattern)} must be a whole number of 0 or more.`)}function concretePointers(value,pattern){let want=pointerSegments(pattern),out=[],walk=(node,depth,at2)=>{if(depth===want.length){out.push(joinPointer(at2));return}if(node===null||typeof node!="object")return;let segment=want[depth],entries=Array.isArray(node)?node.map((item,index)=>[String(index),item]):Object.entries(node);for(let[key,child]of entries)(segment==="*"||segment===key)&&walk(child,depth+1,[...at2,key])};return walk(value,0,[]),out}function applyStructureToJson(value,recipe,labels={}){recipe.exclude.forEach(pattern=>pointerSegments(pattern));let limitEntries=Object.entries(recipe.limits);limitEntries.forEach(([pattern,limit])=>{pointerSegments(pattern),assertLimit2(limit,pattern)});let excluded=new Map,limited=new Map,tally=(map,pattern,chars)=>{let current=map.get(pattern)??{count:0,chars:0};map.set(pattern,{count:current.count+1,chars:current.chars+chars})},walk=(node,segments,cap)=>{let pointer=joinPointer(segments),exclusion=segments.length?recipe.exclude.find(pattern=>patternMatches(pattern,pointer)):void 0;if(exclusion!==void 0){tally(excluded,exclusion,nodeChars(node));return}let limitEntry=limitEntries.find(([pattern])=>patternMatches(pattern,pointer)),limit=limitEntry?.[1];if(limitEntry&&limit?.kind==="stub")return tally(limited,limitEntry[0],nodeChars(node)),stubText(node);let nextCap=limitEntry&&limit?.kind==="maxChars"?{pattern:limitEntry[0],max:limit.value}:cap;if(typeof node=="string")return nextCap&&node.length>nextCap.max?(tally(limited,nextCap.pattern,node.length),truncateText(node,nextCap.max)):node;if(node===null||typeof node!="object")return node;if(Array.isArray(node)){let items=node.map((item,index)=>walk(item,[...segments,String(index)],nextCap)).filter(item=>item!==void 0);return limitEntry&&limit?.kind==="maxItems"&&items.length>limit.value&&(tally(limited,limitEntry[0],items.length-limit.value),items=[...items.slice(0,limit.value),`[\u2026 ${n(items.length-limit.value)} more items]`]),items}let out={};for(let[key,item]of Object.entries(node)){let kept=walk(item,[...segments,key],nextCap);kept!==void 0&&(out[key]=kept)}return out},next=walk(value,[],null)??null,omissions=[];for(let[pattern,{count,chars}]of excluded)omissions.push({path:pattern,reason:"excluded",count,label:labels[pattern]??`${displayPath(pattern)} \u2014 ${n(chars)} chars`});for(let[pattern,{count,chars}]of limited){let limit=recipe.limits[pattern],what=limit.kind==="stub"?`stubbed (${n(chars)} chars)`:limit.kind==="maxChars"?`cut to ${n(limit.value)} chars`:`kept the first ${n(limit.value)} items`;omissions.push({path:pattern,reason:"truncated",count,label:`${labels[pattern]??displayPath(pattern)} \u2014 ${what}`})}return{value:next,omissions}}function applyJsonStructure(draft,recipe,labels={}){let payload=draft.payload;if(payload.kind!=="json"&&payload.kind!=="registered"||!recipe.exclude.length&&!Object.keys(recipe.limits).length)return draft;let{value,omissions}=applyStructureToJson(payload.value,recipe,labels);return deriveDraft(draft,{...payload,value},omissions,"Structure choices")}function outputPointer(value,recipe,pointer){let segments=pointerSegments(pointer),out=[],node=value;for(let depth=0;depth<=segments.length;depth++){let here=joinPointer(segments.slice(0,depth));if(depth>0&&recipe.exclude.some(pattern=>patternMatches(pattern,here)))return null;let limitEntry=Object.entries(recipe.limits).find(([pattern])=>patternMatches(pattern,here));if(limitEntry?.[1].kind==="stub"||depth===segments.length)return joinPointer(out);let segment=segments[depth];if(node===null||typeof node!="object"||node===void 0)return null;if(Array.isArray(node)){let index=Number(segment);if(!Number.isSafeInteger(index)||index>=node.length)return null;let shifted=index;for(let j=0;j<index;j++)recipe.exclude.some(pattern=>patternMatches(pattern,`${here}/${j}`))&&shifted--;if(limitEntry?.[1].kind==="maxItems"&&shifted>=limitEntry[1].value)return null;out.push(String(shifted)),node=node[index]}else{if(!Object.prototype.hasOwnProperty.call(node,segment))return null;out.push(segment),node=node[segment]}}return joinPointer(out)}var RANGE=/^\/text\/(\d+)-(\d+)$/;function isProseSection(section){return RANGE.test(section.path)}function sectionLayout(text,sections,included,limits={}){let keep=new Set(included),parsed=sections.map(section=>{let m=RANGE.exec(section.path),start=m?Number(m[1]):NaN,end=m?Number(m[2]):NaN;if(!m||start>end||end>text.length)throw new ContentTransferError("invalid-section",`Section "${section.path}" is not a range of this text. Declare a prose section as /text/<start>-<end> within the text.`);let limit=limits[section.id];return limit&&assertLimit2(limit,section.path),{section,start,end,limit}}).sort((a,b)=>a.start-b.start),out="",at2=0,omissions=[],ranges={};for(let{section,start,end,limit}of parsed){if(start<at2)throw new ContentTransferError("invalid-section",`Section "${section.label}" overlaps the section before it.`);out+=text.slice(at2,start),at2=end;let body=text.slice(start,end);if(!keep.has(section.id)){omissions.push({path:section.path,reason:"excluded",label:`${section.label} \u2014 ${n(body.length)} chars`});continue}let trailing=/\s*$/.exec(body)?.[0]??"",written=body;limit?.kind==="stub"?(written=`${stubText(body.slice(0,body.length-trailing.length))}${trailing}`,omissions.push({path:section.path,reason:"truncated",label:`${section.label} \u2014 stubbed (${n(body.length)} chars)`})):limit?.kind==="maxChars"&&body.length-trailing.length>limit.value&&(written=`${truncateText(body.slice(0,body.length-trailing.length),limit.value)}${trailing}`,omissions.push({path:section.path,reason:"truncated",label:`${section.label} \u2014 cut to ${n(limit.value)} chars`})),ranges[section.id]={start:out.length,end:out.length+written.length-trailing.length},out+=written}return out+=text.slice(at2),{text:out,omissions,ranges}}function applySectionStructure(draft,sections,included,limits={}){let payload=draft.payload;if(payload.kind==="text"||payload.kind==="markdown"){let layout=sectionLayout(payload.text,sections,included,limits);return deriveDraft(draft,{...payload,text:layout.text},layout.omissions,"Selected sections")}if(payload.kind==="json"||payload.kind==="registered"){let keep=new Set(included),labels=Object.fromEntries(sections.map(section=>[section.path,section.label])),recipe={exclude:sections.filter(section=>!keep.has(section.id)).map(section=>section.path),limits:Object.fromEntries(sections.filter(section=>keep.has(section.id)&&limits[section.id]).map(section=>[section.path,limits[section.id]]))},{value,omissions}=applyStructureToJson(payload.value,recipe,labels);return deriveDraft(draft,{...payload,value},omissions,"Selected sections")}return draft}init_core();var TRANSFER_DEFAULT_RECIPE_SETTING="alchemy.transfer.default_recipe",KEY=/^[a-z0-9][a-z0-9._:-]{0,127}$/;function recipeKey(sourceKind){if(!KEY.test(sourceKind))throw new ContentTransferError("invalid-recipe-key",`"${sourceKind}" is not a source-kind id (lowercase letters, digits and . _ : -, up to 128 characters).`);return sourceKind}var isRecord=v=>v!==null&&typeof v=="object"&&!Array.isArray(v);async function resolveDefaultRecipe(ports,sourceKind,organizationId){let key=recipeKey(sourceKind),notices=[],skip=(error,sentence,remedy)=>{notices.push({sentence,remedy}),ports.diagnostics.capture(error,{area:"operate",detail:{recipeKey:key,sentence}})};if(ports.persistence)try{let own=await ports.persistence.readSetting(TRANSFER_DEFAULT_RECIPE_SETTING);if(own!==null){if(!isRecord(own))skip(new Error("person default recipes are not a map"),"Your saved default recipes could not be read (they are not a list of recipes), so they were skipped.","Save your default recipe for this source again.");else if(own[key]!==void 0){let recipe=own[key];if(isRecord(recipe))return{recipe,from:"person",notices};skip(new Error("person default recipe is not an object"),"Your default recipe for this source is not a recipe, so it was skipped.","Save your default recipe for this source again.")}}}catch(error){skip(error,"Your own default recipe could not be read, so the organization's default applies.","Reopen the workspace to try again.")}if(ports.transferKnobs)try{let org=await ports.transferKnobs.defaultRecipe(key,organizationId);if(org!==null){if(isRecord(org))return{recipe:org,from:"organization",notices};skip(new Error("organization default recipe is not an object"),"Your organization's default recipe for this source is not a recipe, so it was skipped.","Ask an organization admin to set the default recipe again.")}}catch(error){skip(error,"Your organization's default recipe could not be read, so the workspace opens on Full.","Reopen the workspace to try again.")}return{recipe:null,from:"built-in",notices}}async function setPersonDefaultRecipe(ports,sourceKind,recipe){let key=recipeKey(sourceKind),persistence=ports.persistence;if(!persistence)throw new ContentTransferError("capability-absent","This app cannot save your own settings, so a personal default recipe is not available here.");let current;try{current=await persistence.readSetting(TRANSFER_DEFAULT_RECIPE_SETTING)}catch(error){let reason=error instanceof Error?error.message:String(error);throw new ContentTransferError("read-failed",`Nothing was saved: your other default recipes could not be read (${reason}), and saving now would have replaced all of them. Try again in a moment.`)}if(current!==null&&!isRecord(current))throw new ContentTransferError("invalid-setting","Nothing was saved: your saved default recipes are not a list of recipes, so one cannot be changed without replacing them all.");let map={...current??{}};if(recipe===null){if(!(key in map))return;delete map[key]}else map[key]=recipe;await persistence.writeSetting(TRANSFER_DEFAULT_RECIPE_SETTING,Object.keys(map).length===0?null:map)}
30
30
  //# sourceMappingURL=operate.cjs.map