@datarails-ai/dr-appspace-cli 0.10.21 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1366 -782
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "@datarails-ai/dr-appspace-cli",
16
- version: "0.10.21",
16
+ version: "0.11.0",
17
17
  description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
18
18
  type: "module",
19
19
  engines: {
@@ -57,7 +57,7 @@ var package_default = {
57
57
  };
58
58
 
59
59
  // src/buildInfo.ts
60
- var BUILD_TIME = "2026-10-05T09:55:27.001Z";
60
+ var BUILD_TIME = "2026-10-07T10:57:32.115Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -5952,7 +5952,7 @@ var CHART_FORMATS = Object.freeze(["currency", "number", "percent"]);
5952
5952
  var PLATFORM_FORMAT = Object.freeze({ "type": "string", "maxLength": 64, "pattern": "^(General|[^\\u0000-\\u001f]*[#0][^\\u0000-\\u001f]*)$" });
5953
5953
  var NEGATIVE_MODES = Object.freeze(["minus", "red_minus", "absolute", "red_absolute"]);
5954
5954
  var FORMAT_PATTERNS = Object.freeze({ "currency": "$#,###", "number": "#,###", "percent": '#,###.#"%"' });
5955
- var PLATFORM_NUMBERS = Object.freeze({ "dr-app-runtime": "1.8.0", "dr-app-widgets": "1.2.0", "dr-components": "1.2.0", "dr-components-controls": "1.1.0" });
5955
+ var PLATFORM_NUMBERS = Object.freeze({ "dr-app-runtime": "1.9.0", "dr-app-widgets": "1.2.0", "dr-components": "1.2.0", "dr-components-controls": "1.1.0" });
5956
5956
  var ROW_ACTION_WHEN = Object.freeze({ "dr-components": "1.2.0" });
5957
5957
  var PROPS_SINCE = Object.freeze({ "select": { "multiple": "1.3.0", "searchable": "1.3.0", "group_by": "1.3.0", "values": "1.3.0", "options[].group": "1.3.0" }, "data_grid": { "theme": "1.3.0", "hierarchy": "1.3.0", "totals": "1.3.0", "pin": "1.3.0", "csv_params": "1.3.0", "columns[].cell": "1.3.0", "columns[].badge_map": "1.3.0", "columns[].status_map": "1.3.0", "columns[].link_field": "1.3.0", "columns[].call": "1.3.0" } });
5958
5958
  var MODULE_OF = Object.freeze({ "widget": "dr-app-widgets", "form": "dr-app-runtime", "list": "dr-app-runtime", "detail": "dr-app-runtime", "queue": "dr-app-runtime", "action": "dr-app-runtime", "filter": "dr-app-runtime", "stat": "dr-app-runtime", "chart": "dr-app-runtime", "custom": "dr-app-runtime", "field": "dr-app-runtime", "number_input": "dr-components", "tabs": "dr-components", "tab_panel": "dr-components", "select": "dr-components", "date_picker": "dr-components", "data_grid": "dr-components", "dialog": "dr-components", "variance_table": "dr-components", "toggle": "dr-components", "control_group": "dr-components", "callout": "dr-components", "scenario_columns": "dr-components", "slider": "dr-components-controls", "button": "dr-components", "status": "dr-components", "badge": "dr-components", "tag": "dr-components", "empty_state": "dr-components", "stepper": "dr-components", "alert": "dr-components", "checkbox": "dr-components", "radio_group": "dr-components", "file_upload": "dr-components", "skeleton": "dr-components", "progress": "dr-components", "accordion": "dr-components", "tooltip_info": "dr-components", "filter_bar": "dr-components", "filter_chip": "dr-components", "date_range": "dr-components", "fiscal_month": "dr-components" });
@@ -6191,6 +6191,10 @@ var THEME_ATTR = "data-dr-theme";
6191
6191
  var DENSITY_ATTR = "data-dr-density";
6192
6192
  var THEME_FILE = "ui/theme.css";
6193
6193
  var CARD_ATTR = "data-dr-card";
6194
+ var SHELL_ATTR = "data-dr-shell";
6195
+ var HEADER_ACTIONS_ATTR = "data-dr-header-actions";
6196
+ var TITLE_FROM_ATTR = "data-dr-title-from";
6197
+ var NAV_COUNTS_ATTR = "data-dr-nav-counts";
6194
6198
  var EMPHASIS_ATTR = "data-dr-emphasis";
6195
6199
  var MUTATING_VERBS = /* @__PURE__ */ new Set(["insert", "update", "delete"]);
6196
6200
  var TABLE_ONLY_VERBS = /* @__PURE__ */ new Set([...MUTATING_VERBS, "get"]);
@@ -6235,8 +6239,10 @@ function extract(app, { describe: describe7 } = {}) {
6235
6239
  const elements = {};
6236
6240
  const pages = [];
6237
6241
  const pinsByPath = /* @__PURE__ */ new Map();
6242
+ let shell = null;
6238
6243
  for (const page of app.pages) {
6239
6244
  const built = readPage(app, page, spec, elements, problems);
6245
+ if (shell === null && built.shell !== null && built.shell !== void 0) shell = built.shell;
6240
6246
  if (built.page.owned !== true && !page.text.includes(COMPONENT_ATTR)) {
6241
6247
  readBundle(app, page.path, built.page.scripts, elements, problems);
6242
6248
  }
@@ -6299,7 +6305,7 @@ function extract(app, { describe: describe7 } = {}) {
6299
6305
  v: CONTRACT_V,
6300
6306
  generated_by: GENERATED_BY,
6301
6307
  source: describe7 ? "static+describe" : "static",
6302
- app: appSection(app, manifest),
6308
+ app: { ...appSection(app, manifest), ...shell === null ? {} : { shell } },
6303
6309
  pins: [...pinsByPath.values()].map((pin) => ({ ...pin, pages: [...new Set(pin.pages)].sort() })).sort((a, b) => a.order - b.order),
6304
6310
  pages,
6305
6311
  elements,
@@ -6481,14 +6487,21 @@ function readPage(app, page, spec, elements, problems) {
6481
6487
  ...bands.map((entry, index) => ({ at: entry.node.index, section: banded[index] })),
6482
6488
  ...wrappers.map((node, index) => ({ at: node.index, section: wrapped[index] }))
6483
6489
  ].sort((a, b) => a.at - b.at).map((one) => one.section);
6490
+ for (const entry of significant) {
6491
+ if (entry.type !== "region" || entry.node.attrs[HEADER_ACTIONS_ATTR] === void 0) continue;
6492
+ for (const child of childrenOf2.get(entry.node.index) ?? []) elements[uids.get(child)].section = "header";
6493
+ }
6484
6494
  const mount = readMount(scripts, problems);
6485
6495
  const body = tree.nodes.find((node) => node.tag === "body") ?? tree.nodes.find((node) => node.tag === "html");
6496
+ const main2 = tree.nodes.find((node) => node.tag === "main" && "data-dr-app" in node.attrs) ?? null;
6486
6497
  return {
6498
+ shell: shellOf(main2),
6487
6499
  page: {
6488
6500
  path: page.path,
6489
6501
  route: routeOf(app.manifest, page.path),
6490
6502
  title: titleOf(tree, page.text),
6491
6503
  header: headerOf(tree, page.text),
6504
+ ...navOf(tree, main2),
6492
6505
  sections,
6493
6506
  scripts: scripts.map((script) => script.path).filter((path3) => path3 !== page.path),
6494
6507
  theme: body?.attrs[THEME_ATTR] ?? null,
@@ -6751,20 +6764,43 @@ function headerOf(tree, text3) {
6751
6764
  }
6752
6765
  const region = tree.nodes.find((node) => node.attrs[KIT_ATTR] === "header");
6753
6766
  if (!region) return null;
6754
- const heading = firstTag(tree, region, "h1");
6767
+ const heading = firstTag(tree, region, "h1", true);
6755
6768
  if (!heading) return null;
6756
- const summary = firstTag(tree, region, "p");
6757
- return { title: textOf(text3, heading), summary: summary ? textOf(text3, summary) || null : null };
6769
+ const summary = firstTag(tree, region, "p", true);
6770
+ const out = { title: textOf(text3, heading), summary: summary ? textOf(text3, summary) || null : null };
6771
+ const actions = parseJsonAttr(region.attrs[HEADER_ACTIONS_ATTR]);
6772
+ if (Array.isArray(actions)) out.actions = actions;
6773
+ const from = region.attrs[TITLE_FROM_ATTR];
6774
+ if (from !== void 0 && from !== "") out.title_from = from;
6775
+ return out;
6758
6776
  }
6759
- function firstTag(tree, node, tag) {
6777
+ function firstTag(tree, node, tag, outer = false) {
6760
6778
  for (const child of node.children) {
6761
6779
  const candidate = tree.nodes[child];
6762
6780
  if (candidate.tag === tag) return candidate;
6763
- const found = firstTag(tree, candidate, tag);
6781
+ if (outer && (COMPONENT_ATTR in candidate.attrs || FIELD_ATTR in candidate.attrs)) continue;
6782
+ const found = firstTag(tree, candidate, tag, outer);
6764
6783
  if (found) return found;
6765
6784
  }
6766
6785
  return null;
6767
6786
  }
6787
+ function parseJsonAttr(value) {
6788
+ if (value === void 0 || value === "") return void 0;
6789
+ try {
6790
+ return JSON.parse(value);
6791
+ } catch {
6792
+ return void 0;
6793
+ }
6794
+ }
6795
+ function shellOf(main2) {
6796
+ const shell = parseJsonAttr(main2?.attrs[SHELL_ATTR]);
6797
+ return isPlainObject(shell) ? shell : null;
6798
+ }
6799
+ function navOf(tree, main2) {
6800
+ const nav = tree.nodes.find((node) => node.tag === "nav" && node.attrs[KIT_ATTR] === "nav" && node.attrs[NAV_COUNTS_ATTR] !== void 0);
6801
+ const source = nav?.attrs[NAV_COUNTS_ATTR] ?? main2?.attrs[NAV_COUNTS_ATTR];
6802
+ return source === void 0 || source === "" ? {} : { nav: { counts: { source } } };
6803
+ }
6768
6804
  function sectionOf(node, text3, index) {
6769
6805
  const title = textOf(text3, node);
6770
6806
  return { id: node.attrs.id ?? slug2(title) ?? `section_${index + 1}`, title };
@@ -9766,6 +9802,48 @@ var contract_schema_default = {
9766
9802
  },
9767
9803
  capabilities: { type: "object" },
9768
9804
  data_model: { oneOf: [{ type: "null" }, { $ref: "#/$defs/data_model" }] },
9805
+ shell: {
9806
+ type: "object",
9807
+ additionalProperties: false,
9808
+ description: `OPTIONAL, apps (dr-app-runtime@1.8.0, lib/shell.ts). The app shell: nav "rail" draws the page's nav as a 240px left rail, topbar true draws the product top bar (not when the page is framed). Absent, or nav "tabs" with no topbar, the page is 1.7.1's. Compiled as data-dr-shell (sorted JSON) on every page's <main data-dr-app>; the runtime reads it from the embedded contract.`,
9809
+ properties: {
9810
+ nav: { enum: ["tabs", "rail"], description: "tabs (absent is this): the row of page tabs; rail: the left rail" },
9811
+ topbar: { type: "boolean", description: "the 56px product top bar: nav_label (or name), Help/Settings when a help.html/settings.html page exists, the viewer's name" },
9812
+ rail: {
9813
+ type: "object",
9814
+ additionalProperties: false,
9815
+ properties: {
9816
+ title: { type: "string", description: "a small muted label above the entries" },
9817
+ entries: {
9818
+ type: "array",
9819
+ description: "absent: one entry per page, as the tab row draws them",
9820
+ items: {
9821
+ type: "object",
9822
+ required: ["label"],
9823
+ additionalProperties: false,
9824
+ properties: {
9825
+ label: { type: "string", minLength: 1 },
9826
+ page: { type: "string", description: "a page to link to (ui/requests.html)" },
9827
+ param: { type: "object", description: 'page params this entry sets through the param store ({"folder": "inbox"}); active while the store holds them all' },
9828
+ measure: { type: "string", minLength: 1, description: "a measure of pages[].nav.counts.source, drawn as a count (0 is not drawn)" },
9829
+ role: { type: "string", minLength: 1, description: "dr-app-runtime@1.8.1: the app roles this entry is drawn for, any of them, space-separated like data-dr-role. Absent, the runtime gates an entry with page by that page's pages[].roles, and a param entry by its target page's roles; a viewer none of them allows does not see the entry" }
9830
+ }
9831
+ }
9832
+ },
9833
+ footer: {
9834
+ type: "array",
9835
+ description: "links pinned to the bottom of the rail",
9836
+ items: {
9837
+ type: "object",
9838
+ required: ["label", "page"],
9839
+ additionalProperties: false,
9840
+ properties: { label: { type: "string", minLength: 1 }, page: { type: "string" }, role: { type: "string", minLength: 1, description: "dr-app-runtime@1.8.1: the roles this link is drawn for (space-separated, any of them); absent, the page's pages[].roles gate it" } }
9841
+ }
9842
+ }
9843
+ }
9844
+ }
9845
+ }
9846
+ },
9769
9847
  theme: {
9770
9848
  oneOf: [
9771
9849
  { type: "null" },
@@ -9817,7 +9895,9 @@ var contract_schema_default = {
9817
9895
  description: `The words at the top of the page, as a reader sees them: a dashboard's app bar (.dr-appbar__title over .dr-appbar__summary) or an app's header region (data-dr-kit="header" around an h1 and a p). Not <title>, which is the browser tab. OPTIONAL: a contract written before this key existed omits it.`,
9818
9896
  properties: {
9819
9897
  title: { type: "string" },
9820
- summary: { type: ["string", "null"], description: "the one line under the title, or null where the page writes none" }
9898
+ summary: { type: ["string", "null"], description: "the one line under the title, or null where the page writes none" },
9899
+ actions: { type: "array", minItems: 1, items: { type: "string", minLength: 1 }, description: 'OPTIONAL, apps (dr-app-runtime@1.8.0). The ids (props.id) of button or dialog elements on this page drawn as a right-aligned row beside the title, in this order, and not in their band. Compiled inside the header region (data-dr-header-actions), each element with section "header".' },
9900
+ title_from: { enum: ["rail"], description: "OPTIONAL, apps (dr-app-runtime@1.8.0). rail: the title is the active rail entry's label, falling back to title. data-dr-title-from on the header region." }
9821
9901
  }
9822
9902
  }
9823
9903
  ]
@@ -9871,7 +9951,7 @@ var contract_schema_default = {
9871
9951
  additionalProperties: false,
9872
9952
  properties: {
9873
9953
  name: { type: "string" },
9874
- kind: { type: "string", enum: ["period", "multi", "select", "slider", "number", "toggle", "month"] },
9954
+ kind: { type: "string", enum: ["period", "multi", "select", "slider", "number", "toggle", "month", "text", "date"], description: "text (any string) and date (a YYYY-MM-DD day, or '') from dr-app-runtime@1.8.0" },
9875
9955
  label: { type: "string", description: "OPTIONAL. Absent falls back to name." },
9876
9956
  min: { type: "number", description: "OPTIONAL. slider/number only." },
9877
9957
  max: { type: "number", description: "OPTIONAL. slider/number only." },
@@ -9887,9 +9967,25 @@ var contract_schema_default = {
9887
9967
  { properties: { kind: { const: "slider" }, default: { type: "number" } } },
9888
9968
  { properties: { kind: { const: "number" }, default: { type: "number" } } },
9889
9969
  { properties: { kind: { const: "toggle" }, default: { type: "boolean" } } },
9890
- { properties: { kind: { const: "month" }, default: { type: "string" } } }
9970
+ { properties: { kind: { const: "month" }, default: { type: "string" } } },
9971
+ { properties: { kind: { const: "text" }, default: { type: "string" } } },
9972
+ { properties: { kind: { const: "date" }, default: { type: "string", pattern: "^([0-9]{4}-[0-9]{2}-[0-9]{2})?$" } } }
9891
9973
  ]
9892
9974
  }
9975
+ },
9976
+ nav: {
9977
+ type: "object",
9978
+ required: ["counts"],
9979
+ additionalProperties: false,
9980
+ description: "OPTIONAL, apps (dr-app-runtime@1.8.0). counts.source is a summary handler the runtime calls once with {} (and again on the page's refresh): its measures fill the rail entries' counts (app.shell.rail.entries[].measure). data-dr-nav-counts on the page's nav (on <main> when it has none).",
9981
+ properties: {
9982
+ counts: {
9983
+ type: "object",
9984
+ required: ["source"],
9985
+ additionalProperties: false,
9986
+ properties: { source: { type: "string", minLength: 1 } }
9987
+ }
9988
+ }
9893
9989
  }
9894
9990
  }
9895
9991
  },
@@ -11139,11 +11235,11 @@ var MAP = [
11139
11235
  ["--card-foreground", "colour", "--dr-n-900"],
11140
11236
  ["--primary", "colour", "--dr-accent"],
11141
11237
  ["--primary-foreground", "contrast", "--dr-accent"],
11142
- ["--secondary", "colour", "--dr-n-100"],
11238
+ ["--secondary", "colour", "--dr-n-50"],
11143
11239
  ["--secondary-foreground", "colour", "--dr-n-700"],
11144
11240
  ["--muted", "colour", "--dr-n-100"],
11145
11241
  ["--muted-foreground", "colour", "--dr-n-500"],
11146
- ["--accent", "colour", "--dr-n-100"],
11242
+ ["--accent", "colour", "--dr-n-50"],
11147
11243
  ["--accent-foreground", "colour", "--dr-n-900"],
11148
11244
  ["--destructive", "colour", "--dr-negative"],
11149
11245
  ["--destructive-foreground", "contrast", "--dr-negative"],
@@ -12036,6 +12132,325 @@ function isPlatformChartColour(n, value) {
12036
12132
  return colour !== "" && (PLATFORM_CHART[n - 1] === colour || APP_CHART_ORDER[n - 1] === colour);
12037
12133
  }
12038
12134
 
12135
+ // ../flows/insights-builder/kit/contract/look.mjs
12136
+ var look_exports = {};
12137
+ __export(look_exports, {
12138
+ HEADER_ACTION_TYPES: () => HEADER_ACTION_TYPES,
12139
+ headerActionProblems: () => headerActionProblems,
12140
+ humanise: () => humanise,
12141
+ layoutProblems: () => layoutProblems,
12142
+ lookPages: () => lookPages,
12143
+ navCountProblems: () => navCountProblems,
12144
+ railReachProblems: () => railReachProblems,
12145
+ rootsOf: () => rootsOf,
12146
+ untitledCards: () => untitledCards
12147
+ });
12148
+ var isPlainObject5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
12149
+ var named4 = (value) => typeof value === "string" && value !== "";
12150
+ function humanise(name) {
12151
+ const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
12152
+ return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
12153
+ }
12154
+ var WORDING = {
12155
+ custom: { noun: "card", fallback: (p) => [humanise(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
12156
+ chart: { noun: "card", fallback: (p) => [humanise(String(p.y ?? "")), "its y column"] },
12157
+ widget: { noun: "card", fallback: (p) => [humanise(named4(p.source) ? p.source : "widget"), "its source"] },
12158
+ stat: { noun: "tile", fallback: (p) => [humanise(String(p.measure ?? "")), "its measure"] },
12159
+ list: { noun: "card", fallback: (p) => [humanise(named4(p.entity) ? p.entity : "list"), "its entity"] },
12160
+ queue: { noun: "card", fallback: (p) => [humanise(named4(p.entity) ? p.entity : "queue"), "its entity"] },
12161
+ form: { noun: "card", fallback: () => ["New", "the runtime\u2019s default"] },
12162
+ dialog: { noun: "dialog" }
12163
+ };
12164
+ var TITLED = Object.fromEntries(
12165
+ TITLED_TYPES.map((type) => [type, { noun: CONTROL_PARTS.includes(type) ? "input" : "card", fallback: () => [humanise(type), "its type"], ...WORDING[type] }])
12166
+ );
12167
+ var LAYOUT = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
12168
+ function rootsOf(contract, page) {
12169
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12170
+ if (Array.isArray(page?.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid] !== void 0);
12171
+ const contained = /* @__PURE__ */ new Set();
12172
+ for (const element of Object.values(elements)) for (const child of element?.children ?? []) contained.add(child);
12173
+ return Object.keys(elements).filter((uid) => {
12174
+ const element = elements[uid];
12175
+ return element?.page === page?.path && (element.parent === null || element.parent === void 0) && !contained.has(uid);
12176
+ });
12177
+ }
12178
+ function untitledCards(contract) {
12179
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12180
+ const out = [];
12181
+ for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
12182
+ if (page?.owned === true) continue;
12183
+ const seen = /* @__PURE__ */ new Set();
12184
+ const drawn2 = [];
12185
+ const walk4 = (uids) => {
12186
+ for (const uid of uids) {
12187
+ const element = elements[uid];
12188
+ if (element === void 0 || seen.has(uid)) continue;
12189
+ seen.add(uid);
12190
+ if (LAYOUT.has(element.type ?? "")) {
12191
+ const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
12192
+ walk4(children);
12193
+ } else {
12194
+ drawn2.push(uid);
12195
+ }
12196
+ }
12197
+ };
12198
+ walk4(rootsOf(contract, page));
12199
+ for (const uid of drawn2) {
12200
+ const element = elements[uid];
12201
+ const type = element.type ?? "";
12202
+ const titled = TITLED[type];
12203
+ if (titled === void 0) continue;
12204
+ const props = isPlainObject5(element.props) ? element.props : {};
12205
+ const typed = isPlainObject5(props.props) ? props.props : {};
12206
+ const card = isPlainObject5(element.card) ? element.card : isPlainObject5(props.card) ? props.card : {};
12207
+ if (named4(props.label) || named4(card.title)) continue;
12208
+ if (type === "dialog" ? named4(typed.title) : named4(typed.label)) continue;
12209
+ const id = named4(element.id) ? element.id : named4(props.id) ? props.id : null;
12210
+ const [title, from] = named4(props.field) ? [humanise(props.field), "its field"] : id !== null ? [humanise(id), "its id"] : titled.fallback(props);
12211
+ const what = id !== null ? `${type} "${id}"` : `the ${type} ${uid}`;
12212
+ const shown2 = title === "" ? "has no title at all" : `is titled "${title}" after ${from}`;
12213
+ out.push({ page: page.path, uid, type, id, noun: titled.noun, title, from, message: `${what} has no label, so its ${titled.noun} ${shown2}` });
12214
+ }
12215
+ }
12216
+ return out;
12217
+ }
12218
+ var BAND_LOOK2 = ["subtitle", "layout", "surface", "aside"];
12219
+ function lookPages(contract) {
12220
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12221
+ const out = /* @__PURE__ */ new Set();
12222
+ for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
12223
+ if (page?.owned === true || !named4(page?.path)) continue;
12224
+ const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK2.some((key) => named4(section?.[key])));
12225
+ const cards = Object.values(elements).some((element) => element?.page === page.path && (isPlainObject5(element.card) || named4(element.emphasis)));
12226
+ if (bands || cards) out.add(page.path);
12227
+ }
12228
+ return out;
12229
+ }
12230
+ var SPAN_LAYOUTS = /* @__PURE__ */ new Set([void 0, "grid"]);
12231
+ var CONTROL_TYPES = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS]);
12232
+ function bandsOf(contract, page) {
12233
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12234
+ const roots = rootsOf(contract, page);
12235
+ return (Array.isArray(page.sections) ? page.sections : []).filter((section) => named4(section?.id)).map((section) => ({ section, roots: roots.filter((uid) => elements[uid]?.section === section.id) }));
12236
+ }
12237
+ function nameOf3(uid, element) {
12238
+ const id = named4(element?.props?.id) ? element.props.id : named4(element?.id) ? element.id : null;
12239
+ return id === null ? `the ${element?.type ?? "element"} ${uid}` : `${element.type} "${id}"`;
12240
+ }
12241
+ function isControl(element, moduleSource) {
12242
+ const props = isPlainObject5(element?.props) ? element.props : {};
12243
+ if (CONTROL_TYPES.has(element?.type)) return true;
12244
+ if (element?.type !== "custom" && element?.type !== "action" && named4(props.param)) return true;
12245
+ if (isPlainObject5(props.props) && named4(props.props.param)) return true;
12246
+ if (element?.type === "custom" && named4(props.module)) {
12247
+ const source = moduleSource(props.module);
12248
+ return typeof source === "string" && scanUiSetParams(source).length > 0;
12249
+ }
12250
+ return false;
12251
+ }
12252
+ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
12253
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12254
+ const looked = lookPages(contract);
12255
+ const out = [];
12256
+ for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
12257
+ if (!looked.has(page?.path)) continue;
12258
+ for (const { section, roots } of bandsOf(contract, page)) {
12259
+ const band = `band "${section.id}" on ${page.path}`;
12260
+ const layout = named4(section.layout) ? section.layout : void 0;
12261
+ if (layout === "kpi_strip") {
12262
+ for (const uid of roots) {
12263
+ const element = elements[uid];
12264
+ if (element.type === "stat" || element.type === "widget" && element.props?.kind === "kpi") continue;
12265
+ out.push({
12266
+ rule: "L1",
12267
+ page: page.path,
12268
+ section: section.id,
12269
+ uid,
12270
+ message: `${band} is a kpi_strip, which draws equal KPI tiles, and holds ${nameOf3(uid, element)}; move it to a band of its own, or make it a stat (or a kpi widget)`
12271
+ });
12272
+ }
12273
+ }
12274
+ if (layout === "sidebar" && roots.length > 0) {
12275
+ const uid = section.aside === "start" ? roots[0] : roots[roots.length - 1];
12276
+ if (!isControl(elements[uid], moduleSource)) {
12277
+ out.push({
12278
+ rule: "L2",
12279
+ page: page.path,
12280
+ section: section.id,
12281
+ uid,
12282
+ message: `${band} is a sidebar whose aside (its ${section.aside === "start" ? "first" : "last"} element) is ${nameOf3(uid, elements[uid])}; the narrow column holds the controls that turn the page \u2014 a filter, an input, a part in param mode, a custom that calls ui.setParam \u2014 so put one there, or pick another layout`
12283
+ });
12284
+ }
12285
+ }
12286
+ if (SPAN_LAYOUTS.has(layout)) {
12287
+ let row = [];
12288
+ let used = 0;
12289
+ for (const uid of roots) {
12290
+ const raw = elements[uid]?.grid?.span;
12291
+ const span = raw === null || raw === void 0 ? 12 : Number(raw);
12292
+ if (!Number.isInteger(span) || span < 1 || span > 12) {
12293
+ out.push({ rule: "L4", page: page.path, section: section.id, uid, message: `${band}: ${nameOf3(uid, elements[uid])} spans ${JSON.stringify(raw)}, and a span is a whole number of the twelve columns, 1 to 12` });
12294
+ continue;
12295
+ }
12296
+ if (used + span > 12) {
12297
+ if (used < 12) {
12298
+ const spans = [...row.map((one) => one.span), span];
12299
+ out.push({
12300
+ rule: "L4",
12301
+ page: page.path,
12302
+ section: section.id,
12303
+ uid,
12304
+ message: `${band}: ${row.map((one) => nameOf3(one.uid, elements[one.uid])).join(" + ")} + ${nameOf3(uid, elements[uid])} span ${spans.join(" + ")} = ${spans.reduce((a, b) => a + b, 0)} on one row of twelve, so the last wraps and leaves ${12 - used} column${12 - used === 1 ? "" : "s"} of the row empty; make the row's spans add up to 12`
12305
+ });
12306
+ }
12307
+ row = [];
12308
+ used = 0;
12309
+ }
12310
+ row.push({ uid, span });
12311
+ used += span;
12312
+ if (used === 12) {
12313
+ row = [];
12314
+ used = 0;
12315
+ }
12316
+ }
12317
+ }
12318
+ }
12319
+ }
12320
+ return out;
12321
+ }
12322
+ var HEADER_ACTION_TYPES = ["button", "dialog"];
12323
+ function headerActionProblems(contract) {
12324
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12325
+ const out = [];
12326
+ for (const [pageIndex, page] of (Array.isArray(contract?.pages) ? contract.pages : []).entries()) {
12327
+ if (page?.owned === true || !isPlainObject5(page?.header) || page.header.actions === void 0) continue;
12328
+ const say = (index, id, uid, message) => out.push({ rule: "L5", code: "header_action_unknown", page: page.path, pageIndex, index, id, uid, message });
12329
+ const actions = page.header.actions;
12330
+ if (!Array.isArray(actions) || actions.length === 0) {
12331
+ say(null, actions, null, `${page.path} states header.actions as ${JSON.stringify(actions)}; it is a list of the ids of the buttons and dialogs drawn beside the title`);
12332
+ continue;
12333
+ }
12334
+ const seen = /* @__PURE__ */ new Set();
12335
+ for (const [index, id] of actions.entries()) {
12336
+ if (!named4(id)) {
12337
+ say(index, id, null, `${page.path}'s header.actions[${index}] is ${JSON.stringify(id)}; each entry is an element id (props.id) on the page`);
12338
+ continue;
12339
+ }
12340
+ if (seen.has(id)) {
12341
+ say(index, id, null, `${page.path}'s header.actions lists "${id}" twice; the runtime draws it once`);
12342
+ continue;
12343
+ }
12344
+ seen.add(id);
12345
+ const hits = Object.keys(elements).filter((uid) => {
12346
+ const element2 = elements[uid];
12347
+ return element2?.page === page.path && element2.type !== "region" && (named4(element2.props?.id) ? element2.props.id : element2.id) === id;
12348
+ });
12349
+ if (hits.length === 0) {
12350
+ say(index, id, null, `${page.path}'s header.actions names "${id}", and no element on the page has that id, so nothing is drawn beside the title; name a button or dialog of this page by its props.id`);
12351
+ continue;
12352
+ }
12353
+ const element = elements[hits[0]];
12354
+ if (!HEADER_ACTION_TYPES.includes(element.type)) {
12355
+ say(index, id, hits[0], `${page.path}'s header.actions names ${nameOf3(hits[0], element)}, and a header action is a ${HEADER_ACTION_TYPES.join(" or a ")}; make it one, or leave it in its band`);
12356
+ }
12357
+ }
12358
+ }
12359
+ return out;
12360
+ }
12361
+ function navCountProblems(contract) {
12362
+ const out = [];
12363
+ const handlers = isPlainObject5(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
12364
+ const pages = (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true);
12365
+ const entries = Array.isArray(contract?.app?.shell?.rail?.entries) ? contract.app.shell.rail.entries : [];
12366
+ const measured = entries.map((entry, index) => ({ entry, index })).filter(({ entry }) => named4(entry?.measure));
12367
+ const counted = pages.filter((page) => isPlainObject5(page.nav) && page.nav.counts !== void 0);
12368
+ if (counted.length === 0) {
12369
+ if (measured.length > 0) {
12370
+ out.push({
12371
+ rule: "L6",
12372
+ page: null,
12373
+ where: "app.shell.rail.entries",
12374
+ message: `the rail's entries name measures (${measured.map(({ entry }) => JSON.stringify(entry.measure)).join(", ")}) and no page states nav.counts.source, so no count is ever drawn; state the summary handler on the pages that draw the rail`
12375
+ });
12376
+ }
12377
+ return out;
12378
+ }
12379
+ const measuresChecked = /* @__PURE__ */ new Set();
12380
+ for (const page of counted) {
12381
+ const source = page.nav.counts?.source;
12382
+ const where2 = `${page.path} nav.counts.source`;
12383
+ if (!named4(source)) {
12384
+ out.push({ rule: "L6", page: page.path, where: where2, message: `${where2} is ${JSON.stringify(source ?? null)}; it names the summary handler whose measures are the rail's counts` });
12385
+ continue;
12386
+ }
12387
+ const handler = handlers[source];
12388
+ if (!isPlainObject5(handler) || handler.role !== "summary") {
12389
+ out.push({
12390
+ rule: "L6",
12391
+ page: page.path,
12392
+ where: where2,
12393
+ message: `${where2} is "${source}", which is ${isPlainObject5(handler) ? `a ${JSON.stringify(handler.role ?? null)} handler` : "not a handler of this app"}; the rail's counts are one number per measure, which a summary handler answers`
12394
+ });
12395
+ continue;
12396
+ }
12397
+ if (!isPlainObject5(handler.measures) || measuresChecked.has(source)) continue;
12398
+ measuresChecked.add(source);
12399
+ for (const { entry, index } of measured) {
12400
+ if (Object.prototype.hasOwnProperty.call(handler.measures, entry.measure)) continue;
12401
+ out.push({
12402
+ rule: "L6",
12403
+ page: page.path,
12404
+ where: `app.shell.rail.entries[${index}].measure`,
12405
+ message: `rail entry "${entry.label}" counts measure "${entry.measure}", which "${source}" (${page.path} nav.counts.source) does not answer (it has ${Object.keys(handler.measures).map((one) => JSON.stringify(one)).join(", ")}), so its count is never drawn`
12406
+ });
12407
+ }
12408
+ }
12409
+ return out;
12410
+ }
12411
+ function appRoles(contract) {
12412
+ const raw = Array.isArray(contract?.backend?.roles) ? contract.backend.roles : [];
12413
+ return raw.map((role) => typeof role === "string" ? { name: role, sees: [] } : { name: role?.name, sees: Array.isArray(role?.sees) ? role.sees.filter(named4) : [] }).filter((role) => named4(role.name));
12414
+ }
12415
+ function railReachProblems(contract) {
12416
+ const out = [];
12417
+ const rail = contract?.app?.shell?.rail;
12418
+ if (!isPlainObject5(rail)) return out;
12419
+ const roles = appRoles(contract);
12420
+ if (roles.length === 0) return out;
12421
+ const declared = new Map(roles.map((role) => [role.name, role]));
12422
+ const pages = new Map((Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => named4(page?.path)).map((page) => [page.path, page]));
12423
+ const openers = (page) => Array.isArray(page?.roles) && page.roles.filter(named4).length > 0 ? page.roles.filter(named4) : null;
12424
+ const canOpen = (name, page) => {
12425
+ const open2 = openers(page);
12426
+ if (open2 === null) return true;
12427
+ const role = declared.get(name);
12428
+ return open2.includes(name) || (role?.sees ?? []).some((seen) => open2.includes(seen));
12429
+ };
12430
+ for (const key of ["entries", "footer"]) {
12431
+ for (const [index, entry] of (Array.isArray(rail[key]) ? rail[key] : []).entries()) {
12432
+ if (!isPlainObject5(entry) || !named4(entry.label) || !named4(entry.page)) continue;
12433
+ const page = pages.get(entry.page);
12434
+ if (page === void 0) continue;
12435
+ const where2 = `app.shell.rail.${key}[${index}]`;
12436
+ const viewers = named4(entry.role) ? entry.role.split(/\s+/).filter(named4) : openers(page) ?? roles.map((role) => role.name);
12437
+ const known = viewers.filter((name) => declared.has(name));
12438
+ if (known.length === 0) {
12439
+ out.push({ rule: "L7", where: where2, message: `rail ${key === "footer" ? "footer link" : "entry"} "${entry.label}" is drawn for ${viewers.map((one) => JSON.stringify(one)).join(", ")}, and the app declares none of them (${roles.map((role) => JSON.stringify(role.name)).join(", ")}), so nobody sees it` });
12440
+ continue;
12441
+ }
12442
+ if (!known.some((name) => canOpen(name, page))) {
12443
+ out.push({
12444
+ rule: "L7",
12445
+ where: where2,
12446
+ message: `rail ${key === "footer" ? "footer link" : "entry"} "${entry.label}" links to ${entry.page}, which ${known.map((one) => JSON.stringify(one)).join(", ")} may not open (it is for ${openers(page).map((one) => JSON.stringify(one)).join(", ")}), so the entry is dead; give it the page's roles, or link a page they may open`
12447
+ });
12448
+ }
12449
+ }
12450
+ }
12451
+ return out;
12452
+ }
12453
+
12039
12454
  // ../flows/insights-builder/kit/contract/platformBadges.mjs
12040
12455
  var PLATFORM_BADGE_COLOURS = Object.freeze([
12041
12456
  ["--dr-color-additional-blue", "#0061ff"],
@@ -12092,7 +12507,7 @@ function badgesCss() {
12092
12507
  }
12093
12508
 
12094
12509
  // ../flows/insights-builder/kit/contract/compileApp.mjs
12095
- var RUNTIME = "dr-app-runtime@1.7.1";
12510
+ var RUNTIME = "dr-app-runtime@1.8.1";
12096
12511
  var UI_KIT2 = "ui-kit/v4";
12097
12512
  var CHARTS = "echarts@5.6.0";
12098
12513
  var FMT = "dr-fmt@2.0.1";
@@ -12122,12 +12537,12 @@ var REQUIRED_KEYS = {
12122
12537
  var KEY_SHAPE = {
12123
12538
  table: {
12124
12539
  reads: 'HANDLER["table"]',
12125
- ok: (value) => named4(value),
12540
+ ok: (value) => named5(value),
12126
12541
  says: "the name of the table it is over, as a string"
12127
12542
  },
12128
12543
  fields: {
12129
12544
  reads: 'HANDLER["fields"]',
12130
- ok: (value) => Array.isArray(value) && value.length > 0 && value.every(named4),
12545
+ ok: (value) => Array.isArray(value) && value.length > 0 && value.every(named5),
12131
12546
  says: "a non-empty list of the columns it takes off the payload"
12132
12547
  },
12133
12548
  // Two spellings. The ARRAY names the values the status column may be moved
@@ -12137,27 +12552,27 @@ var KEY_SHAPE = {
12137
12552
  // it was not told to.
12138
12553
  decision: {
12139
12554
  reads: 'HANDLER["decision"]',
12140
- ok: (value) => Array.isArray(value) && value.length > 0 && value.every(named4) || isPlainObject5(value) && Object.keys(value).length > 0 && Object.keys(value).every(named4) && Object.values(value).every((spec) => isPlainObject5(spec) && named4(spec.to)),
12555
+ ok: (value) => Array.isArray(value) && value.length > 0 && value.every(named5) || isPlainObject6(value) && Object.keys(value).length > 0 && Object.keys(value).every(named5) && Object.values(value).every((spec) => isPlainObject6(spec) && named5(spec.to)),
12141
12556
  says: "a non-empty list of the values this decision can write, or an object of decision name to {to, note?, set?}"
12142
12557
  },
12143
12558
  guard: {
12144
12559
  reads: 'HANDLER["guard"]',
12145
- ok: (value) => named4(value),
12560
+ ok: (value) => named5(value),
12146
12561
  says: "the one state a row may be acted on from, as a string"
12147
12562
  },
12148
12563
  requires: {
12149
12564
  reads: "REQUIRES",
12150
- ok: (value) => isPlainObject5(value) && Object.keys(value).length > 0 && Object.values(value).every((conditions) => isPlainObject5(conditions) && Object.keys(conditions).length > 0),
12565
+ ok: (value) => isPlainObject6(value) && Object.keys(value).length > 0 && Object.values(value).every((conditions) => isPlainObject6(conditions) && Object.keys(conditions).length > 0),
12151
12566
  says: 'an object of ref column to {column of the row it names: the value it must hold, or {"not": the value it must not}}'
12152
12567
  },
12153
12568
  measures: {
12154
12569
  reads: 'HANDLER["measures"].items()',
12155
- ok: (value) => isPlainObject5(value) && Object.keys(value).length > 0,
12570
+ ok: (value) => isPlainObject6(value) && Object.keys(value).length > 0,
12156
12571
  says: "a non-empty object of measure name to what it answers: an equality filter it counts, or {count: true | sum | avg | avg_days | count_events: true, where}"
12157
12572
  },
12158
12573
  by: {
12159
12574
  reads: 'HANDLER["by"]',
12160
- ok: (value) => named4(value),
12575
+ ok: (value) => named5(value),
12161
12576
  says: "the ref column the rows are filtered by, as a string"
12162
12577
  },
12163
12578
  not_own: {
@@ -12221,11 +12636,11 @@ function isNumberType(type) {
12221
12636
  return typeof type === "string" && (NUMBER_TYPES.has(type) || DECIMAL_RE.test(type));
12222
12637
  }
12223
12638
  function statesTopLevelId(element) {
12224
- return isPlainObject5(element) && named4(element.id);
12639
+ return isPlainObject6(element) && named5(element.id);
12225
12640
  }
12226
12641
  function propsId(element) {
12227
12642
  const props = element?.props;
12228
- return isPlainObject5(props) && named4(props.id) ? props.id : void 0;
12643
+ return isPlainObject6(props) && named5(props.id) ? props.id : void 0;
12229
12644
  }
12230
12645
  function foldStatedIds(elements) {
12231
12646
  const folding = Object.keys(elements).filter((uid) => {
@@ -12238,31 +12653,31 @@ function foldStatedIds(elements) {
12238
12653
  const out = { ...elements };
12239
12654
  for (const uid of folding) {
12240
12655
  const element = out[uid];
12241
- out[uid] = { ...element, id: null, props: { id: element.id, ...isPlainObject5(element.props) ? element.props : {} } };
12656
+ out[uid] = { ...element, id: null, props: { id: element.id, ...isPlainObject6(element.props) ? element.props : {} } };
12242
12657
  }
12243
12658
  return out;
12244
12659
  }
12245
12660
  function roleListOf(element) {
12246
- const role = isPlainObject5(element?.props) ? element.props.role : void 0;
12661
+ const role = isPlainObject6(element?.props) ? element.props.role : void 0;
12247
12662
  return Array.isArray(role) ? role : null;
12248
12663
  }
12249
12664
  function roleListCoversHandler(element, handlers) {
12250
12665
  const listed = roleListOf(element);
12251
- if (listed === null || element?.type !== "action" || !listed.every(named4)) return false;
12666
+ if (listed === null || element?.type !== "action" || !listed.every(named5)) return false;
12252
12667
  const gate = handlers?.[element.props?.action]?.requiresRole;
12253
12668
  return Array.isArray(gate) && gate.length > 0 && gate.every((role) => listed.includes(role));
12254
12669
  }
12255
12670
  function foldRoleLists(elements, handlers = {}) {
12256
12671
  const folding = Object.keys(elements).filter((uid) => {
12257
12672
  const listed = roleListOf(elements[uid]);
12258
- return listed !== null && (listed.length === 1 && named4(listed[0]) || roleListCoversHandler(elements[uid], handlers));
12673
+ return listed !== null && (listed.length === 1 && named5(listed[0]) || roleListCoversHandler(elements[uid], handlers));
12259
12674
  });
12260
12675
  if (folding.length === 0) return elements;
12261
12676
  const out = { ...elements };
12262
12677
  for (const uid of folding) {
12263
12678
  const element = out[uid];
12264
12679
  const { role, ...rest } = element.props;
12265
- out[uid] = { ...element, props: role.length === 1 && named4(role[0]) ? { ...element.props, role: role[0] } : rest };
12680
+ out[uid] = { ...element, props: role.length === 1 && named5(role[0]) ? { ...element.props, role: role[0] } : rest };
12266
12681
  }
12267
12682
  return out;
12268
12683
  }
@@ -12270,7 +12685,7 @@ function roleListProblems(elements, handlers = {}) {
12270
12685
  const problems = [];
12271
12686
  for (const uid of Object.keys(elements).sort(natural3)) {
12272
12687
  const listed = roleListOf(elements[uid]);
12273
- if (listed === null || listed.length === 1 && named4(listed[0]) || roleListCoversHandler(elements[uid], handlers)) continue;
12688
+ if (listed === null || listed.length === 1 && named5(listed[0]) || roleListCoversHandler(elements[uid], handlers)) continue;
12274
12689
  problems.push({
12275
12690
  code: "app_model_mismatch",
12276
12691
  pointer: `/elements/${escapePointer3(uid)}/props/role`,
@@ -12282,7 +12697,7 @@ function roleListProblems(elements, handlers = {}) {
12282
12697
  function foldHeaderRoles(elements) {
12283
12698
  const folding = Object.keys(elements).filter((uid) => {
12284
12699
  const element = elements[uid];
12285
- return isPlainObject5(element) && element.type === "region" && (element.role === void 0 || element.role === null);
12700
+ return isPlainObject6(element) && element.type === "region" && (element.role === void 0 || element.role === null);
12286
12701
  });
12287
12702
  if (folding.length === 0) return elements;
12288
12703
  const out = { ...elements };
@@ -12292,7 +12707,7 @@ function foldHeaderRoles(elements) {
12292
12707
  function foldNavRegions(elements) {
12293
12708
  const folding = Object.keys(elements).filter((uid) => {
12294
12709
  const element = elements[uid];
12295
- return isPlainObject5(element) && element.type === "region" && element.role === "nav";
12710
+ return isPlainObject6(element) && element.type === "region" && element.role === "nav";
12296
12711
  });
12297
12712
  if (folding.length === 0) return elements;
12298
12713
  const out = { ...elements };
@@ -12303,8 +12718,8 @@ function foldDialogTriggers(elements) {
12303
12718
  const words = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
12304
12719
  const folding = Object.keys(elements).filter((uid) => {
12305
12720
  const element = elements[uid];
12306
- if (!isPlainObject5(element) || element.type !== "dialog" || !isPlainObject5(element.props)) return false;
12307
- const typed = isPlainObject5(element.props.props) ? element.props.props : {};
12721
+ if (!isPlainObject6(element) || element.type !== "dialog" || !isPlainObject6(element.props)) return false;
12722
+ const typed = isPlainObject6(element.props.props) ? element.props.props : {};
12308
12723
  if (words(typed.trigger) || typed.values_from === "row") return false;
12309
12724
  return Boolean(words(element.props.label) ?? words(typed.title));
12310
12725
  });
@@ -12312,7 +12727,7 @@ function foldDialogTriggers(elements) {
12312
12727
  const out = { ...elements };
12313
12728
  for (const uid of folding) {
12314
12729
  const element = out[uid];
12315
- const typed = isPlainObject5(element.props.props) ? element.props.props : {};
12730
+ const typed = isPlainObject6(element.props.props) ? element.props.props : {};
12316
12731
  const trigger = words(element.props.label) ?? words(typed.title);
12317
12732
  out[uid] = { ...element, props: { ...element.props, props: { ...typed, trigger } } };
12318
12733
  }
@@ -12322,10 +12737,10 @@ function headerPropsProblems(elements) {
12322
12737
  const problems = [];
12323
12738
  for (const uid of Object.keys(elements).sort(natural3)) {
12324
12739
  const element = elements[uid];
12325
- if (!isPlainObject5(element) || element.type !== "region") continue;
12740
+ if (!isPlainObject6(element) || element.type !== "region") continue;
12326
12741
  if ((element.role ?? "header") !== "header") continue;
12327
12742
  if (element.props === void 0 || element.props === null) continue;
12328
- if (isPlainObject5(element.props) && Object.keys(element.props).length === 0) continue;
12743
+ if (isPlainObject6(element.props) && Object.keys(element.props).length === 0) continue;
12329
12744
  problems.push({
12330
12745
  code: "app_model_mismatch",
12331
12746
  pointer: `/elements/${escapePointer3(uid)}/props`,
@@ -12350,7 +12765,7 @@ function statedIdProblems(elements) {
12350
12765
  return problems;
12351
12766
  }
12352
12767
  function specMapProblems(widgets) {
12353
- if (!isPlainObject5(widgets)) return [];
12768
+ if (!isPlainObject6(widgets)) return [];
12354
12769
  return Object.keys(widgets).sort(natural3).map((id) => ({
12355
12770
  code: "app_model_mismatch",
12356
12771
  pointer: `/data/widgets/${escapePointer3(id)}`,
@@ -12363,14 +12778,14 @@ var SOURCE_TABLE_KEYS = ["where", "order_by", "limit"];
12363
12778
  function originOf(row) {
12364
12779
  const from = row?.from;
12365
12780
  if (Array.isArray(from)) return "depends";
12366
- if (!isPlainObject5(from)) return null;
12781
+ if (!isPlainObject6(from)) return null;
12367
12782
  for (const spelling of ["financeos", "table", "call"]) {
12368
12783
  if (Object.prototype.hasOwnProperty.call(from, spelling)) return spelling;
12369
12784
  }
12370
12785
  return null;
12371
12786
  }
12372
12787
  function sourcesOf(contract) {
12373
- return isPlainObject5(contract?.data?.sources) ? contract.data.sources : {};
12788
+ return isPlainObject6(contract?.data?.sources) ? contract.data.sources : {};
12374
12789
  }
12375
12790
  var FINANCEOS_SCOPES = {
12376
12791
  commands: {
@@ -12414,7 +12829,7 @@ function financeosNeeds(row, origin) {
12414
12829
  }
12415
12830
  return needs;
12416
12831
  }
12417
- const read = isPlainObject5(row.from.financeos) ? row.from.financeos : {};
12832
+ const read = isPlainObject6(row.from.financeos) ? row.from.financeos : {};
12418
12833
  const filterable = Array.isArray(row.filterable) && row.filterable.length > 0;
12419
12834
  if (read.metric_id !== void 0 && read.metric_id !== null) {
12420
12835
  if (read.drill_rows) need("data_models", scopesGranting("namespaces", "data_models"));
@@ -12462,7 +12877,7 @@ function servedSources(elements, sources) {
12462
12877
  const out = /* @__PURE__ */ new Set();
12463
12878
  for (const element of Object.values(elements)) {
12464
12879
  const name = element?.props?.source;
12465
- if (named4(name) && Object.prototype.hasOwnProperty.call(sources, name)) out.add(name);
12880
+ if (named5(name) && Object.prototype.hasOwnProperty.call(sources, name)) out.add(name);
12466
12881
  }
12467
12882
  return [...out].sort(natural3);
12468
12883
  }
@@ -12474,7 +12889,7 @@ function sourceProblems(sources, app, handlers, extra) {
12474
12889
  const granted = Array.isArray(financeos) && financeos.length > 0;
12475
12890
  for (const name of names) {
12476
12891
  const row = sources[name];
12477
- if (!isPlainObject5(row)) {
12892
+ if (!isPlainObject6(row)) {
12478
12893
  problems.push({ code: "source_shape", pointer: at(name), message: `source "${name}" is ${JSON.stringify(row ?? null)}; a source is an object saying where its rows come from (from) and, when it reshapes them, which transform file does it` });
12479
12894
  continue;
12480
12895
  }
@@ -12503,7 +12918,7 @@ function sourceProblems(sources, app, handlers, extra) {
12503
12918
  }
12504
12919
  if (origin === "depends") {
12505
12920
  for (const [index, dep] of row.from.entries()) {
12506
- if (!named4(dep) || !Object.prototype.hasOwnProperty.call(sources, dep)) {
12921
+ if (!named5(dep) || !Object.prototype.hasOwnProperty.call(sources, dep)) {
12507
12922
  problems.push({
12508
12923
  code: "source_unknown",
12509
12924
  pointer: at(name, `from/${index}`),
@@ -12560,7 +12975,7 @@ function orderProblems(name, row, at) {
12560
12975
  }
12561
12976
  if (has("items")) {
12562
12977
  const labels = Array.isArray(row.items) ? row.items : null;
12563
- if (labels === null || labels.length === 0 || !labels.every(named4) || new Set(labels).size !== labels.length) {
12978
+ if (labels === null || labels.length === 0 || !labels.every(named5) || new Set(labels).size !== labels.length) {
12564
12979
  say("items", `states items as ${JSON.stringify(row.items)}; items is the breakdown's labels, each once, in the order they are drawn (["Opening ARR", "New", "Churn"])`);
12565
12980
  } else if (row.order !== "declared") {
12566
12981
  say("items", 'lists items, which are read only with order "declared"; add "order": "declared", or drop items');
@@ -12586,7 +13001,7 @@ function bridgeFixes(row) {
12586
13001
  fixes.push('add "items": [the step labels, in order] beside "order": "declared"');
12587
13002
  }
12588
13003
  } else if (row.order === "source") {
12589
- if (!transformed && !(origin === "table" && named4(row.order_by))) {
13004
+ if (!transformed && !(origin === "table" && named5(row.order_by))) {
12590
13005
  order2 = "unpromised";
12591
13006
  fixes.push(
12592
13007
  origin === "table" ? `add "order_by" naming the column the steps sort by (${unordered} comes back in no promised order), or replace "order": "source" with ${declare}` : `replace "order": "source" with ${declare} \u2014 ${unordered} comes back in no promised order, so its steps could be drawn shuffled`
@@ -12606,15 +13021,15 @@ function carriedFields(row, model) {
12606
13021
  if (row.transform !== void 0 && row.transform !== null) return null;
12607
13022
  const origin = originOf(row);
12608
13023
  if (origin === "financeos") {
12609
- const read = isPlainObject5(row.from.financeos) ? row.from.financeos : {};
12610
- const measures = Array.isArray(read.measures) ? read.measures : isPlainObject5(read.measure) ? [read.measure] : [];
13024
+ const read = isPlainObject6(row.from.financeos) ? row.from.financeos : {};
13025
+ const measures = Array.isArray(read.measures) ? read.measures : isPlainObject6(read.measure) ? [read.measure] : [];
12611
13026
  if (measures.length === 0) return null;
12612
13027
  const fields = /* @__PURE__ */ new Set();
12613
- for (const one of measures) if (named4(one?.field)) fields.add(one.field);
13028
+ for (const one of measures) if (named5(one?.field)) fields.add(one.field);
12614
13029
  if (measures.length === 1) fields.add("value");
12615
- for (const one of Array.isArray(read.dimensions) ? read.dimensions : []) if (named4(one)) fields.add(one);
12616
- const period = isPlainObject5(read.period_field) ? read.period_field.field : read.period_field;
12617
- if (named4(period)) {
13030
+ for (const one of Array.isArray(read.dimensions) ? read.dimensions : []) if (named5(one)) fields.add(one);
13031
+ const period = isPlainObject6(read.period_field) ? read.period_field.field : read.period_field;
13032
+ if (named5(period)) {
12618
13033
  fields.add(period);
12619
13034
  fields.add("period");
12620
13035
  }
@@ -12632,13 +13047,13 @@ function compareProblems(sources, model) {
12632
13047
  const at = (name, key) => `/data/sources/${escapePointer3(name)}/${key}`;
12633
13048
  for (const name of Object.keys(sources).sort(natural3)) {
12634
13049
  const row = sources[name];
12635
- if (!isPlainObject5(row)) continue;
13050
+ if (!isPlainObject6(row)) continue;
12636
13051
  const say = (key, message) => problems.push({ code: "source_compare_field", pointer: at(name, key), message: `source "${name}" ${message}` });
12637
- if (row.compare_label !== void 0 && !named4(row.compare_label)) {
13052
+ if (row.compare_label !== void 0 && !named5(row.compare_label)) {
12638
13053
  say("compare_label", `states compare_label as ${JSON.stringify(row.compare_label)}; compare_label is the words the compare line says ("budget", "plan")`);
12639
13054
  }
12640
13055
  if (row.compare === void 0) continue;
12641
- if (!named4(row.compare)) {
13056
+ if (!named5(row.compare)) {
12642
13057
  say("compare", `states compare as ${JSON.stringify(row.compare)}; compare names ONE measure field of the source's rows ("budget")`);
12643
13058
  continue;
12644
13059
  }
@@ -12646,7 +13061,7 @@ function compareProblems(sources, model) {
12646
13061
  say("compare", `states compare, which only a source shaped kpi reads (this one answers ${JSON.stringify(row.shape ?? "rows")}); give it shape kpi, or drop compare`);
12647
13062
  continue;
12648
13063
  }
12649
- const value = named4(row.value_field) ? row.value_field : "value";
13064
+ const value = named5(row.value_field) ? row.value_field : "value";
12650
13065
  if (row.compare === value) {
12651
13066
  say("compare", `compares its figure (${value}) against itself; compare names the OTHER measure (actual against "budget")`);
12652
13067
  continue;
@@ -12700,7 +13115,7 @@ function appIdProblems(app) {
12700
13115
  }
12701
13116
  ];
12702
13117
  }
12703
- return appIdLengthProblems(id, isPlainObject5(app.data_model.tables) ? Object.keys(app.data_model.tables) : []);
13118
+ return appIdLengthProblems(id, isPlainObject6(app.data_model.tables) ? Object.keys(app.data_model.tables) : []);
12704
13119
  }
12705
13120
  function appIdLengthProblems(id, tables) {
12706
13121
  const bytes = (text3) => new TextEncoder().encode(text3).length;
@@ -12725,10 +13140,10 @@ function themeProblems(app) {
12725
13140
  if (theme === void 0 || theme === null) return [];
12726
13141
  const at = (key) => `/app/theme${key ? `/${key}` : ""}`;
12727
13142
  const refuse = (key, message) => ({ code: "theme_shape", pointer: at(key), message });
12728
- if (!isPlainObject5(theme)) return [refuse("", `app.theme is ${JSON.stringify(theme)}; it is {"file": "ui/theme.css", "tokens": {"--dr-\u2026": "\u2026"}}, or null for none`)];
13143
+ if (!isPlainObject6(theme)) return [refuse("", `app.theme is ${JSON.stringify(theme)}; it is {"file": "ui/theme.css", "tokens": {"--dr-\u2026": "\u2026"}}, or null for none`)];
12729
13144
  const out = [];
12730
13145
  if (theme.file !== THEME_FILE2) out.push(refuse("file", `app.theme.file is ${JSON.stringify(theme.file ?? null)}; an app's theme is written to ${THEME_FILE2}, the one file its pages link after runtime.css`));
12731
- if (!isPlainObject5(theme.tokens)) out.push(refuse("tokens", `app.theme.tokens is ${JSON.stringify(theme.tokens ?? null)}; it is an object of --dr-* tokens to the values the app overrides them with ({} for none)`));
13146
+ if (!isPlainObject6(theme.tokens)) out.push(refuse("tokens", `app.theme.tokens is ${JSON.stringify(theme.tokens ?? null)}; it is an object of --dr-* tokens to the values the app overrides them with ({} for none)`));
12732
13147
  else {
12733
13148
  for (const token of Object.keys(theme.tokens).sort(natural3)) {
12734
13149
  const value = theme.tokens[token];
@@ -12762,7 +13177,7 @@ function themeValue(token, tokens, seen = /* @__PURE__ */ new Set()) {
12762
13177
  return hit ? themeValue(hit[1], tokens, seen) : value;
12763
13178
  }
12764
13179
  function themeCss(theme) {
12765
- const tokens = isPlainObject5(theme?.tokens) ? theme.tokens : {};
13180
+ const tokens = isPlainObject6(theme?.tokens) ? theme.tokens : {};
12766
13181
  const names = Object.keys(tokens).sort(natural3);
12767
13182
  const lines = [
12768
13183
  "/* The app's own look, generated from contract.json: edit app.theme, not this",
@@ -12792,21 +13207,29 @@ function themeCss(theme) {
12792
13207
  }
12793
13208
  function fillTheme(app, pages) {
12794
13209
  const theme = app?.theme;
12795
- if (!isPlainObject5(theme) || theme.preset === void 0 && theme.density === void 0) return { app, pages };
13210
+ if (!isPlainObject6(theme) || theme.preset === void 0 && theme.density === void 0) return { app, pages };
12796
13211
  const filled = pages.map((page) => {
12797
13212
  if (page?.owned === true) return page;
12798
13213
  const out = { ...page };
12799
- if (theme.preset !== void 0 && !named4(page.theme)) out.theme = theme.preset;
12800
- if (theme.density !== void 0 && !named4(page.density)) out.density = theme.density;
13214
+ if (theme.preset !== void 0 && !named5(page.theme)) out.theme = theme.preset;
13215
+ if (theme.density !== void 0 && !named5(page.density)) out.density = theme.density;
12801
13216
  return out;
12802
13217
  });
12803
13218
  const { preset, density, ...rest } = theme;
12804
13219
  return { app: { ...app, theme: rest }, pages: filled };
12805
13220
  }
12806
- var PARAM_KINDS = ["period", "multi", "select", "slider", "number", "toggle", "month"];
13221
+ var PARAM_KINDS = ["period", "multi", "select", "slider", "number", "toggle", "month", "text", "date"];
12807
13222
  var NUMERIC_PARAM_KINDS = /* @__PURE__ */ new Set(["slider", "number"]);
12808
13223
  var STRING_PARAM_KINDS = /* @__PURE__ */ new Set(["select", "multi", "period", "month"]);
12809
13224
  var MONTH_RE = /^\d{4}-\d{2}$/;
13225
+ var DAY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
13226
+ function isRealDay(value) {
13227
+ const found = DAY_RE.exec(value);
13228
+ if (found === null) return false;
13229
+ const [year, month, day] = found.slice(1).map(Number);
13230
+ const at = new Date(Date.UTC(year, month - 1, day));
13231
+ return at.getUTCFullYear() === year && at.getUTCMonth() === month - 1 && at.getUTCDate() === day;
13232
+ }
12810
13233
  function isStandardParam(name) {
12811
13234
  return name === "period" || name === "scenario" || typeof name === "string" && name.startsWith("dimensions:");
12812
13235
  }
@@ -12832,18 +13255,18 @@ function composedOf(sources, name) {
12832
13255
  const queue = [name];
12833
13256
  while (queue.length > 0) {
12834
13257
  const one = queue.shift();
12835
- if (out.has(one) || !isPlainObject5(sources[one])) continue;
13258
+ if (out.has(one) || !isPlainObject6(sources[one])) continue;
12836
13259
  out.add(one);
12837
- if (Array.isArray(sources[one].from)) queue.push(...sources[one].from.filter(named4));
13260
+ if (Array.isArray(sources[one].from)) queue.push(...sources[one].from.filter(named5));
12838
13261
  }
12839
13262
  return out;
12840
13263
  }
12841
13264
  function sourceReaders(contract, sources) {
12842
13265
  const out = /* @__PURE__ */ new Map();
12843
- const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
13266
+ const elements = isPlainObject6(contract?.elements) ? contract.elements : {};
12844
13267
  for (const element of Object.values(elements)) {
12845
- const props = isPlainObject5(element?.props) ? element.props : {};
12846
- const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named4);
13268
+ const props = isPlainObject6(element?.props) ? element.props : {};
13269
+ const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named5);
12847
13270
  for (const read of reads) {
12848
13271
  for (const one of composedOf(sources, read)) {
12849
13272
  if (!out.has(one)) out.set(one, /* @__PURE__ */ new Set());
@@ -12860,7 +13283,7 @@ function paramProblems(contract, sources) {
12860
13283
  for (const { page, pageIndex, index, param } of declared) {
12861
13284
  const at = (key) => `/pages/${pageIndex}/params/${index}${key ? `/${key}` : ""}`;
12862
13285
  const where2 = `page ${JSON.stringify(page.path ?? null)}`;
12863
- if (!isPlainObject5(param) || !named4(param.name)) {
13286
+ if (!isPlainObject6(param) || !named5(param.name)) {
12864
13287
  problems.push({ code: "param_kind_value", pointer: at(), message: `${where2} declares a param as ${JSON.stringify(param ?? null)}; a param is {"name": "...", "kind": "..."} with a name` });
12865
13288
  continue;
12866
13289
  }
@@ -12890,12 +13313,12 @@ function paramProblems(contract, sources) {
12890
13313
  });
12891
13314
  }
12892
13315
  }
12893
- const names = new Set(declared.filter((one) => isPlainObject5(one.param) && named4(one.param.name)).map((one) => one.param.name));
13316
+ const names = new Set(declared.filter((one) => isPlainObject6(one.param) && named5(one.param.name)).map((one) => one.param.name));
12894
13317
  const readers = sourceReaders(contract, sources);
12895
- const namesOn = (paths) => new Set(declared.filter((one) => paths.has(one.page?.path) && isPlainObject5(one.param) && named4(one.param.name)).map((one) => one.param.name));
13318
+ const namesOn = (paths) => new Set(declared.filter((one) => paths.has(one.page?.path) && isPlainObject6(one.param) && named5(one.param.name)).map((one) => one.param.name));
12896
13319
  for (const source of Object.keys(sources).sort(natural3)) {
12897
13320
  const row = sources[source];
12898
- if (!isPlainObject5(row) || row.honors === void 0) continue;
13321
+ if (!isPlainObject6(row) || row.honors === void 0) continue;
12899
13322
  if (!Array.isArray(row.honors)) {
12900
13323
  problems.push({ code: "source_shape", pointer: `/data/sources/${escapePointer3(source)}/honors`, message: `source "${source}" states honors as ${JSON.stringify(row.honors)}; honors is a list of the param names the source takes` });
12901
13324
  continue;
@@ -12904,8 +13327,8 @@ function paramProblems(contract, sources) {
12904
13327
  const scope = pages === null ? names : namesOn(pages);
12905
13328
  const which = pages === null ? "no page declares" : `no page that reads it (${[...pages].sort(natural3).map((one) => JSON.stringify(one)).join(", ")}) declares`;
12906
13329
  for (const [index, honored] of row.honors.entries()) {
12907
- if (named4(honored) && scope.has(honored)) continue;
12908
- if (named4(honored) && isStandardParam(honored) && scope.size === 0) continue;
13330
+ if (named5(honored) && scope.has(honored)) continue;
13331
+ if (named5(honored) && isStandardParam(honored) && scope.size === 0) continue;
12909
13332
  problems.push({
12910
13333
  code: "param_unknown_honor",
12911
13334
  pointer: `/data/sources/${escapePointer3(source)}/honors/${index}`,
@@ -12950,7 +13373,7 @@ function paramValueProblems(param, said2, at) {
12950
13373
  }
12951
13374
  if (param.values !== void 0) {
12952
13375
  if (!STRING_PARAM_KINDS.has(kind)) refuse("values", `states values, which only a select, multi, period or month is held to`);
12953
- else if (!Array.isArray(param.values) || param.values.length === 0 || !param.values.every(named4)) refuse("values", `states values as ${JSON.stringify(param.values)}; it is a non-empty list of strings (leave it out to take the values from the source's catalog)`);
13376
+ else if (!Array.isArray(param.values) || param.values.length === 0 || !param.values.every(named5)) refuse("values", `states values as ${JSON.stringify(param.values)}; it is a non-empty list of strings (leave it out to take the values from the source's catalog)`);
12954
13377
  else if (new Set(param.values).size !== param.values.length) refuse("values", "lists one value twice");
12955
13378
  }
12956
13379
  if (param.max_selected !== void 0) {
@@ -12982,6 +13405,8 @@ function paramValueProblems(param, said2, at) {
12982
13405
  refuse("default", `defaults to ${JSON.stringify(value)}; a ${kind} opens on a string`);
12983
13406
  } else if (kind === "month" && !MONTH_RE.test(value)) {
12984
13407
  refuse("default", `defaults to ${JSON.stringify(value)}; a month is written YYYY-MM`);
13408
+ } else if (kind === "date" && value !== "" && !isRealDay(value)) {
13409
+ refuse("default", `defaults to ${JSON.stringify(value)}; a date is a real day written YYYY-MM-DD, or '' for none`);
12985
13410
  } else if (values !== null && !values.includes(value)) {
12986
13411
  refuse("default", `defaults to ${JSON.stringify(value)}, which is not among its values ${JSON.stringify(values)}`);
12987
13412
  }
@@ -12991,8 +13416,8 @@ function fillFilterable(sources) {
12991
13416
  let out = sources;
12992
13417
  for (const name of Object.keys(sources)) {
12993
13418
  const row = sources[name];
12994
- if (!isPlainObject5(row) || row.filterable !== void 0 || !Array.isArray(row.honors)) continue;
12995
- const dims = row.honors.filter((one) => named4(one) && one.startsWith("dimensions:")).map((one) => one.slice("dimensions:".length));
13419
+ if (!isPlainObject6(row) || row.filterable !== void 0 || !Array.isArray(row.honors)) continue;
13420
+ const dims = row.honors.filter((one) => named5(one) && one.startsWith("dimensions:")).map((one) => one.slice("dimensions:".length));
12996
13421
  if (dims.length === 0) continue;
12997
13422
  if (out === sources) out = { ...sources };
12998
13423
  out[name] = { ...row, filterable: [...new Set(dims)] };
@@ -13056,17 +13481,17 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13056
13481
  const appWidgets = typeof kit?.appWidgets === "string" && kit.appWidgets !== "" ? kit.appWidgets : WIDGETS;
13057
13482
  const appComponents = typeof kit?.appComponents === "string" && kit.appComponents !== "" ? kit.appComponents : COMPONENTS;
13058
13483
  const appControls = typeof kit?.appControls === "string" && kit.appControls !== "" ? kit.appControls : CONTROLS;
13059
- if (app.kind === "dashboard" || app.kind !== "app" && !isPlainObject5(contract?.data?.app)) {
13484
+ if (app.kind === "dashboard" || app.kind !== "app" && !isPlainObject6(contract?.data?.app)) {
13060
13485
  problems.push({
13061
13486
  code: "kind_mismatch",
13062
13487
  pointer: "/app/kind",
13063
- message: `app.kind is ${JSON.stringify(app.kind ?? null)} and data.app ${isPlainObject5(contract?.data?.app) ? "is present" : "is absent"}; compileApp writes App Space apps, which say what their backend does in data.app, and a dashboard is compiled by compile.mjs`
13488
+ message: `app.kind is ${JSON.stringify(app.kind ?? null)} and data.app ${isPlainObject6(contract?.data?.app) ? "is present" : "is absent"}; compileApp writes App Space apps, which say what their backend does in data.app, and a dashboard is compiled by compile.mjs`
13064
13489
  });
13065
13490
  return { files: {}, problems, contract };
13066
13491
  }
13067
13492
  problems.push(...appIdProblems(app));
13068
- const typed = isPlainObject5(contract.elements) ? contract.elements : {};
13069
- const gates = isPlainObject5(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
13493
+ const typed = isPlainObject6(contract.elements) ? contract.elements : {};
13494
+ const gates = isPlainObject6(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
13070
13495
  problems.push(...statedIdProblems(typed), ...roleListProblems(typed, gates));
13071
13496
  const declared = foldDialogTriggers(foldNavRegions(foldHeaderRoles(foldRoleLists(foldStatedIds(typed), gates))));
13072
13497
  problems.push(...headerPropsProblems(declared));
@@ -13080,7 +13505,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13080
13505
  }
13081
13506
  const model = modelIndex(app.data_model);
13082
13507
  problems.push(...pkProblems(model));
13083
- const handlers = isPlainObject5(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
13508
+ const handlers = isPlainObject6(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
13084
13509
  const names = Object.keys(handlers).sort(natural3);
13085
13510
  const stated2 = sourcesOf(contract);
13086
13511
  const sourceFindings = sourceProblems(stated2, app, handlers, extra);
@@ -13088,6 +13513,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13088
13513
  problems.push(...compareProblems(stated2, model));
13089
13514
  problems.push(...paramProblems(contract, stated2));
13090
13515
  problems.push(...themeProblems(app));
13516
+ problems.push(...shellProblems(contract, typed, handlers, stated2));
13091
13517
  const sources = fillFilterable(stated2);
13092
13518
  problems.push(...specMapProblems(contract?.data?.widgets));
13093
13519
  for (const name of names) {
@@ -13112,7 +13538,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13112
13538
  const element = declared[uid];
13113
13539
  if (element?.type !== "custom") continue;
13114
13540
  const args = element.props?.args;
13115
- if (args !== void 0 && args !== null && !isPlainObject5(args)) {
13541
+ if (args !== void 0 && args !== null && !isPlainObject6(args)) {
13116
13542
  problems.push({
13117
13543
  code: "app_model_mismatch",
13118
13544
  pointer: `/elements/${escapePointer3(uid)}/props/args`,
@@ -13121,7 +13547,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13121
13547
  }
13122
13548
  const module = element.props?.module;
13123
13549
  const pointer2 = `/elements/${escapePointer3(uid)}/props/module`;
13124
- if (!named4(module) || !MODULE_RE.test(module)) {
13550
+ if (!named5(module) || !MODULE_RE.test(module)) {
13125
13551
  problems.push({
13126
13552
  code: "custom_module_path",
13127
13553
  pointer: pointer2,
@@ -13157,7 +13583,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13157
13583
  message: `"${uid}" sits on ${declared[uid].page}, which is owned: the compiler writes nothing on that page, so it cannot write this \u2014 move the element to a compiled page, or drop owned`
13158
13584
  });
13159
13585
  }
13160
- const tree = treeOf(declared);
13586
+ let tree = treeOf(declared);
13161
13587
  problems.push(...tree.problems);
13162
13588
  if (tree.problems.length === 0) problems.push(...byProblems(declared, tree, handlers, model), ...pickerProblems(declared, tree, handlers, model), ...whenProblems(declared, tree, model));
13163
13589
  problems.push(...computedProblems(declared, tree, handlers, model));
@@ -13176,8 +13602,10 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13176
13602
  if (sources !== stated2) contract = { ...contract, data: { ...contract.data, sources } };
13177
13603
  const parented = dropEmptyArgs(fillChartFormats(fillParents(declared, tree), handlers, model));
13178
13604
  const headed = fillHeaders(contract, parented);
13179
- const elements = headed.elements;
13180
- const themed = fillTheme(app, headed.pages);
13605
+ const hoisted = hoistHeaderActions(headed.pages, headed.elements);
13606
+ const elements = hoisted.elements;
13607
+ if (hoisted.elements !== headed.elements) tree = treeOf(hoisted.elements);
13608
+ const themed = fillTheme(app, hoisted.pages);
13181
13609
  const pages = themed.pages;
13182
13610
  if (themed.app !== app) contract = { ...contract, app: themed.app };
13183
13611
  const order2 = emittedOrder(contract, elements, pages, tree);
@@ -13195,7 +13623,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13195
13623
  }
13196
13624
  const params = paramsTable(contract);
13197
13625
  for (const name of served) files[`handlers/${name}.py`] = sourceModule(name, sources, transforms, params);
13198
- const theme = isPlainObject5(app.theme) ? app.theme : null;
13626
+ const theme = isPlainObject6(app.theme) ? app.theme : null;
13199
13627
  if (theme !== null) files[THEME_FILE2] = themeCss(theme);
13200
13628
  files[PALETTE_FILE] = paletteCss();
13201
13629
  if (pages.some((page) => page.owned !== true && tokensOn(page, elements))) files[BADGES_FILE] = badgesCss();
@@ -13221,8 +13649,8 @@ function restate(contract, elements, pages, typed, order2) {
13221
13649
  }
13222
13650
  var DASHBOARD_DATA_KEYS = ["page", "reads", "datasets", "library"];
13223
13651
  function dropEmptyDashboardData(data) {
13224
- if (!isPlainObject5(data)) return data;
13225
- const empty = DASHBOARD_DATA_KEYS.filter((key) => isPlainObject5(data[key]) && Object.keys(data[key]).length === 0);
13652
+ if (!isPlainObject6(data)) return data;
13653
+ const empty = DASHBOARD_DATA_KEYS.filter((key) => isPlainObject6(data[key]) && Object.keys(data[key]).length === 0);
13226
13654
  if (empty.length === 0) return data;
13227
13655
  const out = { ...data };
13228
13656
  for (const key of empty) delete out[key];
@@ -13251,7 +13679,7 @@ function pageOrder(contract, page, elements, tree) {
13251
13679
  if (!nested) return;
13252
13680
  for (const child of writtenChildren(uid, elements, tree)) visit(child.uid, child.nested);
13253
13681
  };
13254
- for (const band of bandsOf(page, elements, rootsOf(contract, page, elements, tree))) {
13682
+ for (const band of bandsOf2(page, elements, rootsOf2(contract, page, elements, tree))) {
13255
13683
  for (const uid of band.roots) visit(uid, true);
13256
13684
  }
13257
13685
  return out;
@@ -13260,6 +13688,8 @@ function writtenChildren(uid, elements, tree) {
13260
13688
  const kids = childrenOf(uid, elements, tree);
13261
13689
  if (isPart2(elements[uid])) return kids.map((child) => ({ uid: child, nested: true }));
13262
13690
  switch (elements[uid]?.type) {
13691
+ case "region":
13692
+ return (elements[uid].role ?? "header") === "header" ? kids.map((child) => ({ uid: child, nested: true })) : [];
13263
13693
  case "form":
13264
13694
  return kids.map((child) => ({ uid: child, nested: isPart2(elements[child]) }));
13265
13695
  case "detail": {
@@ -13336,33 +13766,173 @@ function placementProblems(elements, pages, tree, order2) {
13336
13766
  }
13337
13767
  return problems;
13338
13768
  }
13339
- function fillHeaders(contract, elements) {
13340
- const pages = Array.isArray(contract.pages) ? contract.pages : [];
13341
- const wanted = pages.filter(
13342
- (page) => page.owned !== true && isPlainObject5(page.header) && !Object.values(elements).some(
13343
- (element) => element.page === page.path && element.type === "region" && (element.role ?? "header") === "header"
13344
- )
13769
+ var SHELL_NAVS = ["tabs", "rail"];
13770
+ function shellProblems(contract, elements, handlers, sources) {
13771
+ const problems = [];
13772
+ const app = contract?.app ?? {};
13773
+ const pages = Array.isArray(contract?.pages) ? contract.pages : [];
13774
+ const paths = new Set(pages.map((page) => page?.path).filter(named5));
13775
+ const params = new Set(pages.flatMap((page) => (Array.isArray(page?.params) ? page.params : []).map((param) => param?.name)).filter(named5));
13776
+ const refuse = (pointer2, message) => problems.push({ code: "shell_shape", pointer: pointer2, message });
13777
+ const roleNames = new Set(
13778
+ (Array.isArray(contract?.backend?.roles) ? contract.backend.roles : []).map((role) => typeof role === "string" ? role : role?.name).filter(named5)
13345
13779
  );
13346
- if (wanted.length === 0) return { elements, pages };
13347
- const filled = { ...elements };
13348
- const out = pages.map((page) => {
13349
- if (!wanted.includes(page)) return page;
13350
- let uid = `${page.path}#0`;
13351
- while (filled[uid] !== void 0) uid += "a";
13352
- filled[uid] = { uid, page: page.path, type: "region", role: "header", parent: null, children: [] };
13353
- return Array.isArray(page.tree) && page.tree.length > 0 ? { ...page, tree: [uid, ...page.tree] } : page;
13354
- });
13355
- return { elements: filled, pages: out };
13356
- }
13357
- function modelIndex(document) {
13358
- const tables = {};
13359
- const raw = isPlainObject5(document?.tables) ? document.tables : {};
13360
- for (const [name, table] of Object.entries(raw)) {
13361
- const columns = {};
13362
- for (const [column, spec] of Object.entries(isPlainObject5(table?.columns) ? table.columns : {})) {
13363
- const held = typeof spec === "string" ? { type: spec } : isPlainObject5(spec) ? spec : {};
13364
- const pk = Boolean(held.pk);
13365
- columns[column] = {
13780
+ if (app.shell !== void 0 && app.shell !== null) {
13781
+ const shell = app.shell;
13782
+ if (!isPlainObject6(shell)) refuse("/app/shell", `app.shell is ${JSON.stringify(shell)}; it is {"nav": "tabs"|"rail", "topbar"?: true, "rail"?: {...}}`);
13783
+ else {
13784
+ if (shell.nav !== void 0 && !SHELL_NAVS.includes(shell.nav)) refuse("/app/shell/nav", `app.shell.nav is ${JSON.stringify(shell.nav)}; it is ${either3(SHELL_NAVS)}`);
13785
+ if (shell.topbar !== void 0 && typeof shell.topbar !== "boolean") refuse("/app/shell/topbar", `app.shell.topbar is ${JSON.stringify(shell.topbar)}; it is true or false`);
13786
+ if (shell.rail !== void 0 && !isPlainObject6(shell.rail)) refuse("/app/shell/rail", `app.shell.rail is ${JSON.stringify(shell.rail)}; it is {"title"?, "entries"?: [...], "footer"?: [...]}`);
13787
+ else if (isPlainObject6(shell.rail)) {
13788
+ for (const key of ["entries", "footer"]) {
13789
+ const list3 = shell.rail[key];
13790
+ if (list3 === void 0) continue;
13791
+ if (!Array.isArray(list3)) {
13792
+ refuse(`/app/shell/rail/${key}`, `app.shell.rail.${key} is ${JSON.stringify(list3)}; it is a list of {"label", ...}`);
13793
+ continue;
13794
+ }
13795
+ for (const [index, entry] of list3.entries()) {
13796
+ const at = `/app/shell/rail/${key}/${index}`;
13797
+ if (!isPlainObject6(entry) || !named5(entry.label)) {
13798
+ refuse(at, `app.shell.rail.${key}[${index}] is ${JSON.stringify(entry)}; every entry has a label (the runtime drops one without)`);
13799
+ continue;
13800
+ }
13801
+ if (entry.page !== void 0 && !(named5(entry.page) && paths.has(entry.page))) {
13802
+ refuse(`${at}/page`, `rail entry "${entry.label}" links to ${JSON.stringify(entry.page)}, which pages[] does not declare; name a page of this app (${[...paths].sort(natural3).map((one) => JSON.stringify(one)).join(", ")})`);
13803
+ }
13804
+ if (key === "footer" && !named5(entry.page)) refuse(`${at}/page`, `rail footer entry "${entry.label}" links nowhere; a footer entry is {"label", "page"}`);
13805
+ if (entry.param !== void 0) {
13806
+ if (!isPlainObject6(entry.param) || Object.keys(entry.param).length === 0) {
13807
+ refuse(`${at}/param`, `rail entry "${entry.label}" states param as ${JSON.stringify(entry.param)}; it is {"<page param>": <value>}`);
13808
+ } else {
13809
+ for (const name of Object.keys(entry.param).sort(natural3)) {
13810
+ if (!params.has(name)) refuse(`${at}/param/${escapePointer3(name)}`, `rail entry "${entry.label}" sets the param "${name}", which no page declares in pages[].params; the param store keeps only declared params, so the entry would do nothing`);
13811
+ }
13812
+ }
13813
+ }
13814
+ if (entry.measure !== void 0 && !named5(entry.measure)) refuse(`${at}/measure`, `rail entry "${entry.label}" states measure as ${JSON.stringify(entry.measure)}; it is the name of one measure of the page's nav.counts.source`);
13815
+ if (entry.role !== void 0) {
13816
+ const names = named5(entry.role) ? entry.role.split(/\s+/).filter(named5) : [];
13817
+ const unknown = names.filter((name) => !roleNames.has(name));
13818
+ if (names.length === 0) refuse(`${at}/role`, `rail entry "${entry.label}" states role as ${JSON.stringify(entry.role)}; it is one role name, or several space-separated`);
13819
+ else if (unknown.length > 0) {
13820
+ refuse(
13821
+ `${at}/role`,
13822
+ `rail entry "${entry.label}" is for ${unknown.map((one) => JSON.stringify(one)).join(", ")}, which ${roleNames.size === 0 ? "the app does not declare (it declares no roles)" : `backend.roles does not declare (${[...roleNames].map((one) => JSON.stringify(one)).join(", ")})`}; the runtime would draw it for nobody`
13823
+ );
13824
+ }
13825
+ }
13826
+ }
13827
+ }
13828
+ }
13829
+ }
13830
+ }
13831
+ for (const [index, page] of pages.entries()) {
13832
+ if (!isPlainObject6(page) || page.owned === true) continue;
13833
+ if (isPlainObject6(page.header) && page.header.title_from !== void 0 && page.header.title_from !== "rail") {
13834
+ refuse(`/pages/${index}/header/title_from`, `${page.path}'s header.title_from is ${JSON.stringify(page.header.title_from)}; the one this runtime reads is "rail" (the active rail entry's label)`);
13835
+ }
13836
+ if (page.nav !== void 0) {
13837
+ const source = page.nav?.counts?.source;
13838
+ if (!isPlainObject6(page.nav) || !isPlainObject6(page.nav.counts) || !named5(source)) {
13839
+ refuse(`/pages/${index}/nav`, `${page.path}'s nav is ${JSON.stringify(page.nav)}; it is {"counts": {"source": "<summary handler>"}}`);
13840
+ } else if (!Object.prototype.hasOwnProperty.call(handlers, source) && !Object.prototype.hasOwnProperty.call(sources ?? {}, source)) {
13841
+ refuse(`/pages/${index}/nav/counts/source`, `${page.path}'s nav.counts.source is "${source}", which is not a handler or source of this app; name the summary handler whose measures are the rail's counts`);
13842
+ }
13843
+ }
13844
+ }
13845
+ for (const found of headerActionProblems({ ...contract, elements })) {
13846
+ problems.push({ code: found.code, pointer: `/pages/${found.pageIndex}/header/actions${found.index === null ? "" : `/${found.index}`}`, message: found.message });
13847
+ }
13848
+ for (const [index, page] of pages.entries()) {
13849
+ if (!isPlainObject6(page) || page.owned === true || !Array.isArray(page.header?.actions)) continue;
13850
+ for (const [at, id] of page.header.actions.entries()) {
13851
+ const uid = Object.keys(elements).find((one) => elements[one]?.page === page.path && elements[one].type !== "region" && (named5(elements[one].props?.id) ? elements[one].props.id : elements[one].id) === id);
13852
+ if (uid === void 0) continue;
13853
+ const parent = parentUid(uid, elements);
13854
+ if (parent === null) continue;
13855
+ const holder = elements[parent];
13856
+ if (holder?.type === "region" && (holder.role ?? "header") === "header") continue;
13857
+ problems.push({
13858
+ code: "header_action_unknown",
13859
+ pointer: `/pages/${index}/header/actions/${at}`,
13860
+ message: `${page.path}'s header.actions names "${id}", which sits inside "${parent}" (a ${holder?.type ?? "element"}); a header action is one of the page's top-level elements, so move it out to the page`
13861
+ });
13862
+ }
13863
+ }
13864
+ return problems;
13865
+ }
13866
+ function parentUid(uid, elements) {
13867
+ const stated2 = elements[uid]?.parent ?? null;
13868
+ if (stated2 !== null) return stated2;
13869
+ for (const [other, element] of Object.entries(elements)) {
13870
+ if (Array.isArray(element?.children) && element.children.includes(uid)) return other;
13871
+ }
13872
+ return null;
13873
+ }
13874
+ function hoistHeaderActions(pages, elements) {
13875
+ let out = elements;
13876
+ let outPages = pages;
13877
+ for (const [pageIndex, page] of pages.entries()) {
13878
+ if (page?.owned === true || !Array.isArray(page?.header?.actions) || page.header.actions.length === 0) continue;
13879
+ const header = Object.keys(elements).sort(natural3).find((uid) => elements[uid].page === page.path && elements[uid].type === "region" && (elements[uid].role ?? "header") === "header");
13880
+ if (header === void 0) continue;
13881
+ const moved = [];
13882
+ for (const id of page.header.actions) {
13883
+ const uid = Object.keys(elements).sort(natural3).find((one) => elements[one].page === page.path && elements[one].type !== "region" && (named5(elements[one].props?.id) ? elements[one].props.id : elements[one].id) === id);
13884
+ if (uid !== void 0 && !moved.includes(uid)) moved.push(uid);
13885
+ }
13886
+ const region = out[header];
13887
+ const already = moved.every((uid) => out[uid].parent === header && out[uid].section === "header") && JSON.stringify(region.children ?? []) === JSON.stringify(moved);
13888
+ if (moved.length === 0 || already) continue;
13889
+ if (out === elements) out = { ...elements };
13890
+ for (const uid of moved) out[uid] = { ...out[uid], parent: header, section: "header" };
13891
+ out[header] = { ...region, children: moved };
13892
+ if (Array.isArray(page.tree) && page.tree.some((uid) => moved.includes(uid))) {
13893
+ if (outPages === pages) outPages = [...pages];
13894
+ outPages[pageIndex] = { ...page, tree: page.tree.filter((uid) => !moved.includes(uid)) };
13895
+ }
13896
+ }
13897
+ return { elements: out, pages: outPages };
13898
+ }
13899
+ function shellAttr(app) {
13900
+ return isPlainObject6(app?.shell) ? ` data-dr-shell="${attr(JSON.stringify(canonical2(app.shell)))}"` : "";
13901
+ }
13902
+ function navCountsAttr(page) {
13903
+ const source = page?.nav?.counts?.source;
13904
+ return named5(source) ? ` data-dr-nav-counts="${attr(source)}"` : "";
13905
+ }
13906
+ function hasNav(page, elements) {
13907
+ return Object.values(elements).some((element) => element?.page === page.path && element.type === "nav");
13908
+ }
13909
+ function fillHeaders(contract, elements) {
13910
+ const pages = Array.isArray(contract.pages) ? contract.pages : [];
13911
+ const wanted = pages.filter(
13912
+ (page) => page.owned !== true && isPlainObject6(page.header) && !Object.values(elements).some(
13913
+ (element) => element.page === page.path && element.type === "region" && (element.role ?? "header") === "header"
13914
+ )
13915
+ );
13916
+ if (wanted.length === 0) return { elements, pages };
13917
+ const filled = { ...elements };
13918
+ const out = pages.map((page) => {
13919
+ if (!wanted.includes(page)) return page;
13920
+ let uid = `${page.path}#0`;
13921
+ while (filled[uid] !== void 0) uid += "a";
13922
+ filled[uid] = { uid, page: page.path, type: "region", role: "header", parent: null, children: [] };
13923
+ return Array.isArray(page.tree) && page.tree.length > 0 ? { ...page, tree: [uid, ...page.tree] } : page;
13924
+ });
13925
+ return { elements: filled, pages: out };
13926
+ }
13927
+ function modelIndex(document) {
13928
+ const tables = {};
13929
+ const raw = isPlainObject6(document?.tables) ? document.tables : {};
13930
+ for (const [name, table] of Object.entries(raw)) {
13931
+ const columns = {};
13932
+ for (const [column, spec] of Object.entries(isPlainObject6(table?.columns) ? table.columns : {})) {
13933
+ const held = typeof spec === "string" ? { type: spec } : isPlainObject6(spec) ? spec : {};
13934
+ const pk = Boolean(held.pk);
13935
+ columns[column] = {
13366
13936
  name: column,
13367
13937
  type: typeof held.type === "string" ? held.type : held.enum ? "text" : "text",
13368
13938
  declaredType: typeof held.type === "string",
@@ -13374,7 +13944,7 @@ function modelIndex(document) {
13374
13944
  // On a ref column: which column of the target a reader sees instead
13375
13945
  // of the id. Validated in `pickerProblems`, not here — a model is
13376
13946
  // read as written and refused with a pointer, never silently fixed.
13377
- display: named4(held.display) ? held.display : null,
13947
+ display: named5(held.display) ? held.display : null,
13378
13948
  // A child total the platform selects with the row (`computedProblems`
13379
13949
  // holds it to its grammar), and the column it should agree with.
13380
13950
  // Read as written, like `display`.
@@ -13397,7 +13967,7 @@ function modelIndex(document) {
13397
13967
  // What that one row reads as before it is first saved. Read as written;
13398
13968
  // the platform's parser is what holds each value to its column.
13399
13969
  // Only on a singleton, as the platform's parser and the schema have it.
13400
- defaults: table?.singleton === true && isPlainObject5(table?.defaults) ? table.defaults : null,
13970
+ defaults: table?.singleton === true && isPlainObject6(table?.defaults) ? table.defaults : null,
13401
13971
  columns,
13402
13972
  indexes: Array.isArray(table?.indexes) ? table.indexes.map((index) => Array.isArray(index) ? index.map(String) : [String(index)]) : []
13403
13973
  };
@@ -13483,7 +14053,7 @@ function fillParents(elements, tree) {
13483
14053
  function entityOf(uid, elements, tree) {
13484
14054
  for (let current = uid; current !== null && current !== void 0; current = parentOf(tree, current)) {
13485
14055
  const value = elements[current]?.props?.entity;
13486
- if (named4(value)) return value;
14056
+ if (named5(value)) return value;
13487
14057
  if (!elements[current]) return null;
13488
14058
  }
13489
14059
  return null;
@@ -13514,11 +14084,11 @@ function historyColumnsInWords() {
13514
14084
  return `History columns are ${lead.join(", ")}${rest.length > 0 ? ` (also ${rest.join(", ")})` : ""}`;
13515
14085
  }
13516
14086
  function historyFieldProblem(uid, type, props, elements, tree) {
13517
- if (type !== "field" || !named4(props.entity) || props.entity === "_events") return null;
14087
+ if (type !== "field" || !named5(props.entity) || props.entity === "_events") return null;
13518
14088
  const list3 = historyListAround(uid, elements, tree);
13519
14089
  if (list3 === null) return null;
13520
- const listId = named4(elements[list3]?.props?.id) ? elements[list3].props.id : list3;
13521
- const shows = named4(props.field) ? ` shows "${props.field}" and` : "";
14090
+ const listId = named5(elements[list3]?.props?.id) ? elements[list3].props.id : list3;
14091
+ const shows = named5(props.field) ? ` shows "${props.field}" and` : "";
13522
14092
  return {
13523
14093
  code: "app_model_mismatch",
13524
14094
  pointer: `/elements/${escapePointer3(uid)}/props/entity`,
@@ -13527,9 +14097,9 @@ function historyFieldProblem(uid, type, props, elements, tree) {
13527
14097
  }
13528
14098
  function decideButtonProblem(uid, type, props, handlers) {
13529
14099
  if (type !== "action" && type !== "detail") return null;
13530
- if (!named4(props.action) || !Object.prototype.hasOwnProperty.call(handlers, props.action)) return null;
14100
+ if (!named5(props.action) || !Object.prototype.hasOwnProperty.call(handlers, props.action)) return null;
13531
14101
  const row = handlers[props.action] ?? {};
13532
- if (row.role !== "decide" || !isPlainObject5(row.decision)) return null;
14102
+ if (row.role !== "decide" || !isPlainObject6(row.decision)) return null;
13533
14103
  const names = decisionNames(row);
13534
14104
  if (names.length < 2) return null;
13535
14105
  const split2 = names.map((one) => `${props.action}_${one} with {"${one}": {...}}`).join(" and ");
@@ -13552,7 +14122,7 @@ function modelProblems(elements, tree, handlers, model, roles, sources = {}) {
13552
14122
  problems.push(inHistory);
13553
14123
  continue;
13554
14124
  }
13555
- if ((type === "chart" || type === "custom" || type === "widget") && named4(props.entity)) {
14125
+ if ((type === "chart" || type === "custom" || type === "widget") && named5(props.entity)) {
13556
14126
  const answers = type === "widget" ? "its payload is whatever its source answers" : "its rows are whatever its source answers";
13557
14127
  problems.push({
13558
14128
  code: "app_model_mismatch",
@@ -13571,25 +14141,25 @@ function modelProblems(elements, tree, handlers, model, roles, sources = {}) {
13571
14141
  say(uid, `${type} "${uid}" names entity "${entity}", which data_model declares no table for`);
13572
14142
  continue;
13573
14143
  }
13574
- if (named4(props.field) && entity !== null && isPlatformTable(entity)) {
14144
+ if (named5(props.field) && entity !== null && isPlatformTable(entity)) {
13575
14145
  if (!hasColumn(model, entity, props.field)) {
13576
14146
  say(
13577
14147
  uid,
13578
14148
  props.field === "changes" ? `${type} "${uid}" shows "changes", which is a jsonb cell a reader would see as [object Object] \u2014 show "summary", which is changes in words` : `${type} "${uid}" names field "${props.field}", which is no column the platform answers ${entity} with (${PLATFORM_TABLES[entity].join(", ")})`
13579
14149
  );
13580
14150
  }
13581
- } else if (named4(props.field)) {
14151
+ } else if (named5(props.field)) {
13582
14152
  if (entity === null) say(uid, `${type} "${uid}" names field "${props.field}" and sits under no entity, so no column can answer for it`);
13583
14153
  else if (!hasColumn(model, entity, props.field)) {
13584
14154
  const history = PLATFORM_TABLES._events.includes(props.field) ? `; "${props.field}" is a column of a row's history (_events), which only a list over "_events" sourced by a handler with role "history" shows, inside the row's detail` : "";
13585
14155
  say(uid, `${type} "${uid}" names field "${props.field}", which is no column of table "${entity}", nor one the platform injects${history}`);
13586
14156
  }
13587
14157
  }
13588
- for (const column of Object.keys(isPlainObject5(props.filter) ? props.filter : {}).sort()) {
14158
+ for (const column of Object.keys(isPlainObject6(props.filter) ? props.filter : {}).sort()) {
13589
14159
  if (entity !== null && !hasColumn(model, entity, column)) say(uid, `${type} "${uid}" filters on "${column}", which is no column of table "${entity}"`);
13590
14160
  }
13591
14161
  for (const key of ["source", "action"]) {
13592
- if (!named4(props[key])) continue;
14162
+ if (!named5(props[key])) continue;
13593
14163
  if ((type === "chart" || type === "widget") && !stated(props[key])) continue;
13594
14164
  if (Object.prototype.hasOwnProperty.call(handlers, props[key])) continue;
13595
14165
  if (key === "source" && Object.prototype.hasOwnProperty.call(sources, props[key])) continue;
@@ -13658,7 +14228,7 @@ function modelProblems(elements, tree, handlers, model, roles, sources = {}) {
13658
14228
  }
13659
14229
  }
13660
14230
  problems.push(...platformQueryProblems(name, row, model.tables[row.table]));
13661
- const ambiguous = named4(row.guard) && ["decide", "remove", "save"].includes(row.role) ? ambiguousStatus(name, row, model) : null;
14231
+ const ambiguous = named5(row.guard) && ["decide", "remove", "save"].includes(row.role) ? ambiguousStatus(name, row, model) : null;
13662
14232
  if (ambiguous !== null) {
13663
14233
  problems.push(ambiguous);
13664
14234
  } else if (row.role === "decide" && statusColumn(model, row) === null) {
@@ -13667,17 +14237,17 @@ function modelProblems(elements, tree, handlers, model, roles, sources = {}) {
13667
14237
  pointer: `/data/app/handlers/${escapePointer3(name)}/guard`,
13668
14238
  message: `handler "${name}" decides ${JSON.stringify(decisionTargets(row))} from "${row.guard}", and table "${row.table}" has no column whose enum holds both; a decision needs a status column to move`
13669
14239
  });
13670
- } else if (row.role === "decide" && isPlainObject5(row.decision)) {
14240
+ } else if (row.role === "decide" && isPlainObject6(row.decision)) {
13671
14241
  problems.push(...decisionProblems(name, row, model.tables[row.table], statusColumn(model, row)));
13672
14242
  }
13673
- if (ambiguous === null && row.role === "save" && named4(row.guard) && statusColumn(model, row) !== null && Array.isArray(row.fields) && row.fields.includes(statusColumn(model, row))) {
14243
+ if (ambiguous === null && row.role === "save" && named5(row.guard) && statusColumn(model, row) !== null && Array.isArray(row.fields) && row.fields.includes(statusColumn(model, row))) {
13674
14244
  problems.push({
13675
14245
  code: "app_model_mismatch",
13676
14246
  pointer: `/data/app/handlers/${escapePointer3(name)}/fields`,
13677
14247
  message: `handler "${name}" guards its edits on "${row.guard}" and also takes "${statusColumn(model, row)}" off the payload, so an edit could move the row to any state; drop "${statusColumn(model, row)}" from fields and move the row with a decide`
13678
14248
  });
13679
14249
  }
13680
- if (ambiguous === null && (row.role === "remove" || row.role === "save") && named4(row.guard) && statusColumn(model, row) === null) {
14250
+ if (ambiguous === null && (row.role === "remove" || row.role === "save") && named5(row.guard) && statusColumn(model, row) === null) {
13681
14251
  problems.push({
13682
14252
  code: "app_model_mismatch",
13683
14253
  pointer: `/data/app/handlers/${escapePointer3(name)}/guard`,
@@ -13686,7 +14256,7 @@ function modelProblems(elements, tree, handlers, model, roles, sources = {}) {
13686
14256
  }
13687
14257
  if (!misKeyed && row.requires !== void 0) problems.push(...requiresProblems(name, row, model));
13688
14258
  if (row.role === "summary") problems.push(...measureProblems(name, row, model, handlers));
13689
- if ((row.role === "list" || row.role === "inbox") && isPlainObject5(row.where)) {
14259
+ if ((row.role === "list" || row.role === "inbox") && isPlainObject6(row.where)) {
13690
14260
  problems.push(...whereProblems(row.where, model.tables[row.table], (key) => `/data/app/handlers/${escapePointer3(name)}/where/${escapePointer3(key)}`, `handler "${name}"`));
13691
14261
  }
13692
14262
  if (!misKeyed && row.not_own === true) {
@@ -13723,20 +14293,20 @@ function platformQueryProblems(name, row, table) {
13723
14293
  message: `handler "${name}" ${key === "order_by" ? "sorts" : "filters"} on platform column "${column}", and ${why}`
13724
14294
  });
13725
14295
  };
13726
- if (named4(row.order_by)) {
14296
+ if (named5(row.order_by)) {
13727
14297
  const column = row.order_by.replace(/^-+/, "");
13728
14298
  if (column.startsWith("_") && !allowed.has(column)) refuse("order_by", column);
13729
14299
  }
13730
- for (const key of Object.keys(isPlainObject5(row.where) ? row.where : {}).sort()) {
14300
+ for (const key of Object.keys(isPlainObject6(row.where) ? row.where : {}).sort()) {
13731
14301
  const column = key.split("__")[0];
13732
14302
  if (column.startsWith("_") && !allowed.has(column)) refuse("where", column);
13733
14303
  }
13734
14304
  return problems;
13735
14305
  }
13736
14306
  function linkedParent(row, model) {
13737
- if (!named4(row?.by) || !BY_ROLES.includes(row.role)) return null;
14307
+ if (!named5(row?.by) || !BY_ROLES.includes(row.role)) return null;
13738
14308
  const column = model.tables[row.table]?.columns?.[row.by];
13739
- if (!named4(column?.ref)) return null;
14309
+ if (!named5(column?.ref)) return null;
13740
14310
  const target = String(column.ref).split(".")[0];
13741
14311
  return Object.prototype.hasOwnProperty.call(model.tables, target) ? target : null;
13742
14312
  }
@@ -13758,13 +14328,13 @@ function byProblems(elements, tree, handlers, model) {
13758
14328
  say(`handler "${name}" states by as ${JSON.stringify(row.by)}; state by as ${KEY_SHAPE.by.says}`);
13759
14329
  continue;
13760
14330
  }
13761
- if (!named4(row.table) || !Object.prototype.hasOwnProperty.call(model.tables, row.table)) continue;
14331
+ if (!named5(row.table) || !Object.prototype.hasOwnProperty.call(model.tables, row.table)) continue;
13762
14332
  const column = model.tables[row.table].columns[row.by];
13763
14333
  if (column === void 0) {
13764
14334
  say(`handler "${name}" is filtered by "${row.by}", which is no column of table "${row.table}"`);
13765
14335
  continue;
13766
14336
  }
13767
- if (!named4(column.ref)) {
14337
+ if (!named5(column.ref)) {
13768
14338
  say(
13769
14339
  `handler "${name}" is filtered by "${row.by}", and "${row.table}.${row.by}" is not a ref: by is a link to the parent row, so the column has to be a ref to the parent table (e.g. "ref": "invoices.id")`
13770
14340
  );
@@ -13791,7 +14361,7 @@ function byProblems(elements, tree, handlers, model) {
13791
14361
  const element = elements[uid] ?? {};
13792
14362
  for (const key of ["source", "action"]) {
13793
14363
  const name = element.props?.[key];
13794
- if (!named4(name) || !linked.has(name)) continue;
14364
+ if (!named5(name) || !linked.has(name)) continue;
13795
14365
  const target = linked.get(name);
13796
14366
  const by = handlers[name].by;
13797
14367
  if (element.type === "field" && key === "source") {
@@ -13851,7 +14421,7 @@ function historyProblems(elements, tree, handlers, model) {
13851
14421
  if (row.limit !== void 0 && !(Number.isInteger(row.limit) && row.limit > 0)) {
13852
14422
  say("limit", `handler "${name}" states limit as ${JSON.stringify(row.limit)}; state limit as a positive whole number, or omit it for the platform's 100`);
13853
14423
  }
13854
- if (!named4(row.table) || !Object.prototype.hasOwnProperty.call(model.tables, row.table)) continue;
14424
+ if (!named5(row.table) || !Object.prototype.hasOwnProperty.call(model.tables, row.table)) continue;
13855
14425
  if (model.tables[row.table].audit !== true) {
13856
14426
  say(
13857
14427
  "table",
@@ -13867,7 +14437,7 @@ function historyProblems(elements, tree, handlers, model) {
13867
14437
  const props = element.props ?? {};
13868
14438
  const pointer2 = (key) => `/elements/${escapePointer3(uid)}/props/${key}`;
13869
14439
  const say = (key, message) => problems.push({ code: "app_model_mismatch", pointer: pointer2(key), message });
13870
- const source = named4(props.source) ? props.source : null;
14440
+ const source = named5(props.source) ? props.source : null;
13871
14441
  const history = source !== null && handlers[source]?.role === "history";
13872
14442
  if (element.type === "list" && isPlatformTable(props.entity) && !history) {
13873
14443
  say("source", `list "${uid}" shows "${props.entity}", which only a history handler reads \u2014 source it from a handler with role "history"`);
@@ -13938,7 +14508,7 @@ function numberPinProblems(elements, pins) {
13938
14508
  const need = typeof PLATFORM_NUMBERS2[module] === "string" ? `${module}@${PLATFORM_NUMBERS2[module]}` : `a ${module} release that draws it`;
13939
14509
  const walk4 = (node, at) => {
13940
14510
  if (Array.isArray(node)) return node.forEach((child, i) => walk4(child, `${at}/${i}`));
13941
- if (!isPlainObject5(node)) return;
14511
+ if (!isPlainObject6(node)) return;
13942
14512
  for (const [key, value] of Object.entries(node)) {
13943
14513
  const pointer2 = `${at}/${escapePointer3(key)}`;
13944
14514
  const say = (what) => problems.push({
@@ -13966,7 +14536,7 @@ function whenPinProblems(elements, pins) {
13966
14536
  if (typeof pin !== "string" || typeof first === "string" && pinAtLeast(pin, first)) continue;
13967
14537
  const actions = element.props?.props?.row_actions;
13968
14538
  for (const [i, action] of (Array.isArray(actions) ? actions : []).entries()) {
13969
- if (!isPlainObject5(action) || action.when === void 0 || action.when === null) continue;
14539
+ if (!isPlainObject6(action) || action.when === void 0 || action.when === null) continue;
13970
14540
  problems.push({
13971
14541
  code: "when_unsupported",
13972
14542
  pointer: `/elements/${escapePointer3(uid)}/props/props/row_actions/${i}/when`,
@@ -13981,7 +14551,7 @@ function propsPinProblems(elements, pins) {
13981
14551
  for (const uid of Object.keys(elements).sort(natural3)) {
13982
14552
  const element = elements[uid] ?? {};
13983
14553
  const gates = PROPS_SINCE2?.[element.type];
13984
- if (!isPlainObject5(gates)) continue;
14554
+ if (!isPlainObject6(gates)) continue;
13985
14555
  const module = MODULE_OF2[element.type];
13986
14556
  const pin = pins[module];
13987
14557
  if (typeof pin !== "string") continue;
@@ -13993,7 +14563,7 @@ function propsPinProblems(elements, pins) {
13993
14563
  const key = many ? step.slice(0, -2) : step;
13994
14564
  const next = [];
13995
14565
  for (const [at, node] of reached) {
13996
- if (!isPlainObject5(node) || node[key] === void 0 || node[key] === null) continue;
14566
+ if (!isPlainObject6(node) || node[key] === void 0 || node[key] === null) continue;
13997
14567
  const pointer2 = `${at}/${escapePointer3(key)}`;
13998
14568
  if (!many) next.push([pointer2, node[key]]);
13999
14569
  else if (Array.isArray(node[key])) node[key].forEach((item, i) => next.push([`${pointer2}/${i}`, item]));
@@ -14064,7 +14634,7 @@ function chartProblems(elements, tree, handlers, model) {
14064
14634
  say("source", `reads rows, and "${props.source}" has role ${row.role ?? "none"}, ${answers}; source a list, an inbox, or a custom handler that answers {"rows": [...]}`);
14065
14635
  continue;
14066
14636
  }
14067
- if (named4(row.by) && BY_ROLES.includes(row.role)) {
14637
+ if (named5(row.by) && BY_ROLES.includes(row.role)) {
14068
14638
  say(
14069
14639
  "source",
14070
14640
  `reads "${props.source}", which reads the children of an opened row (by "${row.by}"), and a chart sits at the top of its page with no row to name; source a handler with no by, or return every row's points from one custom handler with a series column`
@@ -14110,7 +14680,7 @@ var TYPED_PROP_KINDS = Object.freeze({
14110
14680
  var ORDERED_KINDS = Object.freeze(["waterfall", "waterfall-breakdown", "waterfall-walkthrough"]);
14111
14681
  function widgetProblems(elements, tree, handlers, sources, widgetsPin = WIDGETS) {
14112
14682
  const problems = [];
14113
- const declaredSources = isPlainObject5(sources) ? sources : {};
14683
+ const declaredSources = isPlainObject6(sources) ? sources : {};
14114
14684
  for (const uid of Object.keys(elements).sort(natural3)) {
14115
14685
  const element = elements[uid] ?? {};
14116
14686
  if (element.type !== "widget") continue;
@@ -14137,11 +14707,11 @@ function widgetProblems(elements, tree, handlers, sources, widgetsPin = WIDGETS)
14137
14707
  mismatch(flat[0], `states ${flat.map((key) => JSON.stringify(key)).join(", ")} at the top of its props; a widget's typed props sit under props.props ({"props": {"${flat[0]}": ...}}), which the page carries as data-dr-props`);
14138
14708
  continue;
14139
14709
  }
14140
- if (given(props.props) && !isPlainObject5(props.props)) {
14710
+ if (given(props.props) && !isPlainObject6(props.props)) {
14141
14711
  mismatch("props", `states props ${JSON.stringify(props.props)}; a widget's typed props are an object ({"props": {"goal": 1000}}), written as data-dr-props`);
14142
14712
  continue;
14143
14713
  }
14144
- const typed = isPlainObject5(props.props) ? props.props : {};
14714
+ const typed = isPlainObject6(props.props) ? props.props : {};
14145
14715
  const finance = APP_WIDGET_KINDS2.includes(props.kind);
14146
14716
  const smart = given(typed.variant) || given(typed.compare);
14147
14717
  const pinned = String(widgetsPin ?? "").slice(String(widgetsPin ?? "").lastIndexOf("@") + 1);
@@ -14193,7 +14763,7 @@ function widgetProblems(elements, tree, handlers, sources, widgetsPin = WIDGETS)
14193
14763
  }
14194
14764
  const source = declaredSources[props.source];
14195
14765
  const handled = Object.prototype.hasOwnProperty.call(handlers, props.source);
14196
- if (!handled && !isPlainObject5(source)) continue;
14766
+ if (!handled && !isPlainObject6(source)) continue;
14197
14767
  const row = handlers[props.source] ?? {};
14198
14768
  if (handled && !WIDGET_SOURCE_ROLES.includes(row.role)) {
14199
14769
  const answers = row.role === "summary" ? "which answers one number per measure" : row.role === "history" ? "which answers one row's events" : row.role === "list" || row.role === "inbox" ? "which answers rows" : "which answers no payload";
@@ -14203,8 +14773,8 @@ function widgetProblems(elements, tree, handlers, sources, widgetsPin = WIDGETS)
14203
14773
  );
14204
14774
  continue;
14205
14775
  }
14206
- if (!isPlainObject5(source)) continue;
14207
- const shape = named4(source.shape) ? source.shape : "rows";
14776
+ if (!isPlainObject6(source)) continue;
14777
+ const shape = named5(source.shape) ? source.shape : "rows";
14208
14778
  const legacy = !finance;
14209
14779
  const shaped = (message) => problems.push({ code: "widget_source_shape", pointer: `/elements/${escapePointer3(uid)}/props/source`, message: `widget "${uid}" ${message}`, ...legacy ? { advisory: true } : {} });
14210
14780
  if (shape === "rows") {
@@ -14241,21 +14811,21 @@ var CONTROL_KINDS2 = {
14241
14811
  };
14242
14812
  var TOGGLE_CALL_ROLES = ["custom"];
14243
14813
  function rowTableOf(source, handlers, sources, model) {
14244
- if (!model || !named4(source)) return null;
14814
+ if (!model || !named5(source)) return null;
14245
14815
  let table = null;
14246
14816
  if (Object.prototype.hasOwnProperty.call(handlers, source)) {
14247
14817
  const row = handlers[source] ?? {};
14248
- if (["list", "inbox"].includes(row.role) && named4(row.table)) table = row.table;
14249
- } else if (isPlainObject5(sources) && isPlainObject5(sources[source])) {
14818
+ if (["list", "inbox"].includes(row.role) && named5(row.table)) table = row.table;
14819
+ } else if (isPlainObject6(sources) && isPlainObject6(sources[source])) {
14250
14820
  const declared = sources[source];
14251
- if (!stated(declared.transform) && isPlainObject5(declared.from) && named4(declared.from.table)) table = declared.from.table;
14821
+ if (!stated(declared.transform) && isPlainObject6(declared.from) && named5(declared.from.table)) table = declared.from.table;
14252
14822
  }
14253
14823
  if (table === null) return null;
14254
14824
  return isPlatformTable(table) || Object.prototype.hasOwnProperty.call(model.tables, table) ? table : null;
14255
14825
  }
14256
14826
  var PK_WRITING_ROLES = ["remove", "save", "decide"];
14257
14827
  function pkWriterTable(row, model) {
14258
- if (!model || !isPlainObject5(row) || !PK_WRITING_ROLES.includes(row.role) || !named4(row.table)) return null;
14828
+ if (!model || !isPlainObject6(row) || !PK_WRITING_ROLES.includes(row.role) || !named5(row.table)) return null;
14259
14829
  if (!Object.prototype.hasOwnProperty.call(model.tables, row.table)) return null;
14260
14830
  return primaryKeyOf(model, row.table) === null ? null : row.table;
14261
14831
  }
@@ -14265,17 +14835,17 @@ function primaryKeyOf(model, table) {
14265
14835
  }
14266
14836
  function partProblems(elements, tree, handlers, sources, pages = [], model = null, pins = {}) {
14267
14837
  const problems = [];
14268
- const declaredSources = isPlainObject5(sources) ? sources : {};
14838
+ const declaredSources = isPlainObject6(sources) ? sources : {};
14269
14839
  const paramsOf4 = (path3) => {
14270
14840
  const page = (Array.isArray(pages) ? pages : []).find((one) => one?.path === path3);
14271
- return Array.isArray(page?.params) ? page.params.filter((one) => isPlainObject5(one) && named4(one.name)) : [];
14841
+ return Array.isArray(page?.params) ? page.params.filter((one) => isPlainObject6(one) && named5(one.name)) : [];
14272
14842
  };
14273
14843
  const rowDialogs = /* @__PURE__ */ new Set();
14274
14844
  const dialogsById = /* @__PURE__ */ new Map();
14275
14845
  for (const uid of Object.keys(elements).sort(natural3)) {
14276
14846
  const element = elements[uid];
14277
- const typed = isPlainObject5(element?.props?.props) ? element.props.props : {};
14278
- if (element?.type === "dialog" && typed.values_from === "row" && named4(element.props?.id)) {
14847
+ const typed = isPlainObject6(element?.props?.props) ? element.props.props : {};
14848
+ if (element?.type === "dialog" && typed.values_from === "row" && named5(element.props?.id)) {
14279
14849
  const where2 = `${element.page}|${element.props.id}`;
14280
14850
  rowDialogs.add(where2);
14281
14851
  if (!dialogsById.has(where2)) dialogsById.set(where2, { ...element, uid });
@@ -14286,7 +14856,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14286
14856
  if (!isPart2(element)) continue;
14287
14857
  const type = element.type;
14288
14858
  const entry = PARTS[type];
14289
- const props = isPlainObject5(element.props) ? element.props : {};
14859
+ const props = isPlainObject6(element.props) ? element.props : {};
14290
14860
  const at = `/elements/${escapePointer3(uid)}`;
14291
14861
  const say = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message: `${type} "${props.id ?? uid}" ${message}` });
14292
14862
  const schemaLines = validate2(props, PROPS_SCHEMAS[type], { quote: true });
@@ -14307,7 +14877,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14307
14877
  say("part_children", `/elements/${escapePointer3(kid)}`, `holds "${kid}" (a ${elements[kid]?.type ?? "element"}), and a ${type} holds only ${either3(entry.children)}`);
14308
14878
  }
14309
14879
  }
14310
- const typedProps = isPlainObject5(props.props) ? props.props : {};
14880
+ const typedProps = isPlainObject6(props.props) ? props.props : {};
14311
14881
  const own = PROPS_SCHEMAS[type]?.properties?.props?.properties ?? {};
14312
14882
  if ("field" in own && "param" in own) {
14313
14883
  const field = stated(typedProps.field);
@@ -14371,11 +14941,11 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14371
14941
  }
14372
14942
  }
14373
14943
  const formAbove = () => {
14374
- for (let up = parentOf(tree, uid); named4(up) && up in elements; up = parentOf(tree, up)) if (elements[up]?.type === "form") return up;
14944
+ for (let up = parentOf(tree, uid); named5(up) && up in elements; up = parentOf(tree, up)) if (elements[up]?.type === "form") return up;
14375
14945
  return null;
14376
14946
  };
14377
14947
  const dialogAbove = () => {
14378
- for (let up = parentOf(tree, uid); named4(up) && up in elements; up = parentOf(tree, up)) if (elements[up]?.type === "dialog") return up;
14948
+ for (let up = parentOf(tree, uid); named5(up) && up in elements; up = parentOf(tree, up)) if (elements[up]?.type === "dialog") return up;
14379
14949
  return null;
14380
14950
  };
14381
14951
  if (type === "file_upload") {
@@ -14393,7 +14963,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14393
14963
  if ((type === "checkbox" || type === "toggle") && stated(typedProps.field)) {
14394
14964
  const form = formAbove();
14395
14965
  const table = form !== null ? handlers[elements[form].props?.action]?.table ?? elements[form].props?.entity : null;
14396
- const column = named4(table) ? model?.tables?.[table]?.columns?.[typedProps.field] : null;
14966
+ const column = named5(table) ? model?.tables?.[table]?.columns?.[typedProps.field] : null;
14397
14967
  if (form !== null && column?.type === "bool") {
14398
14968
  say(
14399
14969
  "part_placement",
@@ -14414,10 +14984,10 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14414
14984
  }
14415
14985
  const nested = [];
14416
14986
  if (type === "button" && Array.isArray(typedProps.split)) {
14417
- typedProps.split.forEach((item, index) => isPlainObject5(item) && stated(item.on_click) && nested.push([item.on_click, `${at}/props/props/split/${index}/on_click`, `split entry "${item.label ?? index}"`, entry.events?.on_click ?? []]));
14987
+ typedProps.split.forEach((item, index) => isPlainObject6(item) && stated(item.on_click) && nested.push([item.on_click, `${at}/props/props/split/${index}/on_click`, `split entry "${item.label ?? index}"`, entry.events?.on_click ?? []]));
14418
14988
  }
14419
14989
  if (type === "stepper" && Array.isArray(typedProps.steps)) {
14420
- typedProps.steps.forEach((step, index) => isPlainObject5(step) && stated(step.action) && nested.push([step.action, `${at}/props/props/steps/${index}/action`, `step "${step.id ?? index}"`, entry.events?.on_step ?? []]));
14990
+ typedProps.steps.forEach((step, index) => isPlainObject6(step) && stated(step.action) && nested.push([step.action, `${at}/props/props/steps/${index}/action`, `step "${step.id ?? index}"`, entry.events?.on_step ?? []]));
14421
14991
  }
14422
14992
  for (const [name, pointer2, what, roles] of nested) {
14423
14993
  if (!Object.prototype.hasOwnProperty.call(handlers, name)) {
@@ -14432,19 +15002,19 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14432
15002
  if (type === "dialog" && !stated(typedProps.trigger) && typedProps.values_from !== "row") {
14433
15003
  say("part_mode", `${at}/props/props`, `states neither props.trigger nor values_from: "row", so nothing could open it; give it a trigger, or values_from "row" for a grid's row action to open`);
14434
15004
  }
14435
- if (type === "data_grid" && Array.isArray(typedProps.row_actions) && typedProps.row_actions.some(isPlainObject5)) {
15005
+ if (type === "data_grid" && Array.isArray(typedProps.row_actions) && typedProps.row_actions.some(isPlainObject6)) {
14436
15006
  const key = typedProps.key;
14437
15007
  if (!stated(key)) {
14438
15008
  say("part_row_key", `${at}/props/props`, "has row_actions and states no props.key; a row action posts the row's key as id, so name the column that identifies a row (id, for a table's own rows)");
14439
15009
  } else {
14440
15010
  const writers = [];
14441
15011
  typedProps.row_actions.forEach((action, index) => {
14442
- if (!isPlainObject5(action)) return;
15012
+ if (!isPlainObject6(action)) return;
14443
15013
  if (stated(action.call)) {
14444
15014
  writers.push({ handler: action.call, key, pointer: `${at}/props/props/key`, via: `row action "${action.label ?? index}"` });
14445
15015
  } else if (stated(action.dialog)) {
14446
15016
  const dialog = dialogsById.get(`${element.page}|${action.dialog}`);
14447
- const own2 = isPlainObject5(dialog?.props?.props) ? dialog.props.props : null;
15017
+ const own2 = isPlainObject6(dialog?.props?.props) ? dialog.props.props : null;
14448
15018
  if (!own2 || !stated(own2.on_confirm)) return;
14449
15019
  const ownKey = stated(own2.key);
14450
15020
  writers.push({
@@ -14489,7 +15059,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14489
15059
  }
14490
15060
  }
14491
15061
  typedProps.row_actions.forEach((action, index) => {
14492
- if (!isPlainObject5(action)) return;
15062
+ if (!isPlainObject6(action)) return;
14493
15063
  const pointer2 = `${at}/props/props/row_actions/${index}`;
14494
15064
  if (stated(action.dialog) && !rowDialogs.has(`${element.page}|${action.dialog}`)) {
14495
15065
  say("part_row_action", `${pointer2}/dialog`, `row action "${action.label ?? index}" opens dialog "${action.dialog}", and ${element.page} holds no dialog of that id with values_from: "row"; add one (its inputs start from the row, it posts id with them), or name the one there is`);
@@ -14530,7 +15100,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14530
15100
  const names = declared.map((one) => `"${one.name}"`);
14531
15101
  const filters = type === "filter_bar" ? Array.isArray(typedProps.filters) ? typedProps.filters.map((filter, i) => [filter, `${at}/props/props/filters/${i}`]) : [] : [[typedProps, `${at}/props/props`]];
14532
15102
  for (const [filter, base] of filters) {
14533
- if (!isPlainObject5(filter) || !stated(filter.param)) continue;
15103
+ if (!isPlainObject6(filter) || !stated(filter.param)) continue;
14534
15104
  const who = type === "filter_bar" ? `filter "${filter.label ?? filter.param}"` : "filter";
14535
15105
  const multiple = filter.kind === "option" && filter.multiple === true;
14536
15106
  const kinds = filter.kind === "option" ? multiple ? ["multi"] : ["select", "period", "month"] : filter.kind === "date-range" ? ["period", "month"] : ["month", "period"];
@@ -14573,7 +15143,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14573
15143
  const columns = Array.isArray(typedProps.columns) ? typedProps.columns : [];
14574
15144
  const rowTable = rowTableOf(props.source, handlers, declaredSources, model);
14575
15145
  const carries = (column) => rowTable === null || hasColumn(model, rowTable, column);
14576
- const hierarchy = isPlainObject5(typedProps.hierarchy) ? typedProps.hierarchy : null;
15146
+ const hierarchy = isPlainObject6(typedProps.hierarchy) ? typedProps.hierarchy : null;
14577
15147
  if (hierarchy) {
14578
15148
  const hp = `${at}/props/props/hierarchy`;
14579
15149
  if (stated(hierarchy.label) && !columns.some((column) => column?.field === hierarchy.label)) {
@@ -14590,7 +15160,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14590
15160
  }
14591
15161
  }
14592
15162
  columns.forEach((column, index) => {
14593
- if (!isPlainObject5(column)) return;
15163
+ if (!isPlainObject6(column)) return;
14594
15164
  const cp2 = `${at}/props/props/columns/${index}`;
14595
15165
  if (stated(column.link_field)) {
14596
15166
  if (column.cell !== "link") say("part_cell", `${cp2}/link_field`, `column "${column.field}" states link_field and draws no link; add cell: "link", or drop link_field`);
@@ -14624,14 +15194,14 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14624
15194
  if (!entry.reads.roles.includes(role)) {
14625
15195
  say("part_source_shape", `${at}/props/source`, `reads rows, and "${source}" has role ${role ?? "none"}, which answers no rows a ${type} draws; source a ${either3(entry.reads.roles)} handler, or a data.sources entry with shape rows`);
14626
15196
  }
14627
- } else if (isPlainObject5(declaredSources[source])) {
14628
- const shape = named4(declaredSources[source].shape) ? declaredSources[source].shape : "rows";
15197
+ } else if (isPlainObject6(declaredSources[source])) {
15198
+ const shape = named5(declaredSources[source].shape) ? declaredSources[source].shape : "rows";
14629
15199
  if (shape !== "rows") {
14630
15200
  say("part_source_shape", `${at}/props/source`, `reads rows, and source "${source}" answers ${shape} (its shape); a ${shape} body is what a widget draws \u2014 give the source shape rows`);
14631
15201
  }
14632
15202
  }
14633
15203
  }
14634
- const typed = isPlainObject5(props.props) ? props.props : {};
15204
+ const typed = isPlainObject6(props.props) ? props.props : {};
14635
15205
  for (const [prop, roles] of Object.entries(entry.events ?? {})) {
14636
15206
  if (prop === "on_row_action") continue;
14637
15207
  const name = typed[prop];
@@ -14652,7 +15222,7 @@ function partProblems(elements, tree, handlers, sources, pages = [], model = nul
14652
15222
  var ROWLESS_EVENTS = /* @__PURE__ */ new Set(["button.on_click", "empty_state.on_action"]);
14653
15223
  function rowlessRefusal(name, handlers) {
14654
15224
  const row = handlers[name] ?? {};
14655
- const required = Array.isArray(row.required) ? row.required.filter(named4) : [];
15225
+ const required = Array.isArray(row.required) ? row.required.filter(named5) : [];
14656
15226
  if (row.role === "submit" && required.length > 0) {
14657
15227
  return `a submit handler that requires ${required.map((key) => `"${key}"`).join(", ")}, and the part posts none of them \u2014 it would refuse every click; call a custom handler, or a submit with nothing required`;
14658
15228
  }
@@ -14662,11 +15232,11 @@ function fillChartFormats(elements, handlers, model) {
14662
15232
  let out = null;
14663
15233
  for (const uid of Object.keys(elements)) {
14664
15234
  const element = elements[uid];
14665
- if (element?.type !== "chart" || named4(element.props?.format)) continue;
15235
+ if (element?.type !== "chart" || named5(element.props?.format)) continue;
14666
15236
  const source = element.props?.source;
14667
- const row = named4(source) && Object.prototype.hasOwnProperty.call(handlers, source) ? handlers[source] ?? {} : {};
14668
- const table = named4(row.table) && Object.prototype.hasOwnProperty.call(model.tables, row.table) ? model.tables[row.table] : null;
14669
- const column = named4(element.props?.y) && table !== null && Object.prototype.hasOwnProperty.call(table.columns, element.props.y) ? table.columns[element.props.y] : null;
15237
+ const row = named5(source) && Object.prototype.hasOwnProperty.call(handlers, source) ? handlers[source] ?? {} : {};
15238
+ const table = named5(row.table) && Object.prototype.hasOwnProperty.call(model.tables, row.table) ? model.tables[row.table] : null;
15239
+ const column = named5(element.props?.y) && table !== null && Object.prototype.hasOwnProperty.call(table.columns, element.props.y) ? table.columns[element.props.y] : null;
14670
15240
  const format = row.role !== "custom" && isDecimalType(column?.type) ? "currency" : "number";
14671
15241
  out ??= { ...elements };
14672
15242
  out[uid] = { ...element, props: { ...element.props, format } };
@@ -14675,10 +15245,10 @@ function fillChartFormats(elements, handlers, model) {
14675
15245
  }
14676
15246
  function dropEmptyArgs(elements) {
14677
15247
  let out = null;
14678
- const empty = (value) => value === null || isPlainObject5(value) && Object.keys(value).length === 0;
15248
+ const empty = (value) => value === null || isPlainObject6(value) && Object.keys(value).length === 0;
14679
15249
  for (const uid of Object.keys(elements)) {
14680
15250
  const element = elements[uid];
14681
- if (element?.type !== "custom" || !isPlainObject5(element.props)) continue;
15251
+ if (element?.type !== "custom" || !isPlainObject6(element.props)) continue;
14682
15252
  const drop = ["args", "declares"].filter((key) => key in element.props && empty(element.props[key]));
14683
15253
  if (drop.length === 0) continue;
14684
15254
  const props = { ...element.props };
@@ -14708,8 +15278,8 @@ function computedProblems(elements, tree, handlers, model) {
14708
15278
  const base = `/app/data_model/tables/${escapePointer3(table)}/columns/${escapePointer3(name)}`;
14709
15279
  const said2 = (message, key = "computed") => push(`${base}/${key}`, `"${table}.${name}" ${message}`);
14710
15280
  if (column.matches !== null) {
14711
- const other = named4(column.matches) ? columns[column.matches] : void 0;
14712
- if (!named4(column.matches)) said2(`states matches as ${JSON.stringify(column.matches)}; matches names a column of table "${table}"`, "matches");
15281
+ const other = named5(column.matches) ? columns[column.matches] : void 0;
15282
+ if (!named5(column.matches)) said2(`states matches as ${JSON.stringify(column.matches)}; matches names a column of table "${table}"`, "matches");
14713
15283
  else if (column.computed === null) said2(`states matches, which says which column a computed value should agree with, and "${name}" computes nothing; drop matches, or make "${name}" computed`, "matches");
14714
15284
  else if (column.matches === name) said2('matches itself; name the column its total should agree with, e.g. "subtotal"', "matches");
14715
15285
  else if (other === void 0) said2(`matches "${column.matches}", which is no column of table "${table}"`, "matches");
@@ -14718,7 +15288,7 @@ function computedProblems(elements, tree, handlers, model) {
14718
15288
  }
14719
15289
  if (column.computed === null) continue;
14720
15290
  const spec = column.computed;
14721
- if (!isPlainObject5(spec)) {
15291
+ if (!isPlainObject6(spec)) {
14722
15292
  said2(`states computed as ${JSON.stringify(spec)}; computed is {"sum": "<child table>.<number column>", "by": "<the child's ref column to ${table}>"}`);
14723
15293
  continue;
14724
15294
  }
@@ -14735,7 +15305,7 @@ function computedProblems(elements, tree, handlers, model) {
14735
15305
  if (stated2) said2(`is computed and states ${key === "nullable" ? "nullable: false" : key}; ${why} \u2014 drop ${key}`);
14736
15306
  }
14737
15307
  if (model.tables[table].indexes.some((index) => index.includes(name))) said2("is computed and named in an index; the platform stores no such column, so there is nothing to index \u2014 drop it from indexes");
14738
- if (fns.length !== 1 || !COMPUTED_FNS.includes(fns[0]) || !named4(spec.by)) {
15308
+ if (fns.length !== 1 || !COMPUTED_FNS.includes(fns[0]) || !named5(spec.by)) {
14739
15309
  said2(`states computed as ${JSON.stringify(spec)}; computed is {"sum": "<child table>.<number column>", "by": "<the child's ref column to ${table}>"}`);
14740
15310
  continue;
14741
15311
  }
@@ -14776,17 +15346,17 @@ function computedProblems(elements, tree, handlers, model) {
14776
15346
  for (const key of ["fields", "required"]) {
14777
15347
  for (const hit of (Array.isArray(row[key]) ? row[key] : []).filter((column) => computed.has(column))) refuse(at(key), hit, key === "fields" ? "takes" : "requires");
14778
15348
  }
14779
- for (const fixed of Object.keys(isPlainObject5(row.fixed) ? row.fixed : {}).filter((column) => computed.has(column))) refuse(at("fixed"), fixed, "fixes");
15349
+ for (const fixed of Object.keys(isPlainObject6(row.fixed) ? row.fixed : {}).filter((column) => computed.has(column))) refuse(at("fixed"), fixed, "fixes");
14780
15350
  }
14781
15351
  }
14782
15352
  if (tree.problems.length > 0) return problems;
14783
15353
  for (const uid of Object.keys(elements).sort(natural3)) {
14784
15354
  const element = elements[uid] ?? {};
14785
- if (element.type !== "field" || !named4(element.props?.field)) continue;
15355
+ if (element.type !== "field" || !named5(element.props?.field)) continue;
14786
15356
  const entity = entityOf(uid, elements, tree);
14787
15357
  if (entity === null || !computedIn(entity).has(element.props.field)) continue;
14788
15358
  const holder = elements[parentOf(tree, uid)] ?? null;
14789
- const edits = holder?.type === "form" || holder?.type === "detail" && named4(holder.props?.action);
15359
+ const edits = holder?.type === "form" || holder?.type === "detail" && named5(holder.props?.action);
14790
15360
  if (edits) {
14791
15361
  push(
14792
15362
  `/elements/${escapePointer3(uid)}/props/field`,
@@ -14809,7 +15379,7 @@ function whenProblems(elements, tree, model) {
14809
15379
  push(`"${uid}" states when, and when says which rows a row action shows on: only an action inside a list or a queue has a row \u2014 drop when`);
14810
15380
  continue;
14811
15381
  }
14812
- if (!isPlainObject5(when) || Object.keys(when).length === 0) {
15382
+ if (!isPlainObject6(when) || Object.keys(when).length === 0) {
14813
15383
  push(`"${uid}" states when as ${JSON.stringify(when)}; when is an object of column to the list of values the row may hold \u2014 {"status": ["open", "in_progress"]}`);
14814
15384
  continue;
14815
15385
  }
@@ -14817,7 +15387,7 @@ function whenProblems(elements, tree, model) {
14817
15387
  for (const column of Object.keys(when).sort()) {
14818
15388
  const states = when[column];
14819
15389
  const pointer2 = `${at}/${escapePointer3(column)}`;
14820
- if (!Array.isArray(states) || states.length === 0 || !states.every(named4)) {
15390
+ if (!Array.isArray(states) || states.length === 0 || !states.every(named5)) {
14821
15391
  push(`"${uid}" states when.${column} as ${JSON.stringify(states)}; each column takes a non-empty list of the values it may hold, as strings \u2014 even one: ["open"]`, pointer2);
14822
15392
  continue;
14823
15393
  }
@@ -14842,7 +15412,7 @@ function pickerProblems(elements, tree, handlers, model) {
14842
15412
  for (const [name, column] of Object.entries(model.tables[table].columns)) {
14843
15413
  if (column.display === null) continue;
14844
15414
  const pointer2 = `/app/data_model/tables/${escapePointer3(table)}/columns/${escapePointer3(name)}/display`;
14845
- if (!named4(column.ref)) {
15415
+ if (!named5(column.ref)) {
14846
15416
  push(pointer2, `display says which column of the referenced table to show, and "${table}.${name}" references nothing; drop display, or make "${name}" a ref`);
14847
15417
  continue;
14848
15418
  }
@@ -14856,19 +15426,19 @@ function pickerProblems(elements, tree, handlers, model) {
14856
15426
  for (const uid of Object.keys(elements).sort(natural3)) {
14857
15427
  const element = elements[uid] ?? {};
14858
15428
  const props = element.props ?? {};
14859
- if (element.type !== "field" || !named4(props.source)) continue;
15429
+ if (element.type !== "field" || !named5(props.source)) continue;
14860
15430
  const entity = entityOf(uid, elements, tree);
14861
15431
  const column = entity === null ? void 0 : model.tables[entity]?.columns?.[props.field];
14862
15432
  const handler = handlers[props.source];
14863
15433
  if (column === void 0 || handler === void 0) continue;
14864
15434
  const at = (key) => `/elements/${escapePointer3(uid)}/props/${key}`;
14865
15435
  const holder = elements[parentOf(tree, uid)] ?? null;
14866
- const isInput = holder?.type === "form" || holder?.type === "detail" && named4(holder.props?.action);
15436
+ const isInput = holder?.type === "form" || holder?.type === "detail" && named5(holder.props?.action);
14867
15437
  if (!isInput) {
14868
15438
  push(at("source"), `field "${uid}" names source "${props.source}", and a picker is a form input: under a ${holder?.type ?? "page"} a field is ${holder?.type === "detail" ? "a shown term" : "a column"}, which draws the linked row by its display and needs no source`);
14869
15439
  continue;
14870
15440
  }
14871
- if (!named4(column.ref)) {
15441
+ if (!named5(column.ref)) {
14872
15442
  push(at("source"), `field "${uid}" names source "${props.source}", and a picker draws the rows of the table a ref column points at; "${entity}.${props.field}" is not a ref column`);
14873
15443
  continue;
14874
15444
  }
@@ -14879,17 +15449,17 @@ function pickerProblems(elements, tree, handlers, model) {
14879
15449
  if (!["list", "inbox"].includes(handler.role) || handler.table !== target) {
14880
15450
  const what = ["list", "inbox"].includes(handler.role) ? `is over "${handler.table}"` : `is a ${handler.role ?? "custom"} handler`;
14881
15451
  push(at("source"), `"${props.source}" ${what}, and "${props.field}" references "${target}"; a picker's options come from a list or inbox over the table the ref points at`);
14882
- } else if (named4(handler.by)) {
15452
+ } else if (named5(handler.by)) {
14883
15453
  push(at("source"), `"${props.source}" is filtered by "${handler.by}", so a picker \u2014 which calls it with nothing \u2014 would always get no options; give the picker an unfiltered list over "${target}"`);
14884
15454
  }
14885
- if (named4(props.input_type) && props.input_type !== "select") {
15455
+ if (named5(props.input_type) && props.input_type !== "select") {
14886
15456
  push(at("input_type"), `field "${uid}" is a picker, which is drawn as a select, and states input_type "${props.input_type}"; drop input_type`);
14887
15457
  }
14888
15458
  const gate = Array.isArray(handler.requiresRole) ? handler.requiresRole.map(String) : [];
14889
15459
  if (gate.length > 0) {
14890
15460
  const marked2 = [];
14891
15461
  for (let current = uid; current !== null && elements[current] !== void 0; current = parentOf(tree, current)) {
14892
- if (named4(elements[current].props?.role)) marked2.push(elements[current].props.role);
15462
+ if (named5(elements[current].props?.role)) marked2.push(elements[current].props.role);
14893
15463
  }
14894
15464
  if (!marked2.some((role) => gate.includes(role))) {
14895
15465
  push(
@@ -14904,13 +15474,13 @@ function pickerProblems(elements, tree, handlers, model) {
14904
15474
  function decisionTargets(row) {
14905
15475
  if (row?.role !== void 0 && row.role !== "decide") return [];
14906
15476
  if (Array.isArray(row?.decision)) return row.decision.map(String);
14907
- if (isPlainObject5(row?.decision)) {
14908
- return Object.values(row.decision).filter((spec) => isPlainObject5(spec) && named4(spec.to)).map((spec) => String(spec.to));
15477
+ if (isPlainObject6(row?.decision)) {
15478
+ return Object.values(row.decision).filter((spec) => isPlainObject6(spec) && named5(spec.to)).map((spec) => String(spec.to));
14909
15479
  }
14910
15480
  return [];
14911
15481
  }
14912
15482
  function decisionNames(row) {
14913
- return Object.keys(isPlainObject5(row?.decision) ? row.decision : {}).sort(natural3);
15483
+ return Object.keys(isPlainObject6(row?.decision) ? row.decision : {}).sort(natural3);
14914
15484
  }
14915
15485
  function either3(names) {
14916
15486
  return names.length <= 1 ? names.join("") : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
@@ -14929,10 +15499,10 @@ function decisionProblems(name, row, table, status) {
14929
15499
  }
14930
15500
  }
14931
15501
  const set = spec.set === void 0 ? {} : spec.set;
14932
- if (!isPlainObject5(set)) {
15502
+ if (!isPlainObject6(set)) {
14933
15503
  say(at("set"), `states set as ${JSON.stringify(set)}; set is an object of column to the value the decision writes there \u2014 a literal, null, "$now", "$caller" or "$note"`);
14934
15504
  }
14935
- const sets = isPlainObject5(set) ? set : {};
15505
+ const sets = isPlainObject6(set) ? set : {};
14936
15506
  if (spec.note !== void 0) {
14937
15507
  if (spec.note !== "required" && spec.note !== "optional") {
14938
15508
  say(at("note"), `states note as ${JSON.stringify(spec.note)}; a note is "required" or "optional", and an absent note is optional`);
@@ -15024,7 +15594,7 @@ function requiresProblems(name, row, model) {
15024
15594
  say(at(column), `something of "${column}", which is no column of table "${row.table}"; requires is keyed by a ref column of the handler's own table`);
15025
15595
  continue;
15026
15596
  }
15027
- if (!named4(held.ref)) {
15597
+ if (!named5(held.ref)) {
15028
15598
  say(at(column), `something of "${column}", which references no table; requires reads the row a ref column names, so key it by one (e.g. "vendor_id")`);
15029
15599
  continue;
15030
15600
  }
@@ -15057,7 +15627,7 @@ function requiresProblems(name, row, model) {
15057
15627
  say(where2, `"${field}" of the ${singular(target)}, a ${targetColumn.type} column; a condition compares text, a whole number, true/false or an id \u2014 a ${targetColumn.type} read back never equals the value the contract states`);
15058
15628
  continue;
15059
15629
  }
15060
- const negated = isPlainObject5(wanted);
15630
+ const negated = isPlainObject6(wanted);
15061
15631
  if (negated && !(Object.keys(wanted).length === 1 && "not" in wanted && scalar(wanted.not))) {
15062
15632
  say(where2, `"${field}" to be ${JSON.stringify(wanted)}; a condition is a value the column must hold, or {"not": a value it must not}`);
15063
15633
  continue;
@@ -15077,7 +15647,7 @@ function requiresProblems(name, row, model) {
15077
15647
  function requiresOf(row, model) {
15078
15648
  const table = model.tables[row.table];
15079
15649
  const out = {};
15080
- for (const column of Object.keys(isPlainObject5(row.requires) ? row.requires : {}).sort(natural3)) {
15650
+ for (const column of Object.keys(isPlainObject6(row.requires) ? row.requires : {}).sort(natural3)) {
15081
15651
  const held = table.columns[column];
15082
15652
  const target = String(held.ref).split(".")[0];
15083
15653
  out[column] = [target, singular(target), held.display ?? null, row.requires[column]];
@@ -15085,11 +15655,11 @@ function requiresOf(row, model) {
15085
15655
  return out;
15086
15656
  }
15087
15657
  function requiresDoc(row) {
15088
- if (!isPlainObject5(row.requires)) return "";
15658
+ if (!isPlainObject6(row.requires)) return "";
15089
15659
  return "\n\n`requires` checks the row each ref column names first, read under this\ncaller's own scope: one this caller may not see, or none at all, refuses the\nsame way as one that fails its condition.";
15090
15660
  }
15091
15661
  function requiresConstants(row, model) {
15092
- if (!isPlainObject5(row.requires)) return [];
15662
+ if (!isPlainObject6(row.requires)) return [];
15093
15663
  const requires = requiresOf(row, model);
15094
15664
  return [assignment("REQUIRES", requires, Object.keys(requires))];
15095
15665
  }
@@ -15148,7 +15718,7 @@ var EVENTS_SHAPE = {
15148
15718
  };
15149
15719
  var PLATFORM_EVENT_HANDLERS = ["seed", "dry-run"];
15150
15720
  function isNewMeasure(measure) {
15151
- return isPlainObject5(measure) && Object.keys(measure).some((key) => MEASURE_WORDS.includes(key));
15721
+ return isPlainObject6(measure) && Object.keys(measure).some((key) => MEASURE_WORDS.includes(key));
15152
15722
  }
15153
15723
  function isSummable(type) {
15154
15724
  return type === "int" || type === "bigint" || typeof type === "string" && DECIMAL_RE.test(type);
@@ -15229,12 +15799,12 @@ function whereProblems(where2, table, pointer2, who, { platform = false, noColum
15229
15799
  function measureProblems(name, row, model, handlers = {}) {
15230
15800
  const problems = [];
15231
15801
  const table = model.tables[row.table];
15232
- const measures = isPlainObject5(row.measures) ? row.measures : {};
15802
+ const measures = isPlainObject6(row.measures) ? row.measures : {};
15233
15803
  for (const measure of Object.keys(measures).sort(natural3)) {
15234
15804
  const spec = measures[measure];
15235
15805
  const base = `/data/app/handlers/${escapePointer3(name)}/measures/${escapePointer3(measure)}`;
15236
15806
  const say = (message, tail = null) => problems.push({ code: "app_model_mismatch", pointer: tail === null ? base : `${base}/${escapePointer3(tail)}`, message: `measure "${measure}" of handler "${name}" ${message}` });
15237
- if (!isPlainObject5(spec)) {
15807
+ if (!isPlainObject6(spec)) {
15238
15808
  say(`is ${JSON.stringify(spec)}; a measure is an equality filter it counts ({"status": "open"}), or {count: true | sum | avg | avg_days | count_events: true, where}`);
15239
15809
  continue;
15240
15810
  }
@@ -15247,7 +15817,7 @@ function measureProblems(name, row, model, handlers = {}) {
15247
15817
  }
15248
15818
  const metrics = keys2.filter((key) => MEASURE_METRICS.includes(key));
15249
15819
  const strays = keys2.filter((key) => !MEASURE_WORDS.includes(key));
15250
- const shaped = metrics.length === 1 && strays.length === 0 && (metrics[0] !== "count" || spec.count === true) && (metrics[0] !== "count_events" || spec.count_events === true) && (metrics[0] !== "avg_days" || Array.isArray(spec.avg_days)) && (!["sum", "avg"].includes(metrics[0]) || named4(spec[metrics[0]]));
15820
+ const shaped = metrics.length === 1 && strays.length === 0 && (metrics[0] !== "count" || spec.count === true) && (metrics[0] !== "count_events" || spec.count_events === true) && (metrics[0] !== "avg_days" || Array.isArray(spec.avg_days)) && (!["sum", "avg"].includes(metrics[0]) || named5(spec[metrics[0]]));
15251
15821
  if (!shaped && keys2.every((key) => Object.prototype.hasOwnProperty.call(table.columns, key))) {
15252
15822
  say(
15253
15823
  `reads as a filter on ${keys2.map((key) => `"${key}"`).join(", ")} and as a measure at once, because ${keys2.filter((key) => MEASURE_WORDS.includes(key)).map((key) => `"${key}"`).join(", ")} is also a column of table "${table.name}"; spell the filter as {"count": true, "where": ${JSON.stringify(spec)}}`
@@ -15269,14 +15839,14 @@ function measureProblems(name, row, model, handlers = {}) {
15269
15839
  }
15270
15840
  if (metric === "sum" || metric === "avg") {
15271
15841
  const column = spec[metric];
15272
- const type = named4(column) ? readableType(table, column) : null;
15273
- if (!named4(column)) say(`states ${metric} as ${JSON.stringify(column)}; ${metric} names the column it reads`, metric);
15842
+ const type = named5(column) ? readableType(table, column) : null;
15843
+ if (!named5(column)) say(`states ${metric} as ${JSON.stringify(column)}; ${metric} names the column it reads`, metric);
15274
15844
  else if (type === null) say(`reads ${metric} of "${column}", which is no column of table "${table.name}"`, metric);
15275
15845
  else if (!isSummable(type)) say(`reads ${metric} of "${column}", which is ${type}; ${metric} takes a number column (int, bigint, decimal(p,s))`, metric);
15276
15846
  }
15277
15847
  if (metric === "avg_days") {
15278
15848
  const ends = spec.avg_days;
15279
- if (!Array.isArray(ends) || ends.length !== 2 || !ends.every(named4)) {
15849
+ if (!Array.isArray(ends) || ends.length !== 2 || !ends.every(named5)) {
15280
15850
  say(`states avg_days as ${JSON.stringify(ends)}; avg_days is [<from column>, <to column or "$now">]`, "avg_days");
15281
15851
  } else {
15282
15852
  for (const [index, end] of ends.entries()) {
@@ -15296,7 +15866,7 @@ function measureProblems(name, row, model, handlers = {}) {
15296
15866
  if (spec.where !== void 0) {
15297
15867
  const who = `measure "${measure}" of handler "${name}"`;
15298
15868
  const at = (key) => `${base}/where/${escapePointer3(key)}`;
15299
- if (!isPlainObject5(spec.where)) say(`states where as ${JSON.stringify(spec.where)}; where is an object of "<column>" or "<column>__<op>" to a value`, "where");
15869
+ if (!isPlainObject6(spec.where)) say(`states where as ${JSON.stringify(spec.where)}; where is an object of "<column>" or "<column>__<op>" to a value`, "where");
15300
15870
  else if (events) {
15301
15871
  const found = whereProblems(spec.where, EVENTS_SHAPE, at, who, {
15302
15872
  noColumn: (column) => `filters on "${column}", which is no column of the events; filter on at, action, handler, actor or note`
@@ -15345,11 +15915,11 @@ function measuresOf(row) {
15345
15915
  out[measure] = ["count", "count", spec];
15346
15916
  continue;
15347
15917
  }
15348
- const where2 = isPlainObject5(spec.where) ? spec.where : {};
15918
+ const where2 = isPlainObject6(spec.where) ? spec.where : {};
15349
15919
  if (spec.count === true) out[measure] = ["count", "count", where2];
15350
15920
  else if (spec.count_events === true) out[measure] = ["count_events", "count_events", where2];
15351
- else if (named4(spec.sum)) out[measure] = ["sum", ["sum", spec.sum], where2];
15352
- else if (named4(spec.avg)) out[measure] = ["avg", ["avg", spec.avg], where2];
15921
+ else if (named5(spec.sum)) out[measure] = ["sum", ["sum", spec.sum], where2];
15922
+ else if (named5(spec.avg)) out[measure] = ["avg", ["avg", spec.avg], where2];
15353
15923
  else out[measure] = ["avg_days", ["avg_days", ...spec.avg_days], where2];
15354
15924
  }
15355
15925
  return out;
@@ -15434,11 +16004,11 @@ function manifestOf(contract, app, handlers, names, model, served = []) {
15434
16004
  ...typeof app.description === "string" ? { description: app.description } : {},
15435
16005
  version: app.version ?? DEFAULT_VERSION
15436
16006
  };
15437
- if (named4(app.nav_label)) manifest.nav = { label: app.nav_label };
16007
+ if (named5(app.nav_label)) manifest.nav = { label: app.nav_label };
15438
16008
  manifest.routes = routesOf2(app, contract.pages ?? []);
15439
16009
  const roles = rolesOf2(contract.backend?.roles, model);
15440
16010
  if (roles.length > 0) manifest.roles = roles;
15441
- const gated = (Array.isArray(contract.pages) ? contract.pages : []).filter((page) => named4(page?.path) && gateRoles(page.roles).length > 0).map((page) => ({ path: page.path, roles: gateRoles(page.roles) }));
16011
+ const gated = (Array.isArray(contract.pages) ? contract.pages : []).filter((page) => named5(page?.path) && gateRoles(page.roles).length > 0).map((page) => ({ path: page.path, roles: gateRoles(page.roles) }));
15442
16012
  if (gated.length > 0) manifest.pages = gated;
15443
16013
  manifest.handlers = {};
15444
16014
  for (const name of names) {
@@ -15454,7 +16024,7 @@ function manifestOf(contract, app, handlers, names, model, served = []) {
15454
16024
  manifest.handlers[name] = entry;
15455
16025
  }
15456
16026
  manifest.handlers = Object.fromEntries(Object.keys(manifest.handlers).sort(natural3).map((name) => [name, manifest.handlers[name]]));
15457
- manifest.capabilities = isPlainObject5(app.capabilities) && Object.keys(app.capabilities).length > 0 ? app.capabilities : { data: ["rw"] };
16027
+ manifest.capabilities = isPlainObject6(app.capabilities) && Object.keys(app.capabilities).length > 0 ? app.capabilities : { data: ["rw"] };
15458
16028
  return manifest;
15459
16029
  }
15460
16030
  function routesOf2(app, pages) {
@@ -15470,9 +16040,9 @@ function rolesOf2(declared, model) {
15470
16040
  const out = { name: String(role?.name ?? "") };
15471
16041
  if (role?.default) out.default = true;
15472
16042
  const scope = role?.default ? "own" : "all";
15473
- out.tables = isPlainObject5(role?.tables) && Object.keys(role.tables).length > 0 ? role.tables : Object.fromEntries(tables.map((table) => [table, { read: model.tables[table]?.singleton ? "all" : scope, write: scope }]));
16043
+ out.tables = isPlainObject6(role?.tables) && Object.keys(role.tables).length > 0 ? role.tables : Object.fromEntries(tables.map((table) => [table, { read: model.tables[table]?.singleton ? "all" : scope, write: scope }]));
15474
16044
  if (Array.isArray(role?.sees) && role.sees.length > 0) out.sees = role.sees.map(String);
15475
- if (named4(role?.home)) out.home = role.home;
16045
+ if (named5(role?.home)) out.home = role.home;
15476
16046
  return out;
15477
16047
  });
15478
16048
  }
@@ -15540,8 +16110,8 @@ function decimalsOf(row, table) {
15540
16110
  }
15541
16111
  function groupedBy(row) {
15542
16112
  const columns = [];
15543
- for (const where2 of Object.values(isPlainObject5(row.measures) ? row.measures : {})) {
15544
- for (const column of Object.keys(isPlainObject5(where2) ? where2 : {})) {
16113
+ for (const where2 of Object.values(isPlainObject6(row.measures) ? row.measures : {})) {
16114
+ for (const column of Object.keys(isPlainObject6(where2) ? where2 : {})) {
15545
16115
  if (!columns.includes(column)) columns.push(column);
15546
16116
  }
15547
16117
  }
@@ -15552,8 +16122,8 @@ function singular(name) {
15552
16122
  }
15553
16123
  function queryCall(row, { receiver = 'ctx.data.table(HANDLER["table"])', indent: indent2 = 4, include = false } = {}) {
15554
16124
  const args = [];
15555
- if (isPlainObject5(row.where)) args.push('where=HANDLER["where"]');
15556
- if (named4(row.order_by)) args.push('order_by=HANDLER["order_by"]');
16125
+ if (isPlainObject6(row.where)) args.push('where=HANDLER["where"]');
16126
+ if (named5(row.order_by)) args.push('order_by=HANDLER["order_by"]');
15557
16127
  if (row.limit !== void 0 && row.limit !== null) args.push('limit=HANDLER["limit"]');
15558
16128
  if (include) args.push("include=INCLUDE");
15559
16129
  const pad = " ".repeat(indent2);
@@ -15633,13 +16203,13 @@ function parentLines(row, parent, { missing, invisible }) {
15633
16203
  ];
15634
16204
  }
15635
16205
  function includesOf(table) {
15636
- return Object.keys(table?.columns ?? {}).filter((name) => named4(table.columns[name].ref) && named4(table.columns[name].display));
16206
+ return Object.keys(table?.columns ?? {}).filter((name) => named5(table.columns[name].ref) && named5(table.columns[name].display));
15637
16207
  }
15638
16208
  function refsOf(row, table) {
15639
16209
  const refs = {};
15640
16210
  for (const field of Array.isArray(row.fields) ? row.fields : []) {
15641
16211
  const ref = table.columns?.[field]?.ref;
15642
- if (!named4(ref)) continue;
16212
+ if (!named5(ref)) continue;
15643
16213
  const target = String(ref).split(".")[0];
15644
16214
  refs[field] = [target, singular(target)];
15645
16215
  }
@@ -15663,10 +16233,10 @@ function refLines() {
15663
16233
  }
15664
16234
  function childList(row, parent, include = []) {
15665
16235
  const args = ["where=where"];
15666
- if (named4(row.order_by)) args.push('order_by=HANDLER["order_by"]');
16236
+ if (named5(row.order_by)) args.push('order_by=HANDLER["order_by"]');
15667
16237
  if (row.limit !== void 0 && row.limit !== null) args.push('limit=HANDLER["limit"]');
15668
16238
  if (include.length > 0) args.push("include=INCLUDE");
15669
- const where2 = isPlainObject5(row.where) ? ' where = {**HANDLER["where"], HANDLER["by"]: parent_id}' : ' where = {HANDLER["by"]: parent_id}';
16239
+ const where2 = isPlainObject6(row.where) ? ' where = {**HANDLER["where"], HANDLER["by"]: parent_id}' : ' where = {HANDLER["by"]: parent_id}';
15670
16240
  return {
15671
16241
  docstring: `The ${row.table} of one ${singular(parent)}: the ${singular(parent)} a reader opened, named by \`by\`.
15672
16242
 
@@ -15737,7 +16307,7 @@ function decideMap(name, row, table, model) {
15737
16307
  const noun = singular(row.table);
15738
16308
  const names = decisionNames(row);
15739
16309
  const specs = names.map((one) => row.decision[one]);
15740
- const sets = specs.map((spec) => isPlainObject5(spec.set) ? spec.set : {});
16310
+ const sets = specs.map((spec) => isPlainObject6(spec.set) ? spec.set : {});
15741
16311
  const used = new Set(sets.flatMap((set) => Object.values(set)).filter((value) => SET_TOKENS.includes(value)));
15742
16312
  const setsAnything = sets.some((set) => Object.keys(set).length > 0);
15743
16313
  const requiresNote = specs.some((spec) => spec.note === "required");
@@ -15763,7 +16333,7 @@ function decideMap(name, row, table, model) {
15763
16333
  ` if row is None or row.get(${pyString(column)}) != HANDLER["guard"]:`,
15764
16334
  ` return {"error": ${pyString(`that ${noun} is not ${row.guard}`)}}`,
15765
16335
  ...notOwnLines(row, 'row["id"]', "decide", 4),
15766
- ...isPlainObject5(row.requires) ? requiresLines(row, "{decision}") : []
16336
+ ...isPlainObject6(row.requires) ? requiresLines(row, "{decision}") : []
15767
16337
  );
15768
16338
  if (readsNote) lines.push(' note = str(payload.get("note") or "").strip()');
15769
16339
  if (requiresNote) {
@@ -15864,10 +16434,10 @@ contract, not this file.`,
15864
16434
  ].join("\n")
15865
16435
  }),
15866
16436
  save: (name, row, table, model) => {
15867
- if (table?.singleton === true && !named4(row.guard) && row.not_own !== true) return singletonSave(row, table);
16437
+ if (table?.singleton === true && !named5(row.guard) && row.not_own !== true) return singletonSave(row, table);
15868
16438
  const enums = enumsOf(row, table);
15869
16439
  const refs = refsOf(row, table);
15870
- const guarded = named4(row.guard);
16440
+ const guarded = named5(row.guard);
15871
16441
  const constants = Object.keys(enums).length > 0 ? [assignment("ENUMS", enums, Object.keys(enums))] : [];
15872
16442
  if (Object.keys(refs).length > 0) constants.push(assignment("REFS", refs, Object.keys(refs)));
15873
16443
  const guardDoc = guarded ? `
@@ -15901,7 +16471,7 @@ contract, not this file.`,
15901
16471
  ].join("\n")
15902
16472
  };
15903
16473
  },
15904
- remove: (name, row, table, model) => named4(row.guard) ? guardedRemove(row, model) : {
16474
+ remove: (name, row, table, model) => named5(row.guard) ? guardedRemove(row, model) : {
15905
16475
  docstring: `Delete one ${singular(row.table)}, by the id the row itself carries.
15906
16476
 
15907
16477
  The platform's row scope is what decides whether this caller may: a delete
@@ -15950,11 +16520,11 @@ contract, not this file.`,
15950
16520
  lines.push(...parentLines(row, parent, { missing: `{"error": ${pyString(`${row.by} is required`)}}`, invisible: `{"error": ${pyString(`${row.by} names no ${singular(parent)} this app can see`)}}` }));
15951
16521
  lines.push(' row[HANDLER["by"]] = parent_id');
15952
16522
  }
15953
- if (isPlainObject5(row.requires)) {
16523
+ if (isPlainObject6(row.requires)) {
15954
16524
  constants.push(...requiresConstants(row, model));
15955
16525
  lines.push(...requiresLines(row, "saved"));
15956
16526
  }
15957
- if (isPlainObject5(row.fixed) && Object.keys(row.fixed).length > 0) lines.push(' row.update(HANDLER["fixed"])');
16527
+ if (isPlainObject6(row.fixed) && Object.keys(row.fixed).length > 0) lines.push(' row.update(HANDLER["fixed"])');
15958
16528
  lines.push(' return {"row": ctx.data.table(HANDLER["table"]).insert(row)}');
15959
16529
  const linkDoc = parent === null ? "" : `
15960
16530
 
@@ -15975,7 +16545,7 @@ contract, not this file.`,
15975
16545
  };
15976
16546
  },
15977
16547
  decide: (name, row, table, model) => {
15978
- if (isPlainObject5(row.decision)) return decideMap(name, row, table, model);
16548
+ if (isPlainObject6(row.decision)) return decideMap(name, row, table, model);
15979
16549
  const column = statusColumn(model, row);
15980
16550
  const columns = table.columns ?? {};
15981
16551
  const stamps = ["decided_by", "decided_at"].filter((one) => Object.prototype.hasOwnProperty.call(columns, one) && columns[one].computed === null);
@@ -16001,7 +16571,7 @@ contract, not this file.`,
16001
16571
  // After the guard, so a row that is not there (or not in the state)
16002
16572
  // answers what it always did, and the compiler's own fixture still does.
16003
16573
  ...notOwnLines(row, 'row["id"]', "decide", 4),
16004
- ...isPlainObject5(row.requires) ? requiresLines(row, "{decision}") : [],
16574
+ ...isPlainObject6(row.requires) ? requiresLines(row, "{decision}") : [],
16005
16575
  ` patch = {${pyString(column)}: decision}`
16006
16576
  ];
16007
16577
  if (stamps.includes("decided_by")) lines.push(' patch["decided_by"] = getattr(getattr(ctx, "user", None), "email", None)');
@@ -16073,7 +16643,7 @@ function testCase(name, row) {
16073
16643
  switch (row.role) {
16074
16644
  case "list":
16075
16645
  case "inbox":
16076
- if (named4(row.by)) return { name: `${name} answers with no rows when no ${row.by} is named`, payload: {}, expect: { rows: [] } };
16646
+ if (named5(row.by)) return { name: `${name} answers with no rows when no ${row.by} is named`, payload: {}, expect: { rows: [] } };
16077
16647
  return { name: `${name} answers with rows`, payload: {}, shape: { rows: [] } };
16078
16648
  case "summary":
16079
16649
  return {
@@ -16082,7 +16652,7 @@ function testCase(name, row) {
16082
16652
  // An average over no rows is None, so its shape is `any`; a count
16083
16653
  // and a sum are always numbers.
16084
16654
  shape: Object.fromEntries(
16085
- Object.entries(isPlainObject5(row.measures) ? row.measures : {}).map(([measure, spec]) => [
16655
+ Object.entries(isPlainObject6(row.measures) ? row.measures : {}).map(([measure, spec]) => [
16086
16656
  measure,
16087
16657
  isNewMeasure(spec) && (spec.avg !== void 0 || spec.avg_days !== void 0) ? "any" : "number"
16088
16658
  ])
@@ -16091,7 +16661,7 @@ function testCase(name, row) {
16091
16661
  case "submit":
16092
16662
  case "save": {
16093
16663
  const required = Array.isArray(row.required) ? row.required : [];
16094
- if (required.length === 0 && row.role === "submit" && named4(row.by)) {
16664
+ if (required.length === 0 && row.role === "submit" && named5(row.by)) {
16095
16665
  return { name: `${name} refuses a row with no ${row.by}`, payload: {}, expect: { error: `${row.by} is required` } };
16096
16666
  }
16097
16667
  if (required.length === 0) return { name: `${name} writes the row it is given`, payload: {}, shape: { row: {} } };
@@ -16116,21 +16686,21 @@ function testCase(name, row) {
16116
16686
  }
16117
16687
  function pageHtml(contract, page, elements, tree, model, { runtime, uiKit, charts, fmt, appWidgets, appComponents, appControls, theme = false, extra = {} }) {
16118
16688
  const app = contract.app ?? {};
16119
- const name = named4(app.name) ? app.name : named4(app.id) ? app.id : "";
16689
+ const name = named5(app.name) ? app.name : named5(app.id) ? app.id : "";
16120
16690
  const title = documentTitle(page, name);
16121
16691
  const embedded = JSON.stringify(canonical2(contract)).replace(/<\//g, "<\\/");
16122
16692
  const bodyAttrs = [' class="dr-app-page"'];
16123
- if (named4(page.theme)) bodyAttrs.push(` data-dr-theme="${attr(page.theme)}"`);
16124
- if (named4(page.density)) bodyAttrs.push(` data-dr-density="${attr(page.density)}"`);
16693
+ if (named5(page.theme)) bodyAttrs.push(` data-dr-theme="${attr(page.theme)}"`);
16694
+ if (named5(page.density)) bodyAttrs.push(` data-dr-density="${attr(page.density)}"`);
16125
16695
  const skeleton = [];
16126
- for (const band of bandsOf(page, elements, rootsOf(contract, page, elements, tree))) {
16696
+ for (const band of bandsOf2(page, elements, rootsOf2(contract, page, elements, tree))) {
16127
16697
  if (band.section === null) {
16128
16698
  for (const uid of band.roots) skeleton.push(...nodeLines(uid, contract, page, elements, tree, model, 0));
16129
16699
  continue;
16130
16700
  }
16131
16701
  skeleton.push(`<section data-dr-section="${attr(band.section.id)}"${bandLook(band.section)}>`);
16132
- if (named4(band.section.title)) skeleton.push(` <h2>${text(band.section.title)}</h2>`);
16133
- if (named4(band.section.subtitle)) skeleton.push(` <p>${text(band.section.subtitle)}</p>`);
16702
+ if (named5(band.section.title)) skeleton.push(` <h2>${text(band.section.title)}</h2>`);
16703
+ if (named5(band.section.subtitle)) skeleton.push(` <p>${text(band.section.subtitle)}</p>`);
16134
16704
  for (const uid of band.roots) skeleton.push(...nodeLines(uid, contract, page, elements, tree, model, 1));
16135
16705
  skeleton.push("</section>");
16136
16706
  }
@@ -16148,7 +16718,7 @@ function pageHtml(contract, page, elements, tree, model, { runtime, uiKit, chart
16148
16718
  ...theme ? ['<link rel="stylesheet" href="./theme.css">'] : [],
16149
16719
  `</head><body${bodyAttrs.join("")}>`,
16150
16720
  `<script type="application/json" id="dr-contract">${embedded}</script>`,
16151
- `<main data-dr-app data-dr-page="${attr(page.path)}">`,
16721
+ `<main data-dr-app data-dr-page="${attr(page.path)}"${shellAttr(app)}${hasNav(page, elements) ? "" : navCountsAttr(page)}>`,
16152
16722
  ...skeleton,
16153
16723
  "</main>",
16154
16724
  // P1/P1b stylesheets, in the body after </main> for the reason the
@@ -16190,7 +16760,7 @@ function chartScripts(page, elements, { charts, fmt }) {
16190
16760
  function platformNumbersOn(page, elements) {
16191
16761
  const states = (node) => {
16192
16762
  if (Array.isArray(node)) return node.some(states);
16193
- if (!isPlainObject5(node)) return false;
16763
+ if (!isPlainObject6(node)) return false;
16194
16764
  return Object.entries(node).some(
16195
16765
  ([key, value]) => key === "format" && isPlatformFormat(value) || key === "negative" && NEGATIVE_MODES2.includes(value) || states(value)
16196
16766
  );
@@ -16229,7 +16799,7 @@ function iconsOn(page, elements, extra) {
16229
16799
  for (const element of Object.values(elements)) {
16230
16800
  if (element?.page !== page.path) continue;
16231
16801
  if (ICON_RE.test(JSON.stringify(element.props ?? {}))) return true;
16232
- if (element.type !== "custom" || !named4(element.props?.module)) continue;
16802
+ if (element.type !== "custom" || !named5(element.props?.module)) continue;
16233
16803
  const source = extra[`ui/${element.props.module}`];
16234
16804
  if (typeof source === "string" && ICON_RE.test(source)) return true;
16235
16805
  }
@@ -16238,12 +16808,12 @@ function iconsOn(page, elements, extra) {
16238
16808
  function moduleScripts(page, elements) {
16239
16809
  const modules = /* @__PURE__ */ new Set();
16240
16810
  for (const element of Object.values(elements)) {
16241
- if (element?.page === page.path && element.type === "custom" && named4(element.props?.module)) modules.add(element.props.module);
16811
+ if (element?.page === page.path && element.type === "custom" && named5(element.props?.module)) modules.add(element.props.module);
16242
16812
  }
16243
16813
  return [...modules].sort(natural3).map((module) => `<script src="./${attr(module)}"></script>`);
16244
16814
  }
16245
- function bandsOf(page, elements, roots) {
16246
- const sections = (Array.isArray(page.sections) ? page.sections : []).filter((entry) => named4(entry?.id));
16815
+ function bandsOf2(page, elements, roots) {
16816
+ const sections = (Array.isArray(page.sections) ? page.sections : []).filter((entry) => named5(entry?.id));
16247
16817
  if (sections.length === 0) return [{ section: null, roots }];
16248
16818
  const ids = new Set(sections.map((entry) => entry.id));
16249
16819
  const bands = [{ section: null, roots: roots.filter((uid) => !ids.has(elements[uid]?.section)) }];
@@ -16252,16 +16822,16 @@ function bandsOf(page, elements, roots) {
16252
16822
  }
16253
16823
  return bands;
16254
16824
  }
16255
- var BAND_LOOK2 = ["layout", "surface", "aside"];
16825
+ var BAND_LOOK3 = ["layout", "surface", "aside"];
16256
16826
  function bandLook(section) {
16257
- return BAND_LOOK2.filter((key) => named4(section[key])).map((key) => ` data-dr-${key}="${attr(section[key])}"`).join("");
16827
+ return BAND_LOOK3.filter((key) => named5(section[key])).map((key) => ` data-dr-${key}="${attr(section[key])}"`).join("");
16258
16828
  }
16259
16829
  function documentTitle(page, appName) {
16260
16830
  const own = shortTitle(page, appName);
16261
16831
  return appName === "" || own === appName ? own : `${own} \xB7 ${appName}`;
16262
16832
  }
16263
16833
  function shortTitle(page, appName) {
16264
- const raw = named4(page.title) ? page.title : named4(page.header?.title) ? page.header.title : page.path;
16834
+ const raw = named5(page.title) ? page.title : named5(page.header?.title) ? page.header.title : page.path;
16265
16835
  const suffix = ` \xB7 ${appName}`;
16266
16836
  return appName !== "" && raw.endsWith(suffix) ? raw.slice(0, -suffix.length) : raw;
16267
16837
  }
@@ -16269,7 +16839,7 @@ function routeOf2(path3) {
16269
16839
  const file = String(path3).split("/").pop();
16270
16840
  return file === "index.html" ? "/" : `/${file}`;
16271
16841
  }
16272
- function rootsOf(contract, page, elements, tree) {
16842
+ function rootsOf2(contract, page, elements, tree) {
16273
16843
  if (Array.isArray(page.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid]);
16274
16844
  return Object.keys(elements).filter((uid) => elements[uid].page === page.path && parentOf(tree, uid) === null).sort((a, b) => (elements[a].order ?? 0) - (elements[b].order ?? 0) || natural3(a, b));
16275
16845
  }
@@ -16280,12 +16850,12 @@ function childrenOf(uid, elements, tree) {
16280
16850
  }
16281
16851
  function pageRole(contract, page, elements, tree) {
16282
16852
  if (gateRoles(page.roles).length > 0) return pageGateKey(page.roles);
16283
- if (named4(page.role)) return page.role;
16853
+ if (named5(page.role)) return page.role;
16284
16854
  const roles = /* @__PURE__ */ new Set();
16285
- for (const uid of rootsOf(contract, page, elements, tree)) {
16855
+ for (const uid of rootsOf2(contract, page, elements, tree)) {
16286
16856
  const element = elements[uid];
16287
16857
  if (!COMPONENT_TYPES2.includes(element.type)) continue;
16288
- roles.add(named4(element.props?.role) ? element.props.role : null);
16858
+ roles.add(named5(element.props?.role) ? element.props.role : null);
16289
16859
  }
16290
16860
  return roles.size === 1 && !roles.has(null) ? [...roles][0] : null;
16291
16861
  }
@@ -16298,9 +16868,13 @@ function nodeLines(uid, contract, page, elements, tree, model, depth) {
16298
16868
  case "region": {
16299
16869
  if ((element.role ?? "header") !== "header") return [`${pad}<section data-dr-kit="${attr(element.role ?? "")}"></section>`];
16300
16870
  const header = page.header ?? {};
16301
- const heading = named4(header.title) ? header.title : shortTitle(page, contract.app?.name ?? "");
16302
- const lede = named4(header.summary) ? `<p>${text(header.summary)}</p>` : "";
16303
- return [`${pad}<header data-dr-kit="header"${gridAttrs(element)}><h1>${text(heading)}</h1>${lede}</header>`];
16871
+ const heading = named5(header.title) ? header.title : shortTitle(page, contract.app?.name ?? "");
16872
+ const lede = named5(header.summary) ? `<p>${text(header.summary)}</p>` : "";
16873
+ const actions = Array.isArray(header.actions) && header.actions.length > 0 ? ` data-dr-header-actions="${attr(JSON.stringify(header.actions))}"` : "";
16874
+ const from = named5(header.title_from) ? ` data-dr-title-from="${attr(header.title_from)}"` : "";
16875
+ const open2 = `${pad}<header data-dr-kit="header"${gridAttrs(element)}${actions}${from}><h1>${text(heading)}</h1>${lede}`;
16876
+ if (kids.length === 0) return [`${open2}</header>`];
16877
+ return [open2, ...kids.flatMap((child) => nodeLines(child, contract, page, elements, tree, model, depth + 1)), `${pad}</header>`];
16304
16878
  }
16305
16879
  case "nav": {
16306
16880
  const entries = (contract.pages ?? []).map((one) => {
@@ -16309,7 +16883,7 @@ function nodeLines(uid, contract, page, elements, tree, model, depth) {
16309
16883
  const gate = role === null ? "" : ` data-dr-role="${attr(role)}"`;
16310
16884
  return `<a href="./${attr(String(one.path).split("/").pop())}"${gate}${current}>${text(shortTitle(one, contract.app?.name ?? ""))}</a>`;
16311
16885
  });
16312
- return [`${pad}<nav data-dr-kit="nav"${gridAttrs(element)}>${entries.join("")}</nav>`];
16886
+ return [`${pad}<nav data-dr-kit="nav"${gridAttrs(element)}${navCountsAttr(page)}>${entries.join("")}</nav>`];
16313
16887
  }
16314
16888
  case "stat":
16315
16889
  case "chart":
@@ -16381,7 +16955,7 @@ function fieldInput(uid, elements, tree, model, depth) {
16381
16955
  const column = (model.tables[entity]?.columns ?? {})[props.field] ?? null;
16382
16956
  const label = labelOf(element);
16383
16957
  const required = props.required === true ? " required" : "";
16384
- const picker = named4(column?.ref) && named4(props.source);
16958
+ const picker = named5(column?.ref) && named5(props.source);
16385
16959
  const kind = picker ? "select" : props.input_type ?? (column?.enum ? "select" : isNumberType(column?.type) ? "number" : "text");
16386
16960
  if (kind === "select") {
16387
16961
  const options = picker ? [] : Array.isArray(props.options) && props.options.length > 0 ? props.options : column?.enum ?? [];
@@ -16397,12 +16971,12 @@ function fieldInput(uid, elements, tree, model, depth) {
16397
16971
  }
16398
16972
  function labelOf(element) {
16399
16973
  const props = element.props ?? {};
16400
- if (named4(props.label)) return props.label;
16401
- if (named4(props.field)) return humanise(props.field);
16402
- if (named4(props.id)) return humanise(props.id);
16974
+ if (named5(props.label)) return props.label;
16975
+ if (named5(props.field)) return humanise2(props.field);
16976
+ if (named5(props.id)) return humanise2(props.id);
16403
16977
  return "";
16404
16978
  }
16405
- function humanise(name) {
16979
+ function humanise2(name) {
16406
16980
  const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
16407
16981
  return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
16408
16982
  }
@@ -16419,7 +16993,7 @@ function attrsOf(element) {
16419
16993
  const value = props[key];
16420
16994
  if (value === void 0 || value === null || value === "") continue;
16421
16995
  if (key === "filter") {
16422
- if (!isPlainObject5(value)) continue;
16996
+ if (!isPlainObject6(value)) continue;
16423
16997
  const written = Object.keys(value).sort().map((column) => `${column}=${value[column]}`).join(",");
16424
16998
  if (written !== "") out.push(["filter", written]);
16425
16999
  continue;
@@ -16449,11 +17023,11 @@ function attrsOf(element) {
16449
17023
  }
16450
17024
  const card = writeArgs(element.card);
16451
17025
  if (card !== "") out.push(["card", card]);
16452
- if (named4(element.emphasis)) out.push(["emphasis", element.emphasis]);
17026
+ if (named5(element.emphasis)) out.push(["emphasis", element.emphasis]);
16453
17027
  return gridAttrs(element) + out.map(([key, value]) => ` data-dr-${key}="${attr(value)}"`).join("");
16454
17028
  }
16455
17029
  function gridAttrs(element) {
16456
- const grid = isPlainObject5(element.grid) ? element.grid : {};
17030
+ const grid = isPlainObject6(element.grid) ? element.grid : {};
16457
17031
  let out = "";
16458
17032
  if (grid.span !== null && grid.span !== void 0) out += ` data-dr-span="${attr(grid.span)}"`;
16459
17033
  if (grid.rows !== null && grid.rows !== void 0) out += ` data-dr-rows="${attr(grid.rows)}"`;
@@ -16469,13 +17043,13 @@ function attr(value) {
16469
17043
  function text(value) {
16470
17044
  return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
16471
17045
  }
16472
- function named4(value) {
17046
+ function named5(value) {
16473
17047
  return typeof value === "string" && value !== "";
16474
17048
  }
16475
17049
  function stated(value) {
16476
17050
  return typeof value === "string" && value.trim() !== "";
16477
17051
  }
16478
- function isPlainObject5(value) {
17052
+ function isPlainObject6(value) {
16479
17053
  return typeof value === "object" && value !== null && !Array.isArray(value);
16480
17054
  }
16481
17055
  function escapePointer3(key) {
@@ -18280,11 +18854,11 @@ function composeLayout(contract) {
18280
18854
  const out = clone4(contract);
18281
18855
  const applied = [];
18282
18856
  const record2 = (what, where2, message) => applied.push({ what, where: where2, message });
18283
- const app = isPlainObject6(out.app) ? out.app : out.app = {};
18857
+ const app = isPlainObject7(out.app) ? out.app : out.app = {};
18284
18858
  const pages = Array.isArray(out.pages) ? out.pages : out.pages = [];
18285
- const elements = isPlainObject6(out.elements) ? out.elements : out.elements = {};
18859
+ const elements = isPlainObject7(out.elements) ? out.elements : out.elements = {};
18286
18860
  const preset = presetOf(app, pages);
18287
- if (isPlainObject6(app.theme) && "preset" in app.theme) delete app.theme.preset;
18861
+ if (isPlainObject7(app.theme) && "preset" in app.theme) delete app.theme.preset;
18288
18862
  for (const page of pages) {
18289
18863
  const path3 = page.path ?? "ui/index.html";
18290
18864
  if (page.theme === void 0 || page.theme === null) {
@@ -18352,7 +18926,7 @@ function composeSpans(elements, record2) {
18352
18926
  for (const uid of Object.keys(elements).sort(natural3)) {
18353
18927
  const element = elements[uid];
18354
18928
  if (element?.type !== "widget") continue;
18355
- const grid = isPlainObject6(element.grid) ? element.grid : null;
18929
+ const grid = isPlainObject7(element.grid) ? element.grid : null;
18356
18930
  if (grid && grid.span !== null && grid.span !== void 0) continue;
18357
18931
  const kind = kindOf2(element);
18358
18932
  const span = SPAN_BY_KIND[kind] ?? DEFAULT_SPAN;
@@ -18362,8 +18936,8 @@ function composeSpans(elements, record2) {
18362
18936
  }
18363
18937
  function composeTheme(app, preset, record2) {
18364
18938
  const table = THEME_TOKENS[preset] ?? THEME_TOKENS[DEFAULT_PRESET];
18365
- const theme = isPlainObject6(app.theme) ? app.theme : {};
18366
- const tokens = isPlainObject6(theme.tokens) ? { ...theme.tokens } : {};
18939
+ const theme = isPlainObject7(app.theme) ? app.theme : {};
18940
+ const tokens = isPlainObject7(theme.tokens) ? { ...theme.tokens } : {};
18367
18941
  let filled = 0;
18368
18942
  for (const [token, value] of Object.entries(table)) {
18369
18943
  if (token in tokens) continue;
@@ -18380,7 +18954,7 @@ function composeTheme(app, preset, record2) {
18380
18954
  }
18381
18955
  }
18382
18956
  function presetOf(app, pages) {
18383
- const asked = isPlainObject6(app.theme) ? app.theme.preset : null;
18957
+ const asked = isPlainObject7(app.theme) ? app.theme.preset : null;
18384
18958
  if (typeof asked === "string" && PRESETS2.includes(asked)) return asked;
18385
18959
  for (const page of pages) {
18386
18960
  if (typeof page?.theme === "string" && PRESETS2.includes(page.theme)) return page.theme;
@@ -18388,7 +18962,7 @@ function presetOf(app, pages) {
18388
18962
  return DEFAULT_PRESET;
18389
18963
  }
18390
18964
  function kindOf2(element) {
18391
- const props = isPlainObject6(element?.props) ? element.props : {};
18965
+ const props = isPlainObject7(element?.props) ? element.props : {};
18392
18966
  if (typeof props.kind === "string" && props.kind !== "") return props.kind;
18393
18967
  return typeof element?.role === "string" ? element.role : "";
18394
18968
  }
@@ -18397,7 +18971,7 @@ function routeFor2(path3) {
18397
18971
  return clean === "index.html" ? "/" : `/${clean}`;
18398
18972
  }
18399
18973
  function onlyHandler(app) {
18400
- const names = isPlainObject6(app.handlers) ? Object.keys(app.handlers) : [];
18974
+ const names = isPlainObject7(app.handlers) ? Object.keys(app.handlers) : [];
18401
18975
  return names.length === 1 ? names[0] : "dashboard";
18402
18976
  }
18403
18977
  function sortKeys(object) {
@@ -18407,14 +18981,14 @@ function sortKeys(object) {
18407
18981
  }
18408
18982
  function clone4(value) {
18409
18983
  if (Array.isArray(value)) return value.map(clone4);
18410
- if (isPlainObject6(value)) {
18984
+ if (isPlainObject7(value)) {
18411
18985
  const out = {};
18412
18986
  for (const [key, held] of Object.entries(value)) out[key] = clone4(held);
18413
18987
  return out;
18414
18988
  }
18415
18989
  return value;
18416
18990
  }
18417
- function isPlainObject6(value) {
18991
+ function isPlainObject7(value) {
18418
18992
  return value !== null && typeof value === "object" && !Array.isArray(value);
18419
18993
  }
18420
18994
 
@@ -18433,13 +19007,13 @@ var WIDGET_FILE = "ui/widget.js";
18433
19007
  var MANIFEST_FILE = "manifest.json";
18434
19008
  var PROP_ORDER = ["kind", "params", "title", "subtitle", "description", "source"];
18435
19009
  function compile(contract, { kit, extra = {} } = {}) {
18436
- if (!isPlainObject7(kit)) throw new TypeError("compile(contract, {kit}): the kit manifest (kit.json) says which shelf modules a page loads");
19010
+ if (!isPlainObject8(kit)) throw new TypeError("compile(contract, {kit}): the kit manifest (kit.json) says which shelf modules a page loads");
18437
19011
  const problems = [];
18438
19012
  const refuse = (code, where2, message) => {
18439
19013
  problems.push({ code, where: where2, lossy: false, message });
18440
19014
  return { files: {}, problems };
18441
19015
  };
18442
- const source = isPlainObject7(contract) ? contract : {};
19016
+ const source = isPlainObject8(contract) ? contract : {};
18443
19017
  const kind = source.app?.kind ?? null;
18444
19018
  if (kind !== "dashboard") {
18445
19019
  return refuse(
@@ -18465,11 +19039,11 @@ function compile(contract, { kit, extra = {} } = {}) {
18465
19039
  );
18466
19040
  }
18467
19041
  const handlerName = handlers[0];
18468
- const library = isPlainObject7(composed.data?.library) ? composed.data.library : null;
19042
+ const library = isPlainObject8(composed.data?.library) ? composed.data.library : null;
18469
19043
  const written = writeSpec(composed.data ?? {}, {
18470
19044
  library: {
18471
19045
  module: library?.module ?? LIBRARY?.module ?? DEFAULT_LIBRARY_MODULE,
18472
- ...isPlainObject7(LIBRARY?.functions) ? { functions: LIBRARY.functions } : {}
19046
+ ...isPlainObject8(LIBRARY?.functions) ? { functions: LIBRARY.functions } : {}
18473
19047
  }
18474
19048
  });
18475
19049
  if (written.text === null) {
@@ -18502,17 +19076,17 @@ function compile(contract, { kit, extra = {} } = {}) {
18502
19076
  return { files, problems: problems.sort(byProblem2) };
18503
19077
  }
18504
19078
  function describeTier(data) {
18505
- if (!isPlainObject7(data)) return { where: "handlers/", message: "the contract carries no data section; a dashboard compiles from the SPEC its handler declares" };
19079
+ if (!isPlainObject8(data)) return { where: "handlers/", message: "the contract carries no data section; a dashboard compiles from the SPEC its handler declares" };
18506
19080
  if (data.source !== "static") {
18507
19081
  return {
18508
19082
  where: "/data/source",
18509
19083
  message: `data.source is ${JSON.stringify(data.source ?? null)}; only a "static" section is the SPEC a handler declares, and a describe answer is what one SAYS at runtime`
18510
19084
  };
18511
19085
  }
18512
- const widgets = isPlainObject7(data.widgets) ? data.widgets : {};
19086
+ const widgets = isPlainObject8(data.widgets) ? data.widgets : {};
18513
19087
  for (const key of Object.keys(widgets).sort(natural3)) {
18514
19088
  const row = widgets[key];
18515
- if (!isPlainObject7(row)) {
19089
+ if (!isPlainObject8(row)) {
18516
19090
  return { where: `/data/widgets/${key}`, message: `the row for "${key}" is ${row === null ? "null" : typeof row}; a SPEC row a compiler can write is an object` };
18517
19091
  }
18518
19092
  if ("_described" in row) {
@@ -18526,9 +19100,9 @@ function describeTier(data) {
18526
19100
  }
18527
19101
  function missingExpressions(widgets) {
18528
19102
  const problems = [];
18529
- for (const key of Object.keys(isPlainObject7(widgets) ? widgets : {}).sort(natural3)) {
19103
+ for (const key of Object.keys(isPlainObject8(widgets) ? widgets : {}).sort(natural3)) {
18530
19104
  const row = widgets[key];
18531
- if (!isPlainObject7(row) || typeof row.expr === "string") continue;
19105
+ if (!isPlainObject8(row) || typeof row.expr === "string") continue;
18532
19106
  const transformed = typeof row.transform === "string" && row.transform !== "";
18533
19107
  if (!("compute" in row) && !transformed) continue;
18534
19108
  problems.push({
@@ -18554,7 +19128,7 @@ function manifestFile(contract, handlerName) {
18554
19128
  ...app.nav_label ? { nav: { label: app.nav_label } } : {},
18555
19129
  routes: Array.isArray(app.routes) ? app.routes : [],
18556
19130
  handlers: { [handlerName]: { entrypoint: handlerPath(contract, handlerName), http: handlerHttp(contract, handlerName) } },
18557
- capabilities: isPlainObject7(app.capabilities) ? app.capabilities : { ...DEFAULT_CAPABILITIES }
19131
+ capabilities: isPlainObject8(app.capabilities) ? app.capabilities : { ...DEFAULT_CAPABILITIES }
18558
19132
  };
18559
19133
  return `${JSON.stringify(manifest, null, 2)}
18560
19134
  `;
@@ -18584,8 +19158,8 @@ function themeFile(contract) {
18584
19158
  }
18585
19159
  function widgetFile(contract, pages, handlerName) {
18586
19160
  const mount = pages.find((page) => page.mount?.handler === handlerName)?.mount ?? {};
18587
- const defaults = isPlainObject7(mount.defaults) ? mount.defaults : null;
18588
- const filters = defaults !== null && isPlainObject7(defaults.filters) ? defaults.filters : null;
19161
+ const defaults = isPlainObject8(mount.defaults) ? mount.defaults : null;
19162
+ const filters = defaults !== null && isPlainObject8(defaults.filters) ? defaults.filters : null;
18589
19163
  const rest = defaults === null ? {} : Object.fromEntries(Object.entries(defaults).filter(([key]) => key !== "filters"));
18590
19164
  const lines = [
18591
19165
  "/* The dashboard hands itself to the widget library.",
@@ -18703,7 +19277,7 @@ function pageFile(page, ctx) {
18703
19277
  `;
18704
19278
  }
18705
19279
  function elementsOf(contract, path3) {
18706
- const elements = isPlainObject7(contract.elements) ? contract.elements : {};
19280
+ const elements = isPlainObject8(contract.elements) ? contract.elements : {};
18707
19281
  return Object.keys(elements).sort(natural3).map((uid) => ({ uid, ...elements[uid] })).filter((element) => element.page === path3);
18708
19282
  }
18709
19283
  function shelf(path3, kit, charts, contract, problems) {
@@ -18750,7 +19324,7 @@ function shelf(path3, kit, charts, contract, problems) {
18750
19324
  };
18751
19325
  }
18752
19326
  function appbar(page, elements) {
18753
- const header = isPlainObject7(page.header) ? page.header : { title: page.title ?? "", summary: null };
19327
+ const header = isPlainObject8(page.header) ? page.header : { title: page.title ?? "", summary: null };
18754
19328
  const summary = typeof header.summary === "string" && header.summary !== "" ? `${text2(header.summary)} ` : "";
18755
19329
  return [
18756
19330
  `<header class="${classesOf2(elements, "region", "header", ["dr-appbar"])}" data-dr-kit="header">`,
@@ -18810,7 +19384,7 @@ function controlRow(elements) {
18810
19384
  if (controls.length === 0) return [];
18811
19385
  const out = [`<div class="${classesOf2(elements, "region", "controls", ["dr-controlrow"])}" data-dr-kit="controls">`];
18812
19386
  for (const control of controls) {
18813
- const props = isPlainObject7(control.props) ? control.props : {};
19387
+ const props = isPlainObject8(control.props) ? control.props : {};
18814
19388
  const id = control.id;
18815
19389
  out.push(' <span class="drw-field--inline">');
18816
19390
  if (id && typeof props.label === "string") {
@@ -18856,13 +19430,13 @@ function order(element) {
18856
19430
  return Number.isInteger(element.order) ? element.order : Number.MAX_SAFE_INTEGER;
18857
19431
  }
18858
19432
  function cardBlock(card) {
18859
- const props = isPlainObject7(card.props) ? card.props : {};
19433
+ const props = isPlainObject8(card.props) ? card.props : {};
18860
19434
  const kind = kindOf2(card);
18861
19435
  const classes = (card.classes ?? []).length > 0 ? card.classes.join(" ") : kind === "kpi" ? "dr-card dr-card--kpi" : "dr-card";
18862
- const slots = isPlainObject7(card.slots) && Object.keys(card.slots).length > 0 ? card.slots : { actions: true, body: "widget-body" };
19436
+ const slots = isPlainObject8(card.slots) && Object.keys(card.slots).length > 0 ? card.slots : { actions: true, body: "widget-body" };
18863
19437
  const bodyClass = typeof slots.body === "string" ? slots.body : "widget-body";
18864
19438
  const attrs = [`class="${attr2(classes)}"`];
18865
- const grid = isPlainObject7(card.grid) ? card.grid : {};
19439
+ const grid = isPlainObject8(card.grid) ? card.grid : {};
18866
19440
  if (grid.span !== null && grid.span !== void 0) attrs.push(`data-dr-span="${attr2(grid.span)}"`);
18867
19441
  if (grid.rows !== null && grid.rows !== void 0) attrs.push(`data-dr-rows="${attr2(grid.rows)}"`);
18868
19442
  attrs.push(`data-dr-widget="${attr2(card.id ?? "")}"`);
@@ -18923,7 +19497,7 @@ function jsLiteral(value, depth) {
18923
19497
  if (typeof value === "string") return jsString(value);
18924
19498
  if (typeof value === "number" || typeof value === "boolean") return String(value);
18925
19499
  if (Array.isArray(value)) return `[${value.map((item) => jsLiteral(item, depth + 1)).join(", ")}]`;
18926
- if (isPlainObject7(value)) {
19500
+ if (isPlainObject8(value)) {
18927
19501
  const parts = Object.keys(value).sort(natural3).map((key) => `${/^[A-Za-z_$][\w$]*$/.test(key) ? key : jsString(key)}: ${jsLiteral(value[key], depth + 1)}`);
18928
19502
  return parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
18929
19503
  }
@@ -18935,7 +19509,7 @@ function pyString2(value) {
18935
19509
  function byProblem2(a, b) {
18936
19510
  return natural3(a.code, b.code) || natural3(a.where ?? "", b.where ?? "") || natural3(a.message, b.message);
18937
19511
  }
18938
- function isPlainObject7(value) {
19512
+ function isPlainObject8(value) {
18939
19513
  return value !== null && typeof value === "object" && !Array.isArray(value);
18940
19514
  }
18941
19515
 
@@ -18944,7 +19518,7 @@ var POSITIONAL_KEYS = ["line", "order", "depth", "path"];
18944
19518
  var POSITIONAL_GRID_KEYS = ["uid", "position", "of"];
18945
19519
  function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
18946
19520
  const out = clone5(contract);
18947
- const elements = isPlainObject8(out.elements) ? out.elements : {};
19521
+ const elements = isPlainObject9(out.elements) ? out.elements : {};
18948
19522
  const pagesByPath = new Map((Array.isArray(out.pages) ? out.pages : []).map((page) => [page.path, page]));
18949
19523
  const byPage = /* @__PURE__ */ new Map();
18950
19524
  for (const uid of Object.keys(elements).sort(natural3)) {
@@ -18967,7 +19541,7 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
18967
19541
  element.uid = next;
18968
19542
  if (element.parent !== void 0) element.parent = element.parent === null ? null : rename2(element.parent);
18969
19543
  if (Array.isArray(element.children)) element.children = element.children.map(rename2).sort(natural3);
18970
- if (isPlainObject8(element.grid)) for (const key of POSITIONAL_GRID_KEYS) delete element.grid[key];
19544
+ if (isPlainObject9(element.grid)) for (const key of POSITIONAL_GRID_KEYS) delete element.grid[key];
18971
19545
  rekeyed[next] = element;
18972
19546
  }
18973
19547
  out.elements = sortKeys2(rekeyed);
@@ -19051,7 +19625,7 @@ function rootsAmong(elements, uids) {
19051
19625
  function renameDeep(value, rename2) {
19052
19626
  if (typeof value === "string") return rename2(value);
19053
19627
  if (Array.isArray(value)) return value.map((item) => renameDeep(item, rename2));
19054
- if (isPlainObject8(value)) {
19628
+ if (isPlainObject9(value)) {
19055
19629
  const out = {};
19056
19630
  for (const [key, held] of Object.entries(value)) out[key] = renameDeep(held, rename2);
19057
19631
  return out;
@@ -19065,14 +19639,14 @@ function sortKeys2(object) {
19065
19639
  }
19066
19640
  function clone5(value) {
19067
19641
  if (Array.isArray(value)) return value.map(clone5);
19068
- if (isPlainObject8(value)) {
19642
+ if (isPlainObject9(value)) {
19069
19643
  const out = {};
19070
19644
  for (const [key, held] of Object.entries(value)) out[key] = clone5(held);
19071
19645
  return out;
19072
19646
  }
19073
19647
  return value;
19074
19648
  }
19075
- function isPlainObject8(value) {
19649
+ function isPlainObject9(value) {
19076
19650
  return value !== null && typeof value === "object" && !Array.isArray(value);
19077
19651
  }
19078
19652
 
@@ -20226,7 +20800,7 @@ var CHART_FORMATS4 = Object.freeze(["currency", "number", "percent"]);
20226
20800
  var PLATFORM_FORMAT2 = Object.freeze({ "type": "string", "maxLength": 64, "pattern": "^(General|[^\\u0000-\\u001f]*[#0][^\\u0000-\\u001f]*)$" });
20227
20801
  var NEGATIVE_MODES3 = Object.freeze(["minus", "red_minus", "absolute", "red_absolute"]);
20228
20802
  var FORMAT_PATTERNS3 = Object.freeze({ "currency": "$#,###", "number": "#,###", "percent": '#,###.#"%"' });
20229
- var PLATFORM_NUMBERS3 = Object.freeze({ "dr-app-runtime": "1.8.0", "dr-app-widgets": "1.2.0", "dr-components": "1.2.0", "dr-components-controls": "1.1.0" });
20803
+ var PLATFORM_NUMBERS3 = Object.freeze({ "dr-app-runtime": "1.9.0", "dr-app-widgets": "1.2.0", "dr-components": "1.2.0", "dr-components-controls": "1.1.0" });
20230
20804
  var ROW_ACTION_WHEN3 = Object.freeze({ "dr-components": "1.2.0" });
20231
20805
  var PROPS_SINCE3 = Object.freeze({ "select": { "multiple": "1.3.0", "searchable": "1.3.0", "group_by": "1.3.0", "values": "1.3.0", "options[].group": "1.3.0" }, "data_grid": { "theme": "1.3.0", "hierarchy": "1.3.0", "totals": "1.3.0", "pin": "1.3.0", "csv_params": "1.3.0", "columns[].cell": "1.3.0", "columns[].badge_map": "1.3.0", "columns[].status_map": "1.3.0", "columns[].link_field": "1.3.0", "columns[].call": "1.3.0" } });
20232
20806
  var MODULE_OF3 = Object.freeze({ "widget": "dr-app-widgets", "form": "dr-app-runtime", "list": "dr-app-runtime", "detail": "dr-app-runtime", "queue": "dr-app-runtime", "action": "dr-app-runtime", "filter": "dr-app-runtime", "stat": "dr-app-runtime", "chart": "dr-app-runtime", "custom": "dr-app-runtime", "field": "dr-app-runtime", "number_input": "dr-components", "tabs": "dr-components", "tab_panel": "dr-components", "select": "dr-components", "date_picker": "dr-components", "data_grid": "dr-components", "dialog": "dr-components", "variance_table": "dr-components", "toggle": "dr-components", "control_group": "dr-components", "callout": "dr-components", "scenario_columns": "dr-components", "slider": "dr-components-controls", "button": "dr-components", "status": "dr-components", "badge": "dr-components", "tag": "dr-components", "empty_state": "dr-components", "stepper": "dr-components", "alert": "dr-components", "checkbox": "dr-components", "radio_group": "dr-components", "file_upload": "dr-components", "skeleton": "dr-components", "progress": "dr-components", "accordion": "dr-components", "tooltip_info": "dr-components", "filter_bar": "dr-components", "filter_chip": "dr-components", "date_range": "dr-components", "fiscal_month": "dr-components" });
@@ -20980,7 +21554,7 @@ function readSpec2(handlers) {
20980
21554
  const where2 = handler.file || `handlers.json#${handler.name}`;
20981
21555
  const literals = new Literals2(handler.source, "py");
20982
21556
  const value = literals.constant("SPEC");
20983
- if (!isPlainObject9(value)) {
21557
+ if (!isPlainObject10(value)) {
20984
21558
  unresolved.push({ where: where2, key: "SPEC", text: literals.constantText("SPEC") ?? cut2("SPEC") });
20985
21559
  continue;
20986
21560
  }
@@ -20996,15 +21570,15 @@ function readSpec2(handlers) {
20996
21570
  var DEFAULTS_KEY2 = "_defaults";
20997
21571
  function effective2(table) {
20998
21572
  const defaults = table[DEFAULTS_KEY2];
20999
- if (!isPlainObject9(defaults)) return table;
21573
+ if (!isPlainObject10(defaults)) return table;
21000
21574
  const out = /* @__PURE__ */ Object.create(null);
21001
21575
  for (const [key, row] of Object.entries(table)) {
21002
21576
  if (key === DEFAULTS_KEY2) continue;
21003
- out[key] = key.startsWith("_") || !isPlainObject9(row) ? row : { ...defaults, ...row };
21577
+ out[key] = key.startsWith("_") || !isPlainObject10(row) ? row : { ...defaults, ...row };
21004
21578
  }
21005
21579
  return out;
21006
21580
  }
21007
- function isPlainObject9(value) {
21581
+ function isPlainObject10(value) {
21008
21582
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
21009
21583
  }
21010
21584
 
@@ -21096,7 +21670,7 @@ function readTransforms2(source) {
21096
21670
  if (!literals.has(TRANSFORMS_TABLE2)) return { found: false, texts: null, span: null, text: null };
21097
21671
  const value = literals.constant(TRANSFORMS_TABLE2);
21098
21672
  const span = literals.constantSpan(TRANSFORMS_TABLE2);
21099
- const readable = isPlainObject9(value) && Object.entries(value).every(([path3, text3]) => TRANSFORM_PATH5.test(path3) && typeof text3 === "string");
21673
+ const readable = isPlainObject10(value) && Object.entries(value).every(([path3, text3]) => TRANSFORM_PATH5.test(path3) && typeof text3 === "string");
21100
21674
  if (!readable) return { found: true, texts: null, span, text: literals.constantText(TRANSFORMS_TABLE2) };
21101
21675
  const texts = {};
21102
21676
  for (const path3 of Object.keys(value).sort(natural4)) texts[path3] = value[path3];
@@ -21132,6 +21706,10 @@ var THEME_ATTR3 = "data-dr-theme";
21132
21706
  var DENSITY_ATTR2 = "data-dr-density";
21133
21707
  var THEME_FILE6 = "ui/theme.css";
21134
21708
  var CARD_ATTR2 = "data-dr-card";
21709
+ var SHELL_ATTR2 = "data-dr-shell";
21710
+ var HEADER_ACTIONS_ATTR2 = "data-dr-header-actions";
21711
+ var TITLE_FROM_ATTR2 = "data-dr-title-from";
21712
+ var NAV_COUNTS_ATTR2 = "data-dr-nav-counts";
21135
21713
  var EMPHASIS_ATTR2 = "data-dr-emphasis";
21136
21714
  var MUTATING_VERBS2 = /* @__PURE__ */ new Set(["insert", "update", "delete"]);
21137
21715
  var TABLE_ONLY_VERBS2 = /* @__PURE__ */ new Set([...MUTATING_VERBS2, "get"]);
@@ -21174,8 +21752,10 @@ function extract2(app, { describe: describe7 } = {}) {
21174
21752
  const elements = {};
21175
21753
  const pages = [];
21176
21754
  const pinsByPath = /* @__PURE__ */ new Map();
21755
+ let shell = null;
21177
21756
  for (const page of app.pages) {
21178
21757
  const built = readPage2(app, page, spec, elements, problems);
21758
+ if (shell === null && built.shell !== null && built.shell !== void 0) shell = built.shell;
21179
21759
  if (built.page.owned !== true && !page.text.includes(COMPONENT_ATTR2)) {
21180
21760
  readBundle2(app, page.path, built.page.scripts, elements, problems);
21181
21761
  }
@@ -21238,7 +21818,7 @@ function extract2(app, { describe: describe7 } = {}) {
21238
21818
  v: CONTRACT_V2,
21239
21819
  generated_by: GENERATED_BY2,
21240
21820
  source: describe7 ? "static+describe" : "static",
21241
- app: appSection2(app, manifest),
21821
+ app: { ...appSection2(app, manifest), ...shell === null ? {} : { shell } },
21242
21822
  pins: [...pinsByPath.values()].map((pin) => ({ ...pin, pages: [...new Set(pin.pages)].sort() })).sort((a, b) => a.order - b.order),
21243
21823
  pages,
21244
21824
  elements,
@@ -21256,7 +21836,7 @@ function appSection2(app, manifest) {
21256
21836
  for (const handler of app.handlers) {
21257
21837
  handlers[handler.name] = { http: handler.http || null, entrypoint: handler.file || null };
21258
21838
  }
21259
- const nav = isPlainObject9(manifest.nav) ? manifest.nav : {};
21839
+ const nav = isPlainObject10(manifest.nav) ? manifest.nav : {};
21260
21840
  return {
21261
21841
  id: manifest.id ?? null,
21262
21842
  name: manifest.name ?? null,
@@ -21272,7 +21852,7 @@ function appSection2(app, manifest) {
21272
21852
  api_version: manifest.apiVersion ?? manifest.api_version ?? null,
21273
21853
  routes: routesOf3(manifest),
21274
21854
  handlers,
21275
- capabilities: isPlainObject9(manifest.capabilities) ? manifest.capabilities : {},
21855
+ capabilities: isPlainObject10(manifest.capabilities) ? manifest.capabilities : {},
21276
21856
  data_model: app.dataModel ? normaliseModel2(app.dataModel) : null,
21277
21857
  theme: themeSection2(app)
21278
21858
  };
@@ -21420,14 +22000,21 @@ function readPage2(app, page, spec, elements, problems) {
21420
22000
  ...bands.map((entry, index) => ({ at: entry.node.index, section: banded[index] })),
21421
22001
  ...wrappers.map((node, index) => ({ at: node.index, section: wrapped[index] }))
21422
22002
  ].sort((a, b) => a.at - b.at).map((one) => one.section);
22003
+ for (const entry of significant) {
22004
+ if (entry.type !== "region" || entry.node.attrs[HEADER_ACTIONS_ATTR2] === void 0) continue;
22005
+ for (const child of childrenOf2.get(entry.node.index) ?? []) elements[uids.get(child)].section = "header";
22006
+ }
21423
22007
  const mount = readMount2(scripts, problems);
21424
22008
  const body = tree.nodes.find((node) => node.tag === "body") ?? tree.nodes.find((node) => node.tag === "html");
22009
+ const main2 = tree.nodes.find((node) => node.tag === "main" && "data-dr-app" in node.attrs) ?? null;
21425
22010
  return {
22011
+ shell: shellOf2(main2),
21426
22012
  page: {
21427
22013
  path: page.path,
21428
22014
  route: routeOf3(app.manifest, page.path),
21429
22015
  title: titleOf2(tree, page.text),
21430
22016
  header: headerOf2(tree, page.text),
22017
+ ...navOf2(tree, main2),
21431
22018
  sections,
21432
22019
  scripts: scripts.map((script) => script.path).filter((path3) => path3 !== page.path),
21433
22020
  theme: body?.attrs[THEME_ATTR3] ?? null,
@@ -21521,13 +22108,13 @@ function partCalls2(element) {
21521
22108
  const typed = element.props?.props ?? {};
21522
22109
  const out = [];
21523
22110
  for (const prop of Object.keys(PARTS2[element.type].events ?? {})) {
21524
- if (prop !== "on_row_action" && named5(typed[prop])) out.push(typed[prop]);
22111
+ if (prop !== "on_row_action" && named6(typed[prop])) out.push(typed[prop]);
21525
22112
  }
21526
22113
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
21527
- if (action && typeof action === "object" && named5(action.call)) out.push(action.call);
22114
+ if (action && typeof action === "object" && named6(action.call)) out.push(action.call);
21528
22115
  }
21529
22116
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
21530
- if (column && typeof column === "object" && column.cell === "toggle" && named5(column.call)) out.push(column.call);
22117
+ if (column && typeof column === "object" && column.cell === "toggle" && named6(column.call)) out.push(column.call);
21531
22118
  }
21532
22119
  return out;
21533
22120
  }
@@ -21687,20 +22274,43 @@ function headerOf2(tree, text3) {
21687
22274
  }
21688
22275
  const region = tree.nodes.find((node) => node.attrs[KIT_ATTR2] === "header");
21689
22276
  if (!region) return null;
21690
- const heading = firstTag2(tree, region, "h1");
22277
+ const heading = firstTag2(tree, region, "h1", true);
21691
22278
  if (!heading) return null;
21692
- const summary = firstTag2(tree, region, "p");
21693
- return { title: textOf2(text3, heading), summary: summary ? textOf2(text3, summary) || null : null };
22279
+ const summary = firstTag2(tree, region, "p", true);
22280
+ const out = { title: textOf2(text3, heading), summary: summary ? textOf2(text3, summary) || null : null };
22281
+ const actions = parseJsonAttr2(region.attrs[HEADER_ACTIONS_ATTR2]);
22282
+ if (Array.isArray(actions)) out.actions = actions;
22283
+ const from = region.attrs[TITLE_FROM_ATTR2];
22284
+ if (from !== void 0 && from !== "") out.title_from = from;
22285
+ return out;
21694
22286
  }
21695
- function firstTag2(tree, node, tag) {
22287
+ function firstTag2(tree, node, tag, outer = false) {
21696
22288
  for (const child of node.children) {
21697
22289
  const candidate = tree.nodes[child];
21698
22290
  if (candidate.tag === tag) return candidate;
21699
- const found = firstTag2(tree, candidate, tag);
22291
+ if (outer && (COMPONENT_ATTR2 in candidate.attrs || FIELD_ATTR2 in candidate.attrs)) continue;
22292
+ const found = firstTag2(tree, candidate, tag, outer);
21700
22293
  if (found) return found;
21701
22294
  }
21702
22295
  return null;
21703
22296
  }
22297
+ function parseJsonAttr2(value) {
22298
+ if (value === void 0 || value === "") return void 0;
22299
+ try {
22300
+ return JSON.parse(value);
22301
+ } catch {
22302
+ return void 0;
22303
+ }
22304
+ }
22305
+ function shellOf2(main2) {
22306
+ const shell = parseJsonAttr2(main2?.attrs[SHELL_ATTR2]);
22307
+ return isPlainObject10(shell) ? shell : null;
22308
+ }
22309
+ function navOf2(tree, main2) {
22310
+ const nav = tree.nodes.find((node) => node.tag === "nav" && node.attrs[KIT_ATTR2] === "nav" && node.attrs[NAV_COUNTS_ATTR2] !== void 0);
22311
+ const source = nav?.attrs[NAV_COUNTS_ATTR2] ?? main2?.attrs[NAV_COUNTS_ATTR2];
22312
+ return source === void 0 || source === "" ? {} : { nav: { counts: { source } } };
22313
+ }
21704
22314
  function sectionOf2(node, text3, index) {
21705
22315
  const title = textOf2(text3, node);
21706
22316
  return { id: node.attrs.id ?? slug3(title) ?? `section_${index + 1}`, title };
@@ -21711,13 +22321,13 @@ function wrappedSectionOf2(tree, node, text3, index) {
21711
22321
  const out = { id: id === void 0 || id === "" ? `section_${index + 1}` : id, title: heading ? textOf2(text3, heading) : "" };
21712
22322
  const subtitle = node.children.map((child) => tree.nodes[child]).find((child) => child.tag === "p");
21713
22323
  if (subtitle) out.subtitle = textOf2(text3, subtitle);
21714
- for (const key of BAND_LOOK3) {
22324
+ for (const key of BAND_LOOK4) {
21715
22325
  const value = node.attrs[`data-dr-${key}`];
21716
22326
  if (value !== void 0 && value !== "") out[key] = value;
21717
22327
  }
21718
22328
  return out;
21719
22329
  }
21720
- var BAND_LOOK3 = ["layout", "surface", "aside"];
22330
+ var BAND_LOOK4 = ["layout", "surface", "aside"];
21721
22331
  function slug3(text3) {
21722
22332
  const clean = String(text3 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
21723
22333
  return clean === "" ? null : clean;
@@ -21754,7 +22364,7 @@ function readMount2(scripts, problems) {
21754
22364
  const brace = script.text.indexOf("{", call);
21755
22365
  if (brace === -1) continue;
21756
22366
  const value = new Literals2(script.text, "js").valueAtChar(brace);
21757
- if (!isPlainObject9(value)) {
22367
+ if (!isPlainObject10(value)) {
21758
22368
  problems.push({
21759
22369
  code: "mount_unreadable",
21760
22370
  where: script.path,
@@ -21766,7 +22376,7 @@ function readMount2(scripts, problems) {
21766
22376
  return {
21767
22377
  handler: typeof value.handler === "string" ? value.handler : null,
21768
22378
  prefs: typeof value.prefs === "boolean" ? value.prefs : null,
21769
- defaults: isPlainObject9(value.defaults) ? value.defaults : null
22379
+ defaults: isPlainObject10(value.defaults) ? value.defaults : null
21770
22380
  };
21771
22381
  }
21772
22382
  return null;
@@ -21819,7 +22429,7 @@ function bind2(elements, spec, backend, model) {
21819
22429
  }
21820
22430
  }
21821
22431
  for (const [key, row] of Object.entries(spec.table)) {
21822
- if (!isWidgetKey2(key) || !isPlainObject9(row) || !isPlainObject9(row.params)) continue;
22432
+ if (!isWidgetKey2(key) || !isPlainObject10(row) || !isPlainObject10(row.params)) continue;
21823
22433
  for (const param of Object.keys(row.params)) {
21824
22434
  const entry = controls[param] ?? (controls[param] = { components: [], widgets: [], bound: false });
21825
22435
  if (!entry.widgets.includes(key)) entry.widgets.push(key);
@@ -21861,11 +22471,11 @@ function bindComponents2(elements, { backend = null, model = null } = {}) {
21861
22471
  if (!isComponentType2(element.type) || isDashboardCard2(element)) continue;
21862
22472
  const props = element.props ?? {};
21863
22473
  const entity = typeof props.entity === "string" && props.entity !== "" ? props.entity : null;
21864
- if (named5(props.action)) actionRow(props.action).components.push(uid);
21865
- if (named5(props.source)) actionRow(props.source).reads.push(uid);
22474
+ if (named6(props.action)) actionRow(props.action).components.push(uid);
22475
+ if (named6(props.source)) actionRow(props.source).reads.push(uid);
21866
22476
  if (isPart3(element)) for (const name of partCalls2(element)) actionRow(name).components.push(uid);
21867
22477
  if (entity !== null && element.type !== "field") entityRow(entity).components.push(uid);
21868
- if (!named5(props.field)) continue;
22478
+ if (!named6(props.field)) continue;
21869
22479
  const column = entity === null ? null : (tables[entity]?.columns ?? {})[props.field] ?? null;
21870
22480
  const injected = entity !== null && (has(tables, entity) && INJECTED_COLUMNS3.includes(props.field) || platform(entity) && PLATFORM_TABLES3[entity].includes(props.field));
21871
22481
  const key = `${entity ?? ""}.${props.field}`;
@@ -21892,7 +22502,7 @@ function bindComponents2(elements, { backend = null, model = null } = {}) {
21892
22502
  unreached_mutations: Object.keys(facts).filter((name) => (facts[name].verbs ?? []).some((verb) => MUTATING_VERBS2.has(verb)) && !reached.has(name)).sort(natural5)
21893
22503
  };
21894
22504
  }
21895
- function named5(value) {
22505
+ function named6(value) {
21896
22506
  return typeof value === "string" && value !== "";
21897
22507
  }
21898
22508
  function isWidgetKey2(key) {
@@ -21930,7 +22540,7 @@ function componentProblems2(elements, bindings, { handlers = [], handlersSource
21930
22540
  for (const uid of Object.keys(elements).sort(natural5)) {
21931
22541
  const element = elements[uid];
21932
22542
  if (element.type !== "form") continue;
21933
- const missing = [ACTION_ATTR2, ENTITY_ATTR2].filter((attr3) => !named5(element.props[attr3 === ACTION_ATTR2 ? "action" : "entity"]));
22543
+ const missing = [ACTION_ATTR2, ENTITY_ATTR2].filter((attr3) => !named6(element.props[attr3 === ACTION_ATTR2 ? "action" : "entity"]));
21934
22544
  if (missing.length === 0) continue;
21935
22545
  problems.push({
21936
22546
  code: "unbound_form",
@@ -21952,8 +22562,8 @@ function componentProblems2(elements, bindings, { handlers = [], handlersSource
21952
22562
  for (const uid of Object.keys(elements).sort(natural5)) {
21953
22563
  const element = elements[uid];
21954
22564
  const filter = element.props?.filter;
21955
- if (!isComponentType2(element.type) || !isPlainObject9(filter)) continue;
21956
- const entity = named5(element.props.entity) ? element.props.entity : null;
22565
+ if (!isComponentType2(element.type) || !isPlainObject10(filter)) continue;
22566
+ const entity = named6(element.props.entity) ? element.props.entity : null;
21957
22567
  if (entity === null || !Object.prototype.hasOwnProperty.call(tables, entity)) continue;
21958
22568
  const columns = tables[entity].columns ?? {};
21959
22569
  for (const column of Object.keys(filter).sort()) {
@@ -22001,7 +22611,7 @@ function dataSection2(app, manifest, spec, elements, describe7, problems) {
22001
22611
  ...Object.keys(sources).length > 0 ? { sources } : {}
22002
22612
  };
22003
22613
  if (describe7) {
22004
- const widgets2 = isPlainObject9(describe7.widgets) ? describe7.widgets : {};
22614
+ const widgets2 = isPlainObject10(describe7.widgets) ? describe7.widgets : {};
22005
22615
  const missing = Object.keys(spec.table).filter(isWidgetKey2).filter((key) => !(key in widgets2));
22006
22616
  if (spec.found && spec.unresolved.length === 0 && missing.length > 0) {
22007
22617
  problems.push({
@@ -22050,12 +22660,12 @@ function readSources2(handlers) {
22050
22660
  if (!SOURCES_TABLE2.test(source) || HANDLER_LITERAL2.test(source)) continue;
22051
22661
  const literals = new Literals2(source, "py");
22052
22662
  const table = literals.constant("SOURCES");
22053
- if (!isPlainObject9(table)) continue;
22663
+ if (!isPlainObject10(table)) continue;
22054
22664
  for (const [name, row] of Object.entries(table)) {
22055
- if (isPlainObject9(row) && sources[name] === void 0) sources[name] = row;
22665
+ if (isPlainObject10(row) && sources[name] === void 0) sources[name] = row;
22056
22666
  }
22057
22667
  const carried = literals.constant("TRANSFORMS");
22058
- if (!isPlainObject9(carried)) continue;
22668
+ if (!isPlainObject10(carried)) continue;
22059
22669
  for (const [name, text3] of Object.entries(carried)) {
22060
22670
  const path3 = table[name]?.transform;
22061
22671
  if (typeof path3 === "string" && typeof text3 === "string" && !texts.has(path3)) texts.set(path3, text3);
@@ -22090,7 +22700,7 @@ function specTransforms2(handlers, spec, problems) {
22090
22700
  }
22091
22701
  const unreadable = problems.some((problem5) => problem5.code === "transform_unreadable");
22092
22702
  for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
22093
- const path3 = isPlainObject9(spec.table[key]) ? spec.table[key].transform : void 0;
22703
+ const path3 = isPlainObject10(spec.table[key]) ? spec.table[key].transform : void 0;
22094
22704
  if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
22095
22705
  problems.push({
22096
22706
  code: "transform_not_carried",
@@ -22114,7 +22724,7 @@ function readApp2(handlers, elements, manifest = null) {
22114
22724
  let row = null;
22115
22725
  if (HANDLER_LITERAL2.test(source)) {
22116
22726
  const value = new Literals2(source, "py").constant("HANDLER");
22117
- if (isPlainObject9(value)) row = value;
22727
+ if (isPlainObject10(value)) row = value;
22118
22728
  }
22119
22729
  declared[handler.name] = { role: "custom", ...row ?? {} };
22120
22730
  if (declared[handler.name].requiresRole === void 0) {
@@ -22142,9 +22752,9 @@ function screensOf2(elements, order2 = null) {
22142
22752
  screens.push({
22143
22753
  page: element.page,
22144
22754
  component: element.type,
22145
- id: named5(props.id) ? props.id : element.id ?? null,
22146
- entity: named5(props.entity) ? props.entity : entityAbove2(elements, element),
22147
- handler: named5(props.source) ? props.source : named5(props.action) ? props.action : null
22755
+ id: named6(props.id) ? props.id : element.id ?? null,
22756
+ entity: named6(props.entity) ? props.entity : entityAbove2(elements, element),
22757
+ handler: named6(props.source) ? props.source : named6(props.action) ? props.action : null
22148
22758
  });
22149
22759
  }
22150
22760
  return screens;
@@ -22156,7 +22766,7 @@ function entityAbove2(elements, element) {
22156
22766
  seen.add(at);
22157
22767
  const above = elements[at];
22158
22768
  if (above === void 0) return null;
22159
- if (named5(above.props?.entity)) return above.props.entity;
22769
+ if (named6(above.props?.entity)) return above.props.entity;
22160
22770
  at = above.parent ?? null;
22161
22771
  }
22162
22772
  return null;
@@ -22185,7 +22795,7 @@ function backendSection2(app, manifest, pages, elements) {
22185
22795
  if (!isComponentType2(element.type) || isDashboardCard2(element)) continue;
22186
22796
  for (const key of ["action", "source"]) {
22187
22797
  const name = element.props?.[key];
22188
- if (!named5(name)) continue;
22798
+ if (!named6(name)) continue;
22189
22799
  record2(name, "attribute");
22190
22800
  attributed.add(name);
22191
22801
  }
@@ -22214,14 +22824,14 @@ function backendSection2(app, manifest, pages, elements) {
22214
22824
  data_model: model,
22215
22825
  handlers,
22216
22826
  roles,
22217
- capabilities: isPlainObject9(manifest.capabilities) ? manifest.capabilities : {},
22827
+ capabilities: isPlainObject10(manifest.capabilities) ? manifest.capabilities : {},
22218
22828
  ui: {
22219
22829
  entry,
22220
22830
  handlers_called: [...called].sort(),
22221
22831
  handlers_called_via: [...via.values()].sort((a, b) => natural5(a.name, b.name) || natural5(a.via, b.via)),
22222
22832
  scripts_loaded: [...new Set(pages.flatMap((page) => page.scripts))].sort()
22223
22833
  },
22224
- connect_tables: isPlainObject9(manifest.connectTables) ? manifest.connectTables : isPlainObject9(manifest.connect_tables) ? manifest.connect_tables : {},
22834
+ connect_tables: isPlainObject10(manifest.connectTables) ? manifest.connectTables : isPlainObject10(manifest.connect_tables) ? manifest.connect_tables : {},
22225
22835
  invariants: invariants2(model, roles, handlers)
22226
22836
  };
22227
22837
  }
@@ -22231,7 +22841,7 @@ function callsOnTable2(source, verb) {
22231
22841
  return holders.some((holder) => new RegExp(`(?:^|[^\\w.])(?:${holder})\\s*\\.\\s*${verb}\\s*\\(`, "m").test(source));
22232
22842
  }
22233
22843
  function declaredHandler2(manifest, name) {
22234
- return isPlainObject9(manifest?.handlers) ? manifest.handlers[name] ?? null : null;
22844
+ return isPlainObject10(manifest?.handlers) ? manifest.handlers[name] ?? null : null;
22235
22845
  }
22236
22846
  function signatureOk2(source) {
22237
22847
  const code = source.split("\n").map((line2) => /^\s*#/.test(line2) ? "" : line2).join("\n");
@@ -22258,7 +22868,7 @@ function rolesOf3(manifest) {
22258
22868
  // Declaration order IS the ladder: `authz.py` reads the manifest's list
22259
22869
  // top down, so the first role is the most junior.
22260
22870
  seniority: index,
22261
- tables: isPlainObject9(role?.tables) ? role.tables : {},
22871
+ tables: isPlainObject10(role?.tables) ? role.tables : {},
22262
22872
  // Stated only when the manifest states it, like the platform's own
22263
22873
  // derivation (`app_contract_store._roles_section`): the norm is a role
22264
22874
  // that sees only its own screens, and `[]` on every role would say
@@ -22270,16 +22880,16 @@ function rolesOf3(manifest) {
22270
22880
  }
22271
22881
  function pageRolesOf2(manifest, path3) {
22272
22882
  const pages = Array.isArray(manifest?.pages) ? manifest.pages : [];
22273
- const entry = pages.find((one) => isPlainObject9(one) && one.path === path3);
22883
+ const entry = pages.find((one) => isPlainObject10(one) && one.path === path3);
22274
22884
  const roles = Array.isArray(entry?.roles) ? entry.roles.filter((role) => typeof role === "string" && role !== "") : [];
22275
22885
  return roles.length > 0 ? { roles } : {};
22276
22886
  }
22277
22887
  function normaliseModel2(document) {
22278
22888
  const tables = {};
22279
- const raw = isPlainObject9(document?.tables) ? document.tables : {};
22889
+ const raw = isPlainObject10(document?.tables) ? document.tables : {};
22280
22890
  for (const [name, table] of Object.entries(raw)) {
22281
22891
  const columns = {};
22282
- for (const [column, spec] of Object.entries(isPlainObject9(table?.columns) ? table.columns : {})) {
22892
+ for (const [column, spec] of Object.entries(isPlainObject10(table?.columns) ? table.columns : {})) {
22283
22893
  const pk = Boolean(spec?.pk);
22284
22894
  columns[column] = {
22285
22895
  type: spec?.type ?? null,
@@ -22297,7 +22907,7 @@ function normaliseModel2(document) {
22297
22907
  // `computed` (a child total the platform selects with the row) and
22298
22908
  // `matches` (the column it should agree with), by `display`'s rule:
22299
22909
  // only where stated, so a model without them normalises to its bytes.
22300
- ...isPlainObject9(spec?.computed) ? { computed: spec.computed } : {},
22910
+ ...isPlainObject10(spec?.computed) ? { computed: spec.computed } : {},
22301
22911
  ...typeof spec?.matches === "string" && spec.matches !== "" ? { matches: spec.matches } : {}
22302
22912
  };
22303
22913
  }
@@ -22315,7 +22925,7 @@ function normaliseModel2(document) {
22315
22925
  // row reads as before it is first saved. By `audit`'s rule: only where
22316
22926
  // stated, so a model with neither keeps normalising to its bytes.
22317
22927
  ...table?.singleton === true ? { singleton: true } : {},
22318
- ...table?.singleton === true && isPlainObject9(table?.defaults) ? { defaults: table.defaults } : {},
22928
+ ...table?.singleton === true && isPlainObject10(table?.defaults) ? { defaults: table.defaults } : {},
22319
22929
  columns,
22320
22930
  indexes: Array.isArray(table?.indexes) ? table.indexes.map((index) => Array.isArray(index) ? index.map(String) : [String(index)]) : [],
22321
22931
  injected: [...INJECTED_COLUMNS3]
@@ -22648,10 +23258,10 @@ function isObject5(value) {
22648
23258
  var DECLARES_KEYS2 = Object.freeze(["reads", "calls", "produces", "shows"]);
22649
23259
  var SHOWS_KEYS2 = Object.freeze(["field", "format", "scale"]);
22650
23260
  var SHOWS_FORMATS2 = Object.freeze(["currency", "number", "percent"]);
22651
- var isPlainObject10 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
22652
- var named6 = (value) => typeof value === "string" && value !== "";
22653
- var listOfNames2 = (value) => Array.isArray(value) && value.every(named6);
22654
- var idOf3 = (element, uid) => named6(element?.props?.id) ? element.props.id : uid;
23261
+ var isPlainObject11 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23262
+ var named7 = (value) => typeof value === "string" && value !== "";
23263
+ var listOfNames2 = (value) => Array.isArray(value) && value.every(named7);
23264
+ var idOf3 = (element, uid) => named7(element?.props?.id) ? element.props.id : uid;
22655
23265
  function pyCode2(source) {
22656
23266
  let out = "";
22657
23267
  let i = 0;
@@ -22720,7 +23330,7 @@ function handlerPayloadReads2(row, source) {
22720
23330
  return { generated: false, required: pyPayloadReads2(source).required };
22721
23331
  }
22722
23332
  if (role === "save" || role === "submit") {
22723
- return { generated: true, required: (Array.isArray(row.required) ? row.required : []).filter(named6) };
23333
+ return { generated: true, required: (Array.isArray(row.required) ? row.required : []).filter(named7) };
22724
23334
  }
22725
23335
  return { generated: true, required: [] };
22726
23336
  }
@@ -22890,32 +23500,32 @@ function childIndex2(elements) {
22890
23500
  };
22891
23501
  for (const [uid, element] of Object.entries(elements)) {
22892
23502
  for (const child of Array.isArray(element?.children) ? element.children : []) if (child in elements) add(uid, child);
22893
- if (named6(element?.parent) && element.parent in elements) add(element.parent, uid);
23503
+ if (named7(element?.parent) && element.parent in elements) add(element.parent, uid);
22894
23504
  }
22895
23505
  return index;
22896
23506
  }
22897
23507
  var ROW_HOLDERS2 = /* @__PURE__ */ new Set(["list", "queue", "detail"]);
22898
23508
  var INPUT_PARTS2 = /* @__PURE__ */ new Set(["number_input", "select", "date_picker", "toggle", "slider", "checkbox", "radio_group", "file_upload"]);
22899
23509
  function fieldOf2(element) {
22900
- if (element?.type === "field" && named6(element.props?.field)) return element.props.field;
22901
- if (INPUT_PARTS2.has(element?.type) && named6(element.props?.props?.field)) return element.props.props.field;
23510
+ if (element?.type === "field" && named7(element.props?.field)) return element.props.field;
23511
+ if (INPUT_PARTS2.has(element?.type) && named7(element.props?.props?.field)) return element.props.props.field;
22902
23512
  return null;
22903
23513
  }
22904
23514
  function mentions2(text3, name) {
22905
23515
  return new RegExp(`(^|[^A-Za-z0-9_])${reEscape2(name)}([^A-Za-z0-9_]|$)`).test(text3);
22906
23516
  }
22907
23517
  function payloadProducers2(contract, { moduleSource = () => void 0, pageSource = () => void 0 } = {}) {
22908
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
22909
- const handlers = isPlainObject10(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23518
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23519
+ const handlers = isPlainObject11(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
22910
23520
  const children = childIndex2(elements);
22911
23521
  const out = /* @__PURE__ */ new Map();
22912
23522
  const add = (handler, producer) => {
22913
- if (!named6(handler)) return;
23523
+ if (!named7(handler)) return;
22914
23524
  if (!out.has(handler)) out.set(handler, []);
22915
23525
  out.get(handler).push(producer);
22916
23526
  };
22917
23527
  const insideRow = (uid) => {
22918
- for (let at = elements[uid]?.parent; named6(at) && at in elements; at = elements[at]?.parent) {
23528
+ for (let at = elements[uid]?.parent; named7(at) && at in elements; at = elements[at]?.parent) {
22919
23529
  if (ROW_HOLDERS2.has(elements[at]?.type)) return true;
22920
23530
  }
22921
23531
  return false;
@@ -22936,16 +23546,16 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
22936
23546
  };
22937
23547
  for (const uid of Object.keys(elements).sort()) {
22938
23548
  const element = elements[uid];
22939
- const props = isPlainObject10(element?.props) ? element.props : {};
23549
+ const props = isPlainObject11(element?.props) ? element.props : {};
22940
23550
  const page = element?.page ?? null;
22941
- const typed = isPlainObject10(props.props) ? props.props : {};
22942
- if (named6(props.source)) add(props.source, { page, uid, what: "reader", keys: null });
23551
+ const typed = isPlainObject11(props.props) ? props.props : {};
23552
+ if (named7(props.source)) add(props.source, { page, uid, what: "reader", keys: null });
22943
23553
  switch (element?.type) {
22944
23554
  case "form": {
22945
23555
  const keys2 = fieldsUnder(uid);
22946
23556
  if (insideRow(uid) && !keys2.includes("id")) keys2.push("id");
22947
23557
  const by = handlers[props.action]?.by;
22948
- if (named6(by) && !keys2.includes(by)) keys2.push(by);
23558
+ if (named7(by) && !keys2.includes(by)) keys2.push(by);
22949
23559
  add(props.action, { page, uid, what: "form", keys: keys2 });
22950
23560
  break;
22951
23561
  }
@@ -22960,41 +23570,41 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
22960
23570
  // stepper's current step `{step}`; a file_upload's on_upload its field.
22961
23571
  case "button":
22962
23572
  add(typed.on_click, { page, uid, what: "button", keys: typed.reason === "required" || typed.reason === "optional" ? ["reason"] : [] });
22963
- for (const entry of Array.isArray(typed.split) ? typed.split : []) if (isPlainObject10(entry)) add(entry.on_click, { page, uid, what: "button", keys: [] });
23573
+ for (const entry of Array.isArray(typed.split) ? typed.split : []) if (isPlainObject11(entry)) add(entry.on_click, { page, uid, what: "button", keys: [] });
22964
23574
  break;
22965
23575
  case "empty_state":
22966
23576
  add(typed.on_action, { page, uid, what: "empty_state", keys: [] });
22967
23577
  break;
22968
23578
  case "stepper":
22969
- for (const step of Array.isArray(typed.steps) ? typed.steps : []) if (isPlainObject10(step)) add(step.action, { page, uid, what: "stepper", keys: ["step"] });
23579
+ for (const step of Array.isArray(typed.steps) ? typed.steps : []) if (isPlainObject11(step)) add(step.action, { page, uid, what: "stepper", keys: ["step"] });
22970
23580
  break;
22971
23581
  case "file_upload":
22972
- add(typed.on_upload, { page, uid, what: "file_upload", keys: named6(typed.field) ? [typed.field] : [] });
23582
+ add(typed.on_upload, { page, uid, what: "file_upload", keys: named7(typed.field) ? [typed.field] : [] });
22973
23583
  break;
22974
23584
  case "data_grid":
22975
23585
  add(typed.on_row, { page, uid, what: "data_grid", keys: ["row"] });
22976
23586
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
22977
- if (isPlainObject10(action) && named6(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
23587
+ if (isPlainObject11(action) && named7(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
22978
23588
  }
22979
23589
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
22980
- if (isPlainObject10(column) && column.cell === "toggle" && named6(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
23590
+ if (isPlainObject11(column) && column.cell === "toggle" && named7(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
22981
23591
  }
22982
23592
  break;
22983
23593
  case "action": {
22984
23594
  const keys2 = [];
22985
- if (insideRow(uid)) keys2.push(named6(props.param) ? props.param : "id");
22986
- else if (named6(props.param)) keys2.push(props.param);
23595
+ if (insideRow(uid)) keys2.push(named7(props.param) ? props.param : "id");
23596
+ else if (named7(props.param)) keys2.push(props.param);
22987
23597
  if (handlers[props.action]?.role === "decide") keys2.push("decision", "note");
22988
23598
  add(props.action, { page, uid, what: "action", keys: keys2 });
22989
23599
  break;
22990
23600
  }
22991
23601
  case "custom": {
22992
- const declares = isPlainObject10(props.declares) ? props.declares : {};
22993
- const produces = isPlainObject10(declares.produces) ? declares.produces : {};
23602
+ const declares = isPlainObject11(props.declares) ? props.declares : {};
23603
+ const produces = isPlainObject11(declares.produces) ? declares.produces : {};
22994
23604
  for (const [handler, keys2] of Object.entries(produces)) {
22995
23605
  add(handler, { page, uid, what: "custom", keys: listOfNames2(keys2) ? [...keys2] : null });
22996
23606
  }
22997
- const source = named6(props.module) ? moduleSource(props.module) : void 0;
23607
+ const source = named7(props.module) ? moduleSource(props.module) : void 0;
22998
23608
  if (typeof source !== "string") {
22999
23609
  add("*", { page, uid, what: "custom", keys: null });
23000
23610
  break;
@@ -23020,10 +23630,10 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
23020
23630
  }
23021
23631
  const modules = /* @__PURE__ */ new Set();
23022
23632
  for (const element of Object.values(elements)) {
23023
- if (element?.type === "custom" && named6(element.props?.module)) modules.add(`${element.page}|${element.props.module}`);
23633
+ if (element?.type === "custom" && named7(element.props?.module)) modules.add(`${element.page}|${element.props.module}`);
23024
23634
  }
23025
23635
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23026
- if (!named6(page?.path)) continue;
23636
+ if (!named7(page?.path)) continue;
23027
23637
  const text3 = pageSource(page.path);
23028
23638
  const unknown = { page: page.path, uid: null, what: "script", keys: null };
23029
23639
  if (typeof text3 !== "string") {
@@ -23068,7 +23678,7 @@ function either4(items) {
23068
23678
  return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
23069
23679
  }
23070
23680
  function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, moduleSource, pageSource } = {}) {
23071
- const handlers = isPlainObject10(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23681
+ const handlers = isPlainObject11(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23072
23682
  const producers = payloadProducers2(contract, { moduleSource, pageSource });
23073
23683
  const anywhere = producers.get("*") ?? [];
23074
23684
  const out = [];
@@ -23079,7 +23689,7 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
23079
23689
  if (own.length === 0 || anywhere.length > 0 || own.some((one) => one.keys === null)) continue;
23080
23690
  const produced = [];
23081
23691
  for (const one of own) for (const key of one.keys) if (!produced.includes(key)) produced.push(key);
23082
- const pages = [...new Set(own.map((one) => one.page).filter(named6))];
23692
+ const pages = [...new Set(own.map((one) => one.page).filter(named7))];
23083
23693
  for (const key of reads.required) {
23084
23694
  if (produced.includes(key)) continue;
23085
23695
  const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
@@ -23091,12 +23701,12 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
23091
23701
  return out;
23092
23702
  }
23093
23703
  function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {}) {
23094
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23704
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23095
23705
  const out = [];
23096
23706
  for (const uid of Object.keys(elements).sort()) {
23097
23707
  const element = elements[uid];
23098
- const props = isPlainObject10(element?.props) ? element.props : {};
23099
- if (element?.type !== "custom" || !isPlainObject10(props.declares) || !named6(props.module)) continue;
23708
+ const props = isPlainObject11(element?.props) ? element.props : {};
23709
+ if (element?.type !== "custom" || !isPlainObject11(props.declares) || !named7(props.module)) continue;
23100
23710
  const source = moduleSource(props.module);
23101
23711
  if (typeof source !== "string") continue;
23102
23712
  const calls = listOfNames2(props.declares.calls) ? props.declares.calls : [];
@@ -23124,32 +23734,32 @@ function undeclaredCallMessage2(id, module, handler, calls, how) {
23124
23734
  return { problem: problem5, fix, message: `${problem5} ${fix}` };
23125
23735
  }
23126
23736
  function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
23127
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23128
- const handlers = isPlainObject10(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23737
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23738
+ const handlers = isPlainObject11(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23129
23739
  const parents = /* @__PURE__ */ new Map();
23130
23740
  for (const [parent, kids] of childIndex2(elements)) for (const kid of kids) if (!parents.has(kid)) parents.set(kid, parent);
23131
23741
  const out = [];
23132
23742
  for (const uid of Object.keys(elements).sort()) {
23133
23743
  const element = elements[uid];
23134
- const typed = isPlainObject10(element?.props?.props) ? element.props.props : {};
23135
- if (element?.type !== "file_upload" || !named6(typed.field)) continue;
23744
+ const typed = isPlainObject11(element?.props?.props) ? element.props.props : {};
23745
+ if (element?.type !== "file_upload" || !named7(typed.field)) continue;
23136
23746
  const field = typed.field;
23137
23747
  const receivers = [];
23138
- if (named6(typed.on_upload)) receivers.push([typed.on_upload, "its on_upload"]);
23139
- for (let at = parents.get(uid); named6(at) && at in elements; at = parents.get(at)) {
23748
+ if (named7(typed.on_upload)) receivers.push([typed.on_upload, "its on_upload"]);
23749
+ for (let at = parents.get(uid); named7(at) && at in elements; at = parents.get(at)) {
23140
23750
  const holder = elements[at];
23141
- if (holder?.type === "dialog" && named6(holder.props?.props?.on_confirm)) {
23751
+ if (holder?.type === "dialog" && named7(holder.props?.props?.on_confirm)) {
23142
23752
  receivers.push([holder.props.props.on_confirm, `the dialog "${idOf3(holder, at)}" it sits in`]);
23143
23753
  break;
23144
23754
  }
23145
- if (holder?.type === "form" && named6(holder.props?.action)) {
23755
+ if (holder?.type === "form" && named7(holder.props?.action)) {
23146
23756
  receivers.push([holder.props.action, `the form "${idOf3(holder, at)}" it sits in`]);
23147
23757
  break;
23148
23758
  }
23149
23759
  }
23150
23760
  for (const [handler, via] of receivers) {
23151
23761
  const row = handlers[handler];
23152
- if (!isPlainObject10(row)) continue;
23762
+ if (!isPlainObject11(row)) continue;
23153
23763
  const role = row.role ?? "custom";
23154
23764
  let read;
23155
23765
  if (role === "custom") {
@@ -23172,16 +23782,16 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
23172
23782
  return out;
23173
23783
  }
23174
23784
  function stepsWithoutAction2(contract) {
23175
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23785
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23176
23786
  const out = [];
23177
23787
  for (const uid of Object.keys(elements).sort()) {
23178
23788
  const element = elements[uid];
23179
23789
  const steps = element?.type === "stepper" && Array.isArray(element.props?.props?.steps) ? element.props.props.steps : [];
23180
23790
  steps.forEach((step, index) => {
23181
- if (index === steps.length - 1 || !isPlainObject10(step) || named6(step.action)) return;
23791
+ if (index === steps.length - 1 || !isPlainObject11(step) || named7(step.action)) return;
23182
23792
  const id = idOf3(element, uid);
23183
- const name = named6(step.id) ? step.id : String(index);
23184
- const next = named6(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
23793
+ const name = named7(step.id) ? step.id : String(index);
23794
+ const next = named7(steps[index + 1]?.id) ? steps[index + 1].id : "the next step";
23185
23795
  const problem5 = `stepper "${id}" step "${name}" names no action, so no handler moves the flow on from it \u2014 a flow that reaches it stays there.`;
23186
23796
  const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
23187
23797
  out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem5, fix, message: `${problem5} ${fix}` });
@@ -23445,6 +24055,10 @@ function checkComponents(app, contract, contractError) {
23445
24055
  }
23446
24056
  };
23447
24057
  for (const uid of uids) if (isPart4(elements[uid])) refsIn(elements[uid].props?.props);
24058
+ for (const page of Array.isArray(contract.pages) ? contract.pages : []) {
24059
+ const source = page?.nav?.counts?.source;
24060
+ if (typeof source === "string" && source !== "") attributeRefs.add(source);
24061
+ }
23448
24062
  for (const uid of uids) {
23449
24063
  const element = elements[uid];
23450
24064
  if (element.type !== "custom" || typeof element.props?.module !== "string") continue;
@@ -23586,45 +24200,45 @@ function isReferenced4(name, pageText) {
23586
24200
  }
23587
24201
 
23588
24202
  // ../flows/appspace-builder/kit/contract/levers.mjs
23589
- var isPlainObject11 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23590
- var named7 = (value) => typeof value === "string" && value !== "";
24203
+ var isPlainObject12 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24204
+ var named8 = (value) => typeof value === "string" && value !== "";
23591
24205
  function both2(items) {
23592
24206
  if (items.length <= 1) return items.join("");
23593
24207
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
23594
24208
  }
23595
24209
  function paramPages(contract) {
23596
24210
  return (Array.isArray(contract?.pages) ? contract.pages : []).filter(
23597
- (page) => page?.owned !== true && named7(page?.path) && Array.isArray(page.params) && page.params.length > 0
24211
+ (page) => page?.owned !== true && named8(page?.path) && Array.isArray(page.params) && page.params.length > 0
23598
24212
  );
23599
24213
  }
23600
24214
  function namesOf(page) {
23601
24215
  const out = [];
23602
- for (const param of page.params) if (isPlainObject11(param) && named7(param.name) && !out.includes(param.name)) out.push(param.name);
24216
+ for (const param of page.params) if (isPlainObject12(param) && named8(param.name) && !out.includes(param.name)) out.push(param.name);
23603
24217
  return out;
23604
24218
  }
23605
24219
  function elementsOn(contract, path3) {
23606
- const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
24220
+ const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23607
24221
  return Object.keys(elements).sort().filter((uid) => elements[uid]?.page === path3).map((uid) => [uid, elements[uid]]);
23608
24222
  }
23609
24223
  function paramModeOf(element) {
23610
- const props = isPlainObject11(element?.props) ? element.props : {};
23611
- if (element?.type === "control") return named7(props.param) ? props.param : named7(element.binding) ? element.binding : null;
24224
+ const props = isPlainObject12(element?.props) ? element.props : {};
24225
+ if (element?.type === "control") return named8(props.param) ? props.param : named8(element.binding) ? element.binding : null;
23612
24226
  if (element?.type === "field" || element?.type === "action") return null;
23613
- if (named7(props.param) && element?.type !== "custom") return props.param;
23614
- const typed = isPlainObject11(props.props) ? props.props : {};
23615
- return named7(typed.param) ? typed.param : null;
24227
+ if (named8(props.param) && element?.type !== "custom") return props.param;
24228
+ const typed = isPlainObject12(props.props) ? props.props : {};
24229
+ return named8(typed.param) ? typed.param : null;
23616
24230
  }
23617
24231
  function paramsWrittenBy(element) {
23618
24232
  const out = [];
23619
24233
  const one = paramModeOf(element);
23620
24234
  if (one !== null) out.push(one);
23621
24235
  if (element?.type === "custom") return out;
23622
- const typed = isPlainObject11(element?.props?.props) ? element.props.props : {};
23623
- if (named7(typed.param_to)) out.push(typed.param_to);
24236
+ const typed = isPlainObject12(element?.props?.props) ? element.props.props : {};
24237
+ if (named8(typed.param_to)) out.push(typed.param_to);
23624
24238
  for (const filter of Array.isArray(typed.filters) ? typed.filters : []) {
23625
- if (!isPlainObject11(filter)) continue;
23626
- if (named7(filter.param)) out.push(filter.param);
23627
- if (named7(filter.param_to)) out.push(filter.param_to);
24239
+ if (!isPlainObject12(filter)) continue;
24240
+ if (named8(filter.param)) out.push(filter.param);
24241
+ if (named8(filter.param_to)) out.push(filter.param_to);
23628
24242
  }
23629
24243
  return out;
23630
24244
  }
@@ -23636,10 +24250,10 @@ function paramWriters(contract, { moduleSource = () => void 0 } = {}) {
23636
24250
  for (const [, element] of elementsOn(contract, page.path)) {
23637
24251
  for (const name of paramsWrittenBy(element)) written.add(name);
23638
24252
  if (element?.type !== "custom") continue;
23639
- const props = isPlainObject11(element.props) ? element.props : {};
23640
- const produces = isPlainObject11(props.declares?.produces) ? props.declares.produces : {};
23641
- for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named7(key)) written.add(key);
23642
- if (!named7(props.module)) continue;
24253
+ const props = isPlainObject12(element.props) ? element.props : {};
24254
+ const produces = isPlainObject12(props.declares?.produces) ? props.declares.produces : {};
24255
+ for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named8(key)) written.add(key);
24256
+ if (!named8(props.module)) continue;
23643
24257
  const source = moduleSource(props.module);
23644
24258
  if (typeof source !== "string") {
23645
24259
  unknown = true;
@@ -23650,6 +24264,13 @@ function paramWriters(contract, { moduleSource = () => void 0 } = {}) {
23650
24264
  else written.add(call.name);
23651
24265
  }
23652
24266
  }
24267
+ if (contract?.app?.shell?.nav === "rail") {
24268
+ for (const entry of Array.isArray(contract.app.shell.rail?.entries) ? contract.app.shell.rail.entries : []) {
24269
+ if (isPlainObject12(entry?.param)) {
24270
+ for (const name of Object.keys(entry.param)) if (named8(name)) written.add(name);
24271
+ }
24272
+ }
24273
+ }
23653
24274
  out.set(page.path, { written, unknown });
23654
24275
  }
23655
24276
  return out;
@@ -23663,7 +24284,7 @@ function unboundControls(contract, files = {}) {
23663
24284
  for (const name of namesOf(page)) {
23664
24285
  if (written.has(name)) continue;
23665
24286
  const param = page.params.find((one) => one?.name === name) ?? {};
23666
- const kind = named7(param.kind) ? param.kind : null;
24287
+ const kind = named8(param.kind) ? param.kind : null;
23667
24288
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
23668
24289
  const problem5 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
23669
24290
  const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
@@ -23678,16 +24299,16 @@ function honoursOf(sources, name) {
23678
24299
  const queue = [name];
23679
24300
  while (queue.length > 0) {
23680
24301
  const one = queue.shift();
23681
- if (seen.has(one) || !isPlainObject11(sources[one])) continue;
24302
+ if (seen.has(one) || !isPlainObject12(sources[one])) continue;
23682
24303
  seen.add(one);
23683
- for (const honored of Array.isArray(sources[one].honors) ? sources[one].honors : []) if (named7(honored) && !out.includes(honored)) out.push(honored);
24304
+ for (const honored of Array.isArray(sources[one].honors) ? sources[one].honors : []) if (named8(honored) && !out.includes(honored)) out.push(honored);
23684
24305
  if (Array.isArray(sources[one].from)) queue.push(...sources[one].from);
23685
24306
  }
23686
24307
  return out;
23687
24308
  }
23688
24309
  function unreadParams(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
23689
- const sources = isPlainObject11(contract?.data?.sources) ? contract.data.sources : {};
23690
- const handlers = isPlainObject11(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24310
+ const sources = isPlainObject12(contract?.data?.sources) ? contract.data.sources : {};
24311
+ const handlers = isPlainObject12(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23691
24312
  const out = [];
23692
24313
  for (const page of paramPages(contract)) {
23693
24314
  const read = /* @__PURE__ */ new Set();
@@ -23696,15 +24317,15 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
23696
24317
  const custom = [];
23697
24318
  let unknown = false;
23698
24319
  for (const [, element] of elementsOn(contract, page.path)) {
23699
- const props = isPlainObject11(element?.props) ? element.props : {};
23700
- const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named7);
24320
+ const props = isPlainObject12(element?.props) ? element.props : {};
24321
+ const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named8);
23701
24322
  for (const one of reads) {
23702
24323
  if (Object.prototype.hasOwnProperty.call(sources, one)) {
23703
24324
  if (!named_.includes(one)) named_.push(one);
23704
24325
  for (const honored of honoursOf(sources, one)) read.add(honored);
23705
24326
  } else if (handlers[one]?.role === "custom" && !custom.includes(one)) custom.push(one);
23706
24327
  }
23707
- if (element?.type === "custom" && named7(props.module)) {
24328
+ if (element?.type === "custom" && named8(props.module)) {
23708
24329
  const text3 = moduleSource(props.module);
23709
24330
  if (typeof text3 === "string") modules.push(text3);
23710
24331
  else unknown = true;
@@ -23746,28 +24367,28 @@ function checkLevers(app, contract, contractError) {
23746
24367
  }
23747
24368
 
23748
24369
  // ../flows/appspace-builder/kit/contract/look.mjs
23749
- var isPlainObject12 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23750
- var named8 = (value) => typeof value === "string" && value !== "";
23751
- function humanise2(name) {
24370
+ var isPlainObject13 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24371
+ var named9 = (value) => typeof value === "string" && value !== "";
24372
+ function humanise3(name) {
23752
24373
  const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
23753
24374
  return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
23754
24375
  }
23755
- var WORDING = {
23756
- custom: { noun: "card", fallback: (p) => [humanise2(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
23757
- chart: { noun: "card", fallback: (p) => [humanise2(String(p.y ?? "")), "its y column"] },
23758
- widget: { noun: "card", fallback: (p) => [humanise2(named8(p.source) ? p.source : "widget"), "its source"] },
23759
- stat: { noun: "tile", fallback: (p) => [humanise2(String(p.measure ?? "")), "its measure"] },
23760
- list: { noun: "card", fallback: (p) => [humanise2(named8(p.entity) ? p.entity : "list"), "its entity"] },
23761
- queue: { noun: "card", fallback: (p) => [humanise2(named8(p.entity) ? p.entity : "queue"), "its entity"] },
24376
+ var WORDING2 = {
24377
+ custom: { noun: "card", fallback: (p) => [humanise3(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
24378
+ chart: { noun: "card", fallback: (p) => [humanise3(String(p.y ?? "")), "its y column"] },
24379
+ widget: { noun: "card", fallback: (p) => [humanise3(named9(p.source) ? p.source : "widget"), "its source"] },
24380
+ stat: { noun: "tile", fallback: (p) => [humanise3(String(p.measure ?? "")), "its measure"] },
24381
+ list: { noun: "card", fallback: (p) => [humanise3(named9(p.entity) ? p.entity : "list"), "its entity"] },
24382
+ queue: { noun: "card", fallback: (p) => [humanise3(named9(p.entity) ? p.entity : "queue"), "its entity"] },
23762
24383
  form: { noun: "card", fallback: () => ["New", "the runtime\u2019s default"] },
23763
24384
  dialog: { noun: "dialog" }
23764
24385
  };
23765
- var TITLED = Object.fromEntries(
23766
- TITLED_TYPES2.map((type) => [type, { noun: CONTROL_PARTS2.includes(type) ? "input" : "card", fallback: () => [humanise2(type), "its type"], ...WORDING[type] }])
24386
+ var TITLED2 = Object.fromEntries(
24387
+ TITLED_TYPES2.map((type) => [type, { noun: CONTROL_PARTS2.includes(type) ? "input" : "card", fallback: () => [humanise3(type), "its type"], ...WORDING2[type] }])
23767
24388
  );
23768
- var LAYOUT = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
23769
- function rootsOf2(contract, page) {
23770
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
24389
+ var LAYOUT2 = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
24390
+ function rootsOf3(contract, page) {
24391
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
23771
24392
  if (Array.isArray(page?.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid] !== void 0);
23772
24393
  const contained = /* @__PURE__ */ new Set();
23773
24394
  for (const element of Object.values(elements)) for (const child of element?.children ?? []) contained.add(child);
@@ -23776,8 +24397,8 @@ function rootsOf2(contract, page) {
23776
24397
  return element?.page === page?.path && (element.parent === null || element.parent === void 0) && !contained.has(uid);
23777
24398
  });
23778
24399
  }
23779
- function untitledCards(contract) {
23780
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
24400
+ function untitledCards2(contract) {
24401
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
23781
24402
  const out = [];
23782
24403
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23783
24404
  if (page?.owned === true) continue;
@@ -23788,7 +24409,7 @@ function untitledCards(contract) {
23788
24409
  const element = elements[uid];
23789
24410
  if (element === void 0 || seen.has(uid)) continue;
23790
24411
  seen.add(uid);
23791
- if (LAYOUT.has(element.type ?? "")) {
24412
+ if (LAYOUT2.has(element.type ?? "")) {
23792
24413
  const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
23793
24414
  walk4(children);
23794
24415
  } else {
@@ -23796,19 +24417,19 @@ function untitledCards(contract) {
23796
24417
  }
23797
24418
  }
23798
24419
  };
23799
- walk4(rootsOf2(contract, page));
24420
+ walk4(rootsOf3(contract, page));
23800
24421
  for (const uid of drawn2) {
23801
24422
  const element = elements[uid];
23802
24423
  const type = element.type ?? "";
23803
- const titled = TITLED[type];
24424
+ const titled = TITLED2[type];
23804
24425
  if (titled === void 0) continue;
23805
- const props = isPlainObject12(element.props) ? element.props : {};
23806
- const typed = isPlainObject12(props.props) ? props.props : {};
23807
- const card = isPlainObject12(element.card) ? element.card : isPlainObject12(props.card) ? props.card : {};
23808
- if (named8(props.label) || named8(card.title)) continue;
23809
- if (type === "dialog" ? named8(typed.title) : named8(typed.label)) continue;
23810
- const id = named8(element.id) ? element.id : named8(props.id) ? props.id : null;
23811
- const [title, from] = named8(props.field) ? [humanise2(props.field), "its field"] : id !== null ? [humanise2(id), "its id"] : titled.fallback(props);
24426
+ const props = isPlainObject13(element.props) ? element.props : {};
24427
+ const typed = isPlainObject13(props.props) ? props.props : {};
24428
+ const card = isPlainObject13(element.card) ? element.card : isPlainObject13(props.card) ? props.card : {};
24429
+ if (named9(props.label) || named9(card.title)) continue;
24430
+ if (type === "dialog" ? named9(typed.title) : named9(typed.label)) continue;
24431
+ const id = named9(element.id) ? element.id : named9(props.id) ? props.id : null;
24432
+ const [title, from] = named9(props.field) ? [humanise3(props.field), "its field"] : id !== null ? [humanise3(id), "its id"] : titled.fallback(props);
23812
24433
  const what = id !== null ? `${type} "${id}"` : `the ${type} ${uid}`;
23813
24434
  const shown2 = title === "" ? "has no title at all" : `is titled "${title}" after ${from}`;
23814
24435
  out.push({ page: page.path, uid, type, id, noun: titled.noun, title, from, message: `${what} has no label, so its ${titled.noun} ${shown2}` });
@@ -23816,49 +24437,49 @@ function untitledCards(contract) {
23816
24437
  }
23817
24438
  return out;
23818
24439
  }
23819
- var BAND_LOOK4 = ["subtitle", "layout", "surface", "aside"];
23820
- function lookPages(contract) {
23821
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
24440
+ var BAND_LOOK5 = ["subtitle", "layout", "surface", "aside"];
24441
+ function lookPages2(contract) {
24442
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
23822
24443
  const out = /* @__PURE__ */ new Set();
23823
24444
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23824
- if (page?.owned === true || !named8(page?.path)) continue;
23825
- const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK4.some((key) => named8(section?.[key])));
23826
- const cards = Object.values(elements).some((element) => element?.page === page.path && (isPlainObject12(element.card) || named8(element.emphasis)));
24445
+ if (page?.owned === true || !named9(page?.path)) continue;
24446
+ const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK5.some((key) => named9(section?.[key])));
24447
+ const cards = Object.values(elements).some((element) => element?.page === page.path && (isPlainObject13(element.card) || named9(element.emphasis)));
23827
24448
  if (bands || cards) out.add(page.path);
23828
24449
  }
23829
24450
  return out;
23830
24451
  }
23831
- var SPAN_LAYOUTS = /* @__PURE__ */ new Set([void 0, "grid"]);
23832
- var CONTROL_TYPES = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS2]);
23833
- function bandsOf2(contract, page) {
23834
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23835
- const roots = rootsOf2(contract, page);
23836
- return (Array.isArray(page.sections) ? page.sections : []).filter((section) => named8(section?.id)).map((section) => ({ section, roots: roots.filter((uid) => elements[uid]?.section === section.id) }));
24452
+ var SPAN_LAYOUTS2 = /* @__PURE__ */ new Set([void 0, "grid"]);
24453
+ var CONTROL_TYPES2 = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS2]);
24454
+ function bandsOf3(contract, page) {
24455
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
24456
+ const roots = rootsOf3(contract, page);
24457
+ return (Array.isArray(page.sections) ? page.sections : []).filter((section) => named9(section?.id)).map((section) => ({ section, roots: roots.filter((uid) => elements[uid]?.section === section.id) }));
23837
24458
  }
23838
- function nameOf3(uid, element) {
23839
- const id = named8(element?.props?.id) ? element.props.id : named8(element?.id) ? element.id : null;
24459
+ function nameOf4(uid, element) {
24460
+ const id = named9(element?.props?.id) ? element.props.id : named9(element?.id) ? element.id : null;
23840
24461
  return id === null ? `the ${element?.type ?? "element"} ${uid}` : `${element.type} "${id}"`;
23841
24462
  }
23842
- function isControl(element, moduleSource) {
23843
- const props = isPlainObject12(element?.props) ? element.props : {};
23844
- if (CONTROL_TYPES.has(element?.type)) return true;
23845
- if (element?.type !== "custom" && element?.type !== "action" && named8(props.param)) return true;
23846
- if (isPlainObject12(props.props) && named8(props.props.param)) return true;
23847
- if (element?.type === "custom" && named8(props.module)) {
24463
+ function isControl2(element, moduleSource) {
24464
+ const props = isPlainObject13(element?.props) ? element.props : {};
24465
+ if (CONTROL_TYPES2.has(element?.type)) return true;
24466
+ if (element?.type !== "custom" && element?.type !== "action" && named9(props.param)) return true;
24467
+ if (isPlainObject13(props.props) && named9(props.props.param)) return true;
24468
+ if (element?.type === "custom" && named9(props.module)) {
23848
24469
  const source = moduleSource(props.module);
23849
24470
  return typeof source === "string" && scanUiSetParams2(source).length > 0;
23850
24471
  }
23851
24472
  return false;
23852
24473
  }
23853
- function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23854
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23855
- const looked = lookPages(contract);
24474
+ function layoutProblems2(contract, { moduleSource = () => void 0 } = {}) {
24475
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
24476
+ const looked = lookPages2(contract);
23856
24477
  const out = [];
23857
24478
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23858
24479
  if (!looked.has(page?.path)) continue;
23859
- for (const { section, roots } of bandsOf2(contract, page)) {
24480
+ for (const { section, roots } of bandsOf3(contract, page)) {
23860
24481
  const band = `band "${section.id}" on ${page.path}`;
23861
- const layout = named8(section.layout) ? section.layout : void 0;
24482
+ const layout = named9(section.layout) ? section.layout : void 0;
23862
24483
  if (layout === "kpi_strip") {
23863
24484
  for (const uid of roots) {
23864
24485
  const element = elements[uid];
@@ -23868,30 +24489,30 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23868
24489
  page: page.path,
23869
24490
  section: section.id,
23870
24491
  uid,
23871
- message: `${band} is a kpi_strip, which draws equal KPI tiles, and holds ${nameOf3(uid, element)}; move it to a band of its own, or make it a stat (or a kpi widget)`
24492
+ message: `${band} is a kpi_strip, which draws equal KPI tiles, and holds ${nameOf4(uid, element)}; move it to a band of its own, or make it a stat (or a kpi widget)`
23872
24493
  });
23873
24494
  }
23874
24495
  }
23875
24496
  if (layout === "sidebar" && roots.length > 0) {
23876
24497
  const uid = section.aside === "start" ? roots[0] : roots[roots.length - 1];
23877
- if (!isControl(elements[uid], moduleSource)) {
24498
+ if (!isControl2(elements[uid], moduleSource)) {
23878
24499
  out.push({
23879
24500
  rule: "L2",
23880
24501
  page: page.path,
23881
24502
  section: section.id,
23882
24503
  uid,
23883
- message: `${band} is a sidebar whose aside (its ${section.aside === "start" ? "first" : "last"} element) is ${nameOf3(uid, elements[uid])}; the narrow column holds the controls that turn the page \u2014 a filter, an input, a part in param mode, a custom that calls ui.setParam \u2014 so put one there, or pick another layout`
24504
+ message: `${band} is a sidebar whose aside (its ${section.aside === "start" ? "first" : "last"} element) is ${nameOf4(uid, elements[uid])}; the narrow column holds the controls that turn the page \u2014 a filter, an input, a part in param mode, a custom that calls ui.setParam \u2014 so put one there, or pick another layout`
23884
24505
  });
23885
24506
  }
23886
24507
  }
23887
- if (SPAN_LAYOUTS.has(layout)) {
24508
+ if (SPAN_LAYOUTS2.has(layout)) {
23888
24509
  let row = [];
23889
24510
  let used = 0;
23890
24511
  for (const uid of roots) {
23891
24512
  const raw = elements[uid]?.grid?.span;
23892
24513
  const span = raw === null || raw === void 0 ? 12 : Number(raw);
23893
24514
  if (!Number.isInteger(span) || span < 1 || span > 12) {
23894
- out.push({ rule: "L4", page: page.path, section: section.id, uid, message: `${band}: ${nameOf3(uid, elements[uid])} spans ${JSON.stringify(raw)}, and a span is a whole number of the twelve columns, 1 to 12` });
24515
+ out.push({ rule: "L4", page: page.path, section: section.id, uid, message: `${band}: ${nameOf4(uid, elements[uid])} spans ${JSON.stringify(raw)}, and a span is a whole number of the twelve columns, 1 to 12` });
23895
24516
  continue;
23896
24517
  }
23897
24518
  if (used + span > 12) {
@@ -23902,7 +24523,7 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23902
24523
  page: page.path,
23903
24524
  section: section.id,
23904
24525
  uid,
23905
- message: `${band}: ${row.map((one) => nameOf3(one.uid, elements[one.uid])).join(" + ")} + ${nameOf3(uid, elements[uid])} span ${spans.join(" + ")} = ${spans.reduce((a, b) => a + b, 0)} on one row of twelve, so the last wraps and leaves ${12 - used} column${12 - used === 1 ? "" : "s"} of the row empty; make the row's spans add up to 12`
24526
+ message: `${band}: ${row.map((one) => nameOf4(one.uid, elements[one.uid])).join(" + ")} + ${nameOf4(uid, elements[uid])} span ${spans.join(" + ")} = ${spans.reduce((a, b) => a + b, 0)} on one row of twelve, so the last wraps and leaves ${12 - used} column${12 - used === 1 ? "" : "s"} of the row empty; make the row's spans add up to 12`
23906
24527
  });
23907
24528
  }
23908
24529
  row = [];
@@ -23920,6 +24541,137 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23920
24541
  }
23921
24542
  return out;
23922
24543
  }
24544
+ var HEADER_ACTION_TYPES2 = ["button", "dialog"];
24545
+ function headerActionProblems2(contract) {
24546
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
24547
+ const out = [];
24548
+ for (const [pageIndex, page] of (Array.isArray(contract?.pages) ? contract.pages : []).entries()) {
24549
+ if (page?.owned === true || !isPlainObject13(page?.header) || page.header.actions === void 0) continue;
24550
+ const say = (index, id, uid, message) => out.push({ rule: "L5", code: "header_action_unknown", page: page.path, pageIndex, index, id, uid, message });
24551
+ const actions = page.header.actions;
24552
+ if (!Array.isArray(actions) || actions.length === 0) {
24553
+ say(null, actions, null, `${page.path} states header.actions as ${JSON.stringify(actions)}; it is a list of the ids of the buttons and dialogs drawn beside the title`);
24554
+ continue;
24555
+ }
24556
+ const seen = /* @__PURE__ */ new Set();
24557
+ for (const [index, id] of actions.entries()) {
24558
+ if (!named9(id)) {
24559
+ say(index, id, null, `${page.path}'s header.actions[${index}] is ${JSON.stringify(id)}; each entry is an element id (props.id) on the page`);
24560
+ continue;
24561
+ }
24562
+ if (seen.has(id)) {
24563
+ say(index, id, null, `${page.path}'s header.actions lists "${id}" twice; the runtime draws it once`);
24564
+ continue;
24565
+ }
24566
+ seen.add(id);
24567
+ const hits = Object.keys(elements).filter((uid) => {
24568
+ const element2 = elements[uid];
24569
+ return element2?.page === page.path && element2.type !== "region" && (named9(element2.props?.id) ? element2.props.id : element2.id) === id;
24570
+ });
24571
+ if (hits.length === 0) {
24572
+ say(index, id, null, `${page.path}'s header.actions names "${id}", and no element on the page has that id, so nothing is drawn beside the title; name a button or dialog of this page by its props.id`);
24573
+ continue;
24574
+ }
24575
+ const element = elements[hits[0]];
24576
+ if (!HEADER_ACTION_TYPES2.includes(element.type)) {
24577
+ say(index, id, hits[0], `${page.path}'s header.actions names ${nameOf4(hits[0], element)}, and a header action is a ${HEADER_ACTION_TYPES2.join(" or a ")}; make it one, or leave it in its band`);
24578
+ }
24579
+ }
24580
+ }
24581
+ return out;
24582
+ }
24583
+ function navCountProblems2(contract) {
24584
+ const out = [];
24585
+ const handlers = isPlainObject13(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24586
+ const pages = (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true);
24587
+ const entries = Array.isArray(contract?.app?.shell?.rail?.entries) ? contract.app.shell.rail.entries : [];
24588
+ const measured = entries.map((entry, index) => ({ entry, index })).filter(({ entry }) => named9(entry?.measure));
24589
+ const counted = pages.filter((page) => isPlainObject13(page.nav) && page.nav.counts !== void 0);
24590
+ if (counted.length === 0) {
24591
+ if (measured.length > 0) {
24592
+ out.push({
24593
+ rule: "L6",
24594
+ page: null,
24595
+ where: "app.shell.rail.entries",
24596
+ message: `the rail's entries name measures (${measured.map(({ entry }) => JSON.stringify(entry.measure)).join(", ")}) and no page states nav.counts.source, so no count is ever drawn; state the summary handler on the pages that draw the rail`
24597
+ });
24598
+ }
24599
+ return out;
24600
+ }
24601
+ const measuresChecked = /* @__PURE__ */ new Set();
24602
+ for (const page of counted) {
24603
+ const source = page.nav.counts?.source;
24604
+ const where2 = `${page.path} nav.counts.source`;
24605
+ if (!named9(source)) {
24606
+ out.push({ rule: "L6", page: page.path, where: where2, message: `${where2} is ${JSON.stringify(source ?? null)}; it names the summary handler whose measures are the rail's counts` });
24607
+ continue;
24608
+ }
24609
+ const handler = handlers[source];
24610
+ if (!isPlainObject13(handler) || handler.role !== "summary") {
24611
+ out.push({
24612
+ rule: "L6",
24613
+ page: page.path,
24614
+ where: where2,
24615
+ message: `${where2} is "${source}", which is ${isPlainObject13(handler) ? `a ${JSON.stringify(handler.role ?? null)} handler` : "not a handler of this app"}; the rail's counts are one number per measure, which a summary handler answers`
24616
+ });
24617
+ continue;
24618
+ }
24619
+ if (!isPlainObject13(handler.measures) || measuresChecked.has(source)) continue;
24620
+ measuresChecked.add(source);
24621
+ for (const { entry, index } of measured) {
24622
+ if (Object.prototype.hasOwnProperty.call(handler.measures, entry.measure)) continue;
24623
+ out.push({
24624
+ rule: "L6",
24625
+ page: page.path,
24626
+ where: `app.shell.rail.entries[${index}].measure`,
24627
+ message: `rail entry "${entry.label}" counts measure "${entry.measure}", which "${source}" (${page.path} nav.counts.source) does not answer (it has ${Object.keys(handler.measures).map((one) => JSON.stringify(one)).join(", ")}), so its count is never drawn`
24628
+ });
24629
+ }
24630
+ }
24631
+ return out;
24632
+ }
24633
+ function appRoles2(contract) {
24634
+ const raw = Array.isArray(contract?.backend?.roles) ? contract.backend.roles : [];
24635
+ return raw.map((role) => typeof role === "string" ? { name: role, sees: [] } : { name: role?.name, sees: Array.isArray(role?.sees) ? role.sees.filter(named9) : [] }).filter((role) => named9(role.name));
24636
+ }
24637
+ function railReachProblems2(contract) {
24638
+ const out = [];
24639
+ const rail = contract?.app?.shell?.rail;
24640
+ if (!isPlainObject13(rail)) return out;
24641
+ const roles = appRoles2(contract);
24642
+ if (roles.length === 0) return out;
24643
+ const declared = new Map(roles.map((role) => [role.name, role]));
24644
+ const pages = new Map((Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => named9(page?.path)).map((page) => [page.path, page]));
24645
+ const openers = (page) => Array.isArray(page?.roles) && page.roles.filter(named9).length > 0 ? page.roles.filter(named9) : null;
24646
+ const canOpen = (name, page) => {
24647
+ const open2 = openers(page);
24648
+ if (open2 === null) return true;
24649
+ const role = declared.get(name);
24650
+ return open2.includes(name) || (role?.sees ?? []).some((seen) => open2.includes(seen));
24651
+ };
24652
+ for (const key of ["entries", "footer"]) {
24653
+ for (const [index, entry] of (Array.isArray(rail[key]) ? rail[key] : []).entries()) {
24654
+ if (!isPlainObject13(entry) || !named9(entry.label) || !named9(entry.page)) continue;
24655
+ const page = pages.get(entry.page);
24656
+ if (page === void 0) continue;
24657
+ const where2 = `app.shell.rail.${key}[${index}]`;
24658
+ const viewers = named9(entry.role) ? entry.role.split(/\s+/).filter(named9) : openers(page) ?? roles.map((role) => role.name);
24659
+ const known = viewers.filter((name) => declared.has(name));
24660
+ if (known.length === 0) {
24661
+ out.push({ rule: "L7", where: where2, message: `rail ${key === "footer" ? "footer link" : "entry"} "${entry.label}" is drawn for ${viewers.map((one) => JSON.stringify(one)).join(", ")}, and the app declares none of them (${roles.map((role) => JSON.stringify(role.name)).join(", ")}), so nobody sees it` });
24662
+ continue;
24663
+ }
24664
+ if (!known.some((name) => canOpen(name, page))) {
24665
+ out.push({
24666
+ rule: "L7",
24667
+ where: where2,
24668
+ message: `rail ${key === "footer" ? "footer link" : "entry"} "${entry.label}" links to ${entry.page}, which ${known.map((one) => JSON.stringify(one)).join(", ")} may not open (it is for ${openers(page).map((one) => JSON.stringify(one)).join(", ")}), so the entry is dead; give it the page's roles, or link a page they may open`
24669
+ });
24670
+ }
24671
+ }
24672
+ }
24673
+ return out;
24674
+ }
23923
24675
 
23924
24676
  // ../flows/appspace-builder/kit/contract/platformPalette.mjs
23925
24677
  var PLATFORM_CHART2 = Object.freeze([
@@ -24141,7 +24893,7 @@ function rgbText2(parts) {
24141
24893
 
24142
24894
  // ../flows/appspace-builder/kit/checks/rules/look.mjs
24143
24895
  var THEME_FILE8 = "ui/theme.css";
24144
- var RULES3 = ["L1", "L2", "L3", "L4"];
24896
+ var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7"];
24145
24897
  function checkLook(app, contract, contractError) {
24146
24898
  const view = {
24147
24899
  ...app,
@@ -24153,14 +24905,23 @@ function checkLook(app, contract, contractError) {
24153
24905
  return { violations, skipped: RULES3.map((rule) => ({ rule, reason })) };
24154
24906
  }
24155
24907
  const moduleSource = (module) => app.ui.find((file) => file.path === `ui/${module}`)?.text;
24156
- for (const found of layoutProblems(contract, { moduleSource })) {
24908
+ for (const found of layoutProblems2(contract, { moduleSource })) {
24157
24909
  violations.push({ rule: found.rule, where: found.uid ?? found.page, message: found.message });
24158
24910
  }
24159
- const looked = lookPages(contract);
24160
- for (const found of untitledCards(contract)) {
24911
+ const looked = lookPages2(contract);
24912
+ for (const found of untitledCards2(contract)) {
24161
24913
  if (!looked.has(found.page)) continue;
24162
24914
  violations.push({ rule: "L3", where: found.uid, message: `${found.message}; give it props.label (or card.title) \u2014 what a reader should call this ${found.noun}` });
24163
24915
  }
24916
+ for (const found of headerActionProblems2(contract)) {
24917
+ violations.push({ rule: "L5", where: found.uid ?? `${found.page} header.actions`, message: `${found.code}: ${found.message}` });
24918
+ }
24919
+ for (const found of navCountProblems2(contract)) {
24920
+ violations.push({ rule: "L6", where: found.where, message: found.message });
24921
+ }
24922
+ for (const found of railReachProblems2(contract)) {
24923
+ violations.push({ rule: "L7", where: found.where, message: found.message });
24924
+ }
24164
24925
  return { violations, skipped: [] };
24165
24926
  }
24166
24927
 
@@ -24321,7 +25082,7 @@ function rolesOf5(manifest) {
24321
25082
  // ../flows/appspace-builder/kit/checks/rules/ux.mjs
24322
25083
  var RULES4 = ["A3.23", "A3.24", "A3.25", "A3.26", "A3.27"];
24323
25084
  var STATE_COLUMNS = ["status", "state"];
24324
- var isPlainObject13 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
25085
+ var isPlainObject14 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
24325
25086
  var said = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
24326
25087
  var OPENERS4 = /* @__PURE__ */ new Set(["dialog", "list", "queue", "detail"]);
24327
25088
  var ROWS = /* @__PURE__ */ new Set(["list", "queue", "data_grid"]);
@@ -24330,9 +25091,9 @@ function checkUx(app, contract, contractError) {
24330
25091
  const reason = `the contract could not be extracted (${contractError ?? "no reason given"}), so the forms, lists and dialogs are not knowable here`;
24331
25092
  return { violations: [], skipped: RULES4.map((rule) => ({ rule, reason })) };
24332
25093
  }
24333
- const elements = isPlainObject13(contract.elements) ? contract.elements : {};
24334
- const handlers = isPlainObject13(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
24335
- const tables = isPlainObject13(contract.app?.data_model?.tables) ? contract.app.data_model.tables : {};
25094
+ const elements = isPlainObject14(contract.elements) ? contract.elements : {};
25095
+ const handlers = isPlainObject14(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
25096
+ const tables = isPlainObject14(contract.app?.data_model?.tables) ? contract.app.data_model.tables : {};
24336
25097
  const uids = Object.keys(elements).sort(byPageThenRank);
24337
25098
  const kidsOf = (uid) => {
24338
25099
  const el = elements[uid];
@@ -24370,15 +25131,15 @@ function checkUx(app, contract, contractError) {
24370
25131
  if (!ROWS.has(el?.type)) continue;
24371
25132
  if (ancestors(uid).some((up) => up.type === "detail")) continue;
24372
25133
  const table = tableOf2(el);
24373
- if (!table || table.startsWith("_") || !isPlainObject13(tables[table]?.columns) || tables[table].singleton === true) continue;
25134
+ if (!table || table.startsWith("_") || !isPlainObject14(tables[table]?.columns) || tables[table].singleton === true) continue;
24374
25135
  const columns = tables[table].columns;
24375
- const pk = Object.keys(columns).find((c) => isPlainObject13(columns[c]) && columns[c].pk === true) ?? "id";
25136
+ const pk = Object.keys(columns).find((c) => isPlainObject14(columns[c]) && columns[c].pk === true) ?? "id";
24376
25137
  const fields = Object.keys(columns).filter((c) => c !== pk && !c.startsWith("_"));
24377
25138
  let shown2;
24378
25139
  let opens3;
24379
25140
  if (el.type === "data_grid") {
24380
- const typed = isPlainObject13(el.props?.props) ? el.props.props : {};
24381
- shown2 = new Set((Array.isArray(typed.columns) ? typed.columns : []).map((c) => isPlainObject13(c) ? c.field : c).filter(said));
25141
+ const typed = isPlainObject14(el.props?.props) ? el.props.props : {};
25142
+ shown2 = new Set((Array.isArray(typed.columns) ? typed.columns : []).map((c) => isPlainObject14(c) ? c.field : c).filter(said));
24382
25143
  opens3 = Boolean(said(typed.on_row)) || Array.isArray(typed.row_actions) && typed.row_actions.length > 0;
24383
25144
  } else {
24384
25145
  const kids = kidsOf(uid).map((k) => elements[k]);
@@ -24398,7 +25159,7 @@ function checkUx(app, contract, contractError) {
24398
25159
  for (const uid of uids) {
24399
25160
  const el = elements[uid];
24400
25161
  if (el?.type !== "dialog") continue;
24401
- const typed = isPlainObject13(el.props?.props) ? el.props.props : {};
25162
+ const typed = isPlainObject14(el.props?.props) ? el.props.props : {};
24402
25163
  if (typed.values_from === "row") continue;
24403
25164
  if (said(typed.trigger) || said(el.props?.label) || said(typed.title)) continue;
24404
25165
  violations.push({
@@ -24411,8 +25172,8 @@ function checkUx(app, contract, contractError) {
24411
25172
  const el = elements[uid];
24412
25173
  if (!ROWS.has(el?.type) || el.type === "data_grid") continue;
24413
25174
  const table = tableOf2(el);
24414
- const columns = isPlainObject13(tables[table]?.columns) ? tables[table].columns : {};
24415
- const state = STATE_COLUMNS.find((c) => isPlainObject13(columns[c]) && Array.isArray(columns[c].enum));
25175
+ const columns = isPlainObject14(tables[table]?.columns) ? tables[table].columns : {};
25176
+ const state = STATE_COLUMNS.find((c) => isPlainObject14(columns[c]) && Array.isArray(columns[c].enum));
24416
25177
  const seen = /* @__PURE__ */ new Map();
24417
25178
  for (const kid of kidsOf(uid)) {
24418
25179
  const action = elements[kid];
@@ -24453,8 +25214,8 @@ function split(uid) {
24453
25214
  }
24454
25215
 
24455
25216
  // ../flows/appspace-builder/kit/contract/workflow.mjs
24456
- var isPlainObject14 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24457
- var named9 = (value) => typeof value === "string" && value !== "";
25217
+ var isPlainObject15 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
25218
+ var named10 = (value) => typeof value === "string" && value !== "";
24458
25219
  function either5(items) {
24459
25220
  if (items.length <= 1) return items.join("");
24460
25221
  return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
@@ -24470,16 +25231,16 @@ function roleModel2(contract) {
24470
25231
  const sees = /* @__PURE__ */ new Map();
24471
25232
  const home = /* @__PURE__ */ new Map();
24472
25233
  for (const row of rows2) {
24473
- if (!isPlainObject14(row) || !named9(row.name) || names.includes(row.name)) continue;
25234
+ if (!isPlainObject15(row) || !named10(row.name) || names.includes(row.name)) continue;
24474
25235
  names.push(row.name);
24475
- sees.set(row.name, new Set((Array.isArray(row.sees) ? row.sees : []).filter(named9)));
24476
- if (named9(row.home)) home.set(row.name, row.home);
25236
+ sees.set(row.name, new Set((Array.isArray(row.sees) ? row.sees : []).filter(named10)));
25237
+ if (named10(row.home)) home.set(row.name, row.home);
24477
25238
  }
24478
25239
  return { names, sees, home };
24479
25240
  }
24480
25241
  function gateRoles2(gate) {
24481
- if (Array.isArray(gate)) return gate.filter(named9);
24482
- return named9(gate) ? gate.split(/[\s,]+/).filter(named9) : [];
25242
+ if (Array.isArray(gate)) return gate.filter(named10);
25243
+ return named10(gate) ? gate.split(/[\s,]+/).filter(named10) : [];
24483
25244
  }
24484
25245
  function seesGate2(model, role, gate) {
24485
25246
  const wanted = gateRoles2(gate);
@@ -24497,7 +25258,7 @@ var HREF2 = /\bhref\s*=\s*["']([^"']*)["']/i;
24497
25258
  var ROLE_ATTR4 = /\bdata-dr-role\s*=\s*["']([^"']*)["']/i;
24498
25259
  var PAGE_LITERAL2 = /["'`](?:\.\/)?([A-Za-z0-9_-]+\.html)(?:[?#][^"'`]*)?["'`]/g;
24499
25260
  function targetOf2(href) {
24500
- if (!named9(href) || /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("/") || href.startsWith("#")) return null;
25261
+ if (!named10(href) || /^(?:[a-z]+:)?\/\//i.test(href) || href.startsWith("/") || href.startsWith("#")) return null;
24501
25262
  const file = href.replace(/^\.\//, "").replace(/[?#].*$/, "");
24502
25263
  if (!/^[A-Za-z0-9_-]+\.html$/.test(file)) return null;
24503
25264
  return `ui/${file}`;
@@ -24535,7 +25296,7 @@ function moduleLinks2(source) {
24535
25296
  return out;
24536
25297
  }
24537
25298
  function pagesOf2(contract) {
24538
- return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => named9(page?.path));
25299
+ return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => named10(page?.path));
24539
25300
  }
24540
25301
  function entryPage2(contract) {
24541
25302
  const pages = pagesOf2(contract);
@@ -24554,10 +25315,10 @@ function reachByRole2(contract, { pageSource = () => void 0, moduleSource = () =
24554
25315
  const model = roleModel2(contract);
24555
25316
  const pages = pagesOf2(contract);
24556
25317
  const byPath = new Map(pages.map((page) => [page.path, page]));
24557
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
25318
+ const elements = isPlainObject15(contract?.elements) ? contract.elements : {};
24558
25319
  const modulesOn = /* @__PURE__ */ new Map();
24559
25320
  for (const element of Object.values(elements)) {
24560
- if (element?.type !== "custom" || !named9(element.page) || !named9(element.props?.module)) continue;
25321
+ if (element?.type !== "custom" || !named10(element.page) || !named10(element.props?.module)) continue;
24561
25322
  if (!modulesOn.has(element.page)) modulesOn.set(element.page, /* @__PURE__ */ new Set());
24562
25323
  modulesOn.get(element.page).add(element.props.module);
24563
25324
  }
@@ -24682,23 +25443,23 @@ function moduleAsks2(source) {
24682
25443
  return ASKS2.some((pattern) => pattern.test(String(source ?? "")));
24683
25444
  }
24684
25445
  function decideAsks2(row) {
24685
- if (row?.role !== "decide" || !isPlainObject14(row.decision)) return false;
25446
+ if (row?.role !== "decide" || !isPlainObject15(row.decision)) return false;
24686
25447
  const names = Object.keys(row.decision);
24687
25448
  if (names.length !== 1) return false;
24688
25449
  const spec = row.decision[names[0]];
24689
- return isPlainObject14(spec) && (spec.note === "required" || spec.note === "optional");
25450
+ return isPlainObject15(spec) && (spec.note === "required" || spec.note === "optional");
24690
25451
  }
24691
25452
  function insideDialog2(elements, uid) {
24692
25453
  const seen = /* @__PURE__ */ new Set();
24693
- for (let at = elements[uid]?.parent; named9(at) && !seen.has(at); at = elements[at]?.parent) {
25454
+ for (let at = elements[uid]?.parent; named10(at) && !seen.has(at); at = elements[at]?.parent) {
24694
25455
  seen.add(at);
24695
25456
  if (elements[at]?.type === "dialog") return true;
24696
25457
  }
24697
25458
  return false;
24698
25459
  }
24699
25460
  function stateCallers2(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
24700
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
24701
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25461
+ const elements = isPlainObject15(contract?.elements) ? contract.elements : {};
25462
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24702
25463
  const changing = new Set(Object.keys(handlers).filter((name) => changesState2(name, handlers[name], handlerSource(name))));
24703
25464
  const out = /* @__PURE__ */ new Map();
24704
25465
  const add = (handler, caller) => {
@@ -24708,10 +25469,10 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24708
25469
  };
24709
25470
  for (const uid of Object.keys(elements).sort()) {
24710
25471
  const element = elements[uid];
24711
- const props = isPlainObject14(element?.props) ? element.props : {};
24712
- const typed = isPlainObject14(props.props) ? props.props : {};
25472
+ const props = isPlainObject15(element?.props) ? element.props : {};
25473
+ const typed = isPlainObject15(props.props) ? props.props : {};
24713
25474
  const page = element?.page ?? null;
24714
- const id = named9(props.id) ? props.id : uid;
25475
+ const id = named10(props.id) ? props.id : uid;
24715
25476
  switch (element?.type) {
24716
25477
  case "form":
24717
25478
  add(props.action, { page, uid, id, what: "form", asks: true });
@@ -24722,27 +25483,27 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24722
25483
  case "data_grid":
24723
25484
  add(typed.on_row, { page, uid, id, what: "grid row click", asks: false });
24724
25485
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
24725
- if (!isPlainObject14(action) || !named9(action.call)) continue;
24726
- const asks = action.confirm === true || named9(action.confirm);
25486
+ if (!isPlainObject15(action) || !named10(action.call)) continue;
25487
+ const asks = action.confirm === true || named10(action.confirm);
24727
25488
  add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
24728
25489
  }
24729
25490
  break;
24730
25491
  case "action":
24731
25492
  case "detail": {
24732
- if (!named9(props.action)) break;
24733
- const asks = insideDialog2(elements, uid) || decideAsks2(handlers[props.action]) || props.confirm === true || named9(props.confirm);
25493
+ if (!named10(props.action)) break;
25494
+ const asks = insideDialog2(elements, uid) || decideAsks2(handlers[props.action]) || props.confirm === true || named10(props.confirm);
24734
25495
  add(props.action, { page, uid, id, what: element.type === "action" ? "button" : "detail button", asks });
24735
25496
  break;
24736
25497
  }
24737
25498
  case "custom": {
24738
- const declares = isPlainObject14(props.declares) ? props.declares : {};
25499
+ const declares = isPlainObject15(props.declares) ? props.declares : {};
24739
25500
  const called = /* @__PURE__ */ new Set([
24740
- ...Array.isArray(declares.calls) ? declares.calls.filter(named9) : [],
24741
- ...Object.keys(isPlainObject14(declares.produces) ? declares.produces : {})
25501
+ ...Array.isArray(declares.calls) ? declares.calls.filter(named10) : [],
25502
+ ...Object.keys(isPlainObject15(declares.produces) ? declares.produces : {})
24742
25503
  ]);
24743
- const source = named9(props.module) ? moduleSource(props.module) : void 0;
25504
+ const source = named10(props.module) ? moduleSource(props.module) : void 0;
24744
25505
  if (typeof source === "string") {
24745
- for (const call of scanUiCalls2(source)) if (named9(call.handler)) called.add(call.handler);
25506
+ for (const call of scanUiCalls2(source)) if (named10(call.handler)) called.add(call.handler);
24746
25507
  }
24747
25508
  const asks = typeof source === "string" ? moduleAsks2(source) : null;
24748
25509
  for (const handler of called) add(handler, { page, uid, id, what: `custom ${props.module ?? ""}`.trim(), asks, module: props.module });
@@ -24755,7 +25516,7 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24755
25516
  return out;
24756
25517
  }
24757
25518
  function unconfirmedStateChanges2(contract, files = {}) {
24758
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25519
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24759
25520
  const out = [];
24760
25521
  for (const [handler, callers] of [...stateCallers2(contract, files).entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
24761
25522
  const bare = callers.filter((one) => one.asks === false);
@@ -24827,7 +25588,7 @@ function requiresReason2(lines, key, vars) {
24827
25588
  return false;
24828
25589
  }
24829
25590
  function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource, pageSource } = {}) {
24830
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25591
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24831
25592
  const producers = payloadProducers2(contract, { moduleSource, pageSource });
24832
25593
  const out = [];
24833
25594
  for (const name of Object.keys(handlers).sort()) {
@@ -24843,7 +25604,7 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
24843
25604
  for (const key of one.keys) {
24844
25605
  if (!REASON_KEYS2.includes(key)) continue;
24845
25606
  if (!sent.has(key)) sent.set(key, /* @__PURE__ */ new Set());
24846
- if (named9(one.page)) sent.get(key).add(one.page);
25607
+ if (named10(one.page)) sent.get(key).add(one.page);
24847
25608
  }
24848
25609
  }
24849
25610
  for (const [key, pages] of [...sent.entries()].sort()) {
@@ -24866,7 +25627,7 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
24866
25627
  return out;
24867
25628
  }
24868
25629
  function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
24869
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25630
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24870
25631
  const out = [];
24871
25632
  for (const name of Object.keys(handlers).sort()) {
24872
25633
  if ((handlers[name]?.role ?? "custom") !== "custom") continue;
@@ -24924,13 +25685,13 @@ function rolesLandingNowhere2(contract) {
24924
25685
  function sourceRoles2(contract, name) {
24925
25686
  const source = contract?.data?.sources?.[name];
24926
25687
  const stated2 = gateRoles2(source?.requiresRole);
24927
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
25688
+ const elements = isPlainObject15(contract?.elements) ? contract.elements : {};
24928
25689
  const pages = new Map(pagesOf2(contract).map((page) => [page.path, page]));
24929
25690
  const union = [];
24930
25691
  let readers = 0;
24931
25692
  let open2 = false;
24932
25693
  for (const element of Object.values(elements)) {
24933
- const props = isPlainObject14(element?.props) ? element.props : {};
25694
+ const props = isPlainObject15(element?.props) ? element.props : {};
24934
25695
  const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []];
24935
25696
  if (!reads.includes(name)) continue;
24936
25697
  readers += 1;
@@ -24965,14 +25726,14 @@ function openToEveryRole2(contract) {
24965
25726
  { one: "page", many: "pages", verb: "open" },
24966
25727
  "State pages[].roles on each: the roles it is for (all of them, if that is what you mean)."
24967
25728
  );
24968
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25729
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24969
25730
  say(
24970
25731
  "handler",
24971
25732
  Object.keys(handlers).sort().filter((name) => gateRoles2(handlers[name]?.requiresRole).length === 0).map((name) => `"${name}"`),
24972
25733
  { one: "handler", many: "handlers", verb: "call" },
24973
25734
  "State requiresRole on each: the roles that may call it (all of them, if that is what you mean)."
24974
25735
  );
24975
- const sources = isPlainObject14(contract?.data?.sources) ? contract.data.sources : {};
25736
+ const sources = isPlainObject15(contract?.data?.sources) ? contract.data.sources : {};
24976
25737
  say(
24977
25738
  "source",
24978
25739
  Object.keys(sources).sort().filter((name) => {
@@ -25019,7 +25780,7 @@ function kitFromDisk2() {
25019
25780
  }
25020
25781
  return kitOnDisk2;
25021
25782
  }
25022
- var ADVISORY = /* @__PURE__ */ new Set(["A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25", "A3.26", "A3.27", "A3.28", "A3.29"]);
25783
+ var ADVISORY = /* @__PURE__ */ new Set(["L6", "L7", "A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25", "A3.26", "A3.27", "A3.28", "A3.29"]);
25023
25784
  function verify2({ app: appRef }, { extractContract: extractContract2 = extract2, kit } = {}) {
25024
25785
  const KIT = kit ?? kitFromDisk2();
25025
25786
  const app = loadApp2(resolve12(appRef.dir));
@@ -27092,18 +27853,18 @@ async function resolveFile(path3, base, { read = readFile11, confine = false, re
27092
27853
  const filename = basename2(full);
27093
27854
  return { bytes, filename, mimeType: mimeTypeOf(filename) };
27094
27855
  }
27095
- function isPlainObject15(value) {
27856
+ function isPlainObject16(value) {
27096
27857
  return typeof value === "object" && value !== null && !Array.isArray(value);
27097
27858
  }
27098
27859
  function fileColumnsOf(model) {
27099
27860
  const out = /* @__PURE__ */ new Set();
27100
- const tables = isPlainObject15(model) ? model["tables"] : void 0;
27101
- if (!isPlainObject15(tables)) return out;
27861
+ const tables = isPlainObject16(model) ? model["tables"] : void 0;
27862
+ if (!isPlainObject16(tables)) return out;
27102
27863
  for (const table of Object.values(tables)) {
27103
- const columns = isPlainObject15(table) ? table["columns"] : void 0;
27104
- if (!isPlainObject15(columns)) continue;
27864
+ const columns = isPlainObject16(table) ? table["columns"] : void 0;
27865
+ if (!isPlainObject16(columns)) continue;
27105
27866
  for (const [name, column] of Object.entries(columns)) {
27106
- const type = typeof column === "string" ? column : isPlainObject15(column) ? column["type"] : void 0;
27867
+ const type = typeof column === "string" ? column : isPlainObject16(column) ? column["type"] : void 0;
27107
27868
  if (type === "file") out.add(name);
27108
27869
  }
27109
27870
  }
@@ -27167,7 +27928,7 @@ function typeOf(value) {
27167
27928
  if (Array.isArray(value)) return "array";
27168
27929
  return typeof value;
27169
27930
  }
27170
- function isPlainObject16(value) {
27931
+ function isPlainObject17(value) {
27171
27932
  return typeof value === "object" && value !== null && !Array.isArray(value);
27172
27933
  }
27173
27934
  function describe6(value) {
@@ -27194,7 +27955,7 @@ function validateShape(shape, path3 = "$") {
27194
27955
  }
27195
27956
  return shape.length === 0 ? [] : validateShape(shape[0], `${path3}[]`);
27196
27957
  }
27197
- if (isPlainObject16(shape)) {
27958
+ if (isPlainObject17(shape)) {
27198
27959
  return Object.entries(shape).flatMap(
27199
27960
  ([key, sub]) => validateShape(sub, `${path3}.${key}`)
27200
27961
  );
@@ -27223,8 +27984,8 @@ function matchShape(value, shape, path3 = "$") {
27223
27984
  if (shape.length === 0) return [];
27224
27985
  return value.flatMap((item, i) => matchShape(item, shape[0], `${path3}[${String(i)}]`));
27225
27986
  }
27226
- if (isPlainObject16(shape)) {
27227
- if (!isPlainObject16(value)) {
27987
+ if (isPlainObject17(shape)) {
27988
+ if (!isPlainObject17(value)) {
27228
27989
  return [`${path3}: expected object, got ${typeOf(value)} (${describe6(value)}).`];
27229
27990
  }
27230
27991
  return Object.entries(shape).flatMap(
@@ -27234,8 +27995,8 @@ function matchShape(value, shape, path3 = "$") {
27234
27995
  return [`${path3}: ${describe6(shape)} is not a shape.`];
27235
27996
  }
27236
27997
  function matchExpect(value, expected, path3 = "$") {
27237
- if (isPlainObject16(expected)) {
27238
- if (!isPlainObject16(value)) {
27998
+ if (isPlainObject17(expected)) {
27999
+ if (!isPlainObject17(value)) {
27239
28000
  return [`${path3}: expected an object, got ${typeOf(value)} (${describe6(value)}).`];
27240
28001
  }
27241
28002
  return Object.entries(expected).flatMap(
@@ -27266,7 +28027,7 @@ function isUiEntry(entry) {
27266
28027
  }
27267
28028
  function parseError(raw, where2) {
27268
28029
  if (raw === true) return {};
27269
- if (!isPlainObject16(raw)) {
28030
+ if (!isPlainObject17(raw)) {
27270
28031
  throw new FixtureError(
27271
28032
  `${where2}: "error" is true, or an object with "type" and/or "contains" \u2014 not ${describe6(raw)}.`
27272
28033
  );
@@ -27284,12 +28045,12 @@ function parseError(raw, where2) {
27284
28045
  }
27285
28046
  var FILE_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
27286
28047
  function parseFiles(raw, payload, where2) {
27287
- if (!isPlainObject16(raw)) {
28048
+ if (!isPlainObject17(raw)) {
27288
28049
  throw new FixtureError(
27289
28050
  `${where2}: "files" maps a payload key to "sample" or a path in the app folder \u2014 not ${describe6(raw)}.`
27290
28051
  );
27291
28052
  }
27292
- if (!isPlainObject16(payload)) {
28053
+ if (!isPlainObject17(payload)) {
27293
28054
  throw new FixtureError(
27294
28055
  `${where2}: a case with "files" puts {"upload": \u2026} under a payload key, so its "payload" is an object.`
27295
28056
  );
@@ -27315,7 +28076,7 @@ function parseFiles(raw, payload, where2) {
27315
28076
  }
27316
28077
  function parseCase(handler, raw, index) {
27317
28078
  const where2 = `handlers.${handler}[${String(index)}]`;
27318
- if (!isPlainObject16(raw)) {
28079
+ if (!isPlainObject17(raw)) {
27319
28080
  throw new FixtureError(`${where2}: a case is an object, not ${describe6(raw)}.`);
27320
28081
  }
27321
28082
  const name = raw["name"];
@@ -27348,11 +28109,11 @@ function parseCase(handler, raw, index) {
27348
28109
  return kase;
27349
28110
  }
27350
28111
  function parseFixtures(raw, source) {
27351
- if (!isPlainObject16(raw)) {
28112
+ if (!isPlainObject17(raw)) {
27352
28113
  throw new FixtureError(`${source}: the fixtures file is a JSON object, not ${describe6(raw)}.`);
27353
28114
  }
27354
28115
  const handlersRaw = raw["handlers"] ?? {};
27355
- if (!isPlainObject16(handlersRaw)) {
28116
+ if (!isPlainObject17(handlersRaw)) {
27356
28117
  throw new FixtureError(`${source}: "handlers" is an object keyed by handler name.`);
27357
28118
  }
27358
28119
  const cases = [];
@@ -27372,7 +28133,7 @@ function parseFixtures(raw, source) {
27372
28133
  });
27373
28134
  }
27374
28135
  const uiRaw = raw["ui"] ?? {};
27375
- if (!isPlainObject16(uiRaw)) {
28136
+ if (!isPlainObject17(uiRaw)) {
27376
28137
  throw new FixtureError(`${source}: "ui" is an object.`);
27377
28138
  }
27378
28139
  const entry = uiRaw["entry"] ?? "index.html";
@@ -29446,45 +30207,45 @@ __export(levers_exports, {
29446
30207
  unboundControls: () => unboundControls2,
29447
30208
  unreadParams: () => unreadParams2
29448
30209
  });
29449
- var isPlainObject17 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
29450
- var named10 = (value) => typeof value === "string" && value !== "";
30210
+ var isPlainObject18 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
30211
+ var named11 = (value) => typeof value === "string" && value !== "";
29451
30212
  function both4(items) {
29452
30213
  if (items.length <= 1) return items.join("");
29453
30214
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
29454
30215
  }
29455
30216
  function paramPages2(contract) {
29456
30217
  return (Array.isArray(contract?.pages) ? contract.pages : []).filter(
29457
- (page) => page?.owned !== true && named10(page?.path) && Array.isArray(page.params) && page.params.length > 0
30218
+ (page) => page?.owned !== true && named11(page?.path) && Array.isArray(page.params) && page.params.length > 0
29458
30219
  );
29459
30220
  }
29460
30221
  function namesOf2(page) {
29461
30222
  const out = [];
29462
- for (const param of page.params) if (isPlainObject17(param) && named10(param.name) && !out.includes(param.name)) out.push(param.name);
30223
+ for (const param of page.params) if (isPlainObject18(param) && named11(param.name) && !out.includes(param.name)) out.push(param.name);
29463
30224
  return out;
29464
30225
  }
29465
30226
  function elementsOn2(contract, path3) {
29466
- const elements = isPlainObject17(contract?.elements) ? contract.elements : {};
30227
+ const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29467
30228
  return Object.keys(elements).sort().filter((uid) => elements[uid]?.page === path3).map((uid) => [uid, elements[uid]]);
29468
30229
  }
29469
30230
  function paramModeOf2(element) {
29470
- const props = isPlainObject17(element?.props) ? element.props : {};
29471
- if (element?.type === "control") return named10(props.param) ? props.param : named10(element.binding) ? element.binding : null;
30231
+ const props = isPlainObject18(element?.props) ? element.props : {};
30232
+ if (element?.type === "control") return named11(props.param) ? props.param : named11(element.binding) ? element.binding : null;
29472
30233
  if (element?.type === "field" || element?.type === "action") return null;
29473
- if (named10(props.param) && element?.type !== "custom") return props.param;
29474
- const typed = isPlainObject17(props.props) ? props.props : {};
29475
- return named10(typed.param) ? typed.param : null;
30234
+ if (named11(props.param) && element?.type !== "custom") return props.param;
30235
+ const typed = isPlainObject18(props.props) ? props.props : {};
30236
+ return named11(typed.param) ? typed.param : null;
29476
30237
  }
29477
30238
  function paramsWrittenBy2(element) {
29478
30239
  const out = [];
29479
30240
  const one = paramModeOf2(element);
29480
30241
  if (one !== null) out.push(one);
29481
30242
  if (element?.type === "custom") return out;
29482
- const typed = isPlainObject17(element?.props?.props) ? element.props.props : {};
29483
- if (named10(typed.param_to)) out.push(typed.param_to);
30243
+ const typed = isPlainObject18(element?.props?.props) ? element.props.props : {};
30244
+ if (named11(typed.param_to)) out.push(typed.param_to);
29484
30245
  for (const filter of Array.isArray(typed.filters) ? typed.filters : []) {
29485
- if (!isPlainObject17(filter)) continue;
29486
- if (named10(filter.param)) out.push(filter.param);
29487
- if (named10(filter.param_to)) out.push(filter.param_to);
30246
+ if (!isPlainObject18(filter)) continue;
30247
+ if (named11(filter.param)) out.push(filter.param);
30248
+ if (named11(filter.param_to)) out.push(filter.param_to);
29488
30249
  }
29489
30250
  return out;
29490
30251
  }
@@ -29496,10 +30257,10 @@ function paramWriters2(contract, { moduleSource = () => void 0 } = {}) {
29496
30257
  for (const [, element] of elementsOn2(contract, page.path)) {
29497
30258
  for (const name of paramsWrittenBy2(element)) written.add(name);
29498
30259
  if (element?.type !== "custom") continue;
29499
- const props = isPlainObject17(element.props) ? element.props : {};
29500
- const produces = isPlainObject17(props.declares?.produces) ? props.declares.produces : {};
29501
- for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named10(key)) written.add(key);
29502
- if (!named10(props.module)) continue;
30260
+ const props = isPlainObject18(element.props) ? element.props : {};
30261
+ const produces = isPlainObject18(props.declares?.produces) ? props.declares.produces : {};
30262
+ for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named11(key)) written.add(key);
30263
+ if (!named11(props.module)) continue;
29503
30264
  const source = moduleSource(props.module);
29504
30265
  if (typeof source !== "string") {
29505
30266
  unknown = true;
@@ -29510,6 +30271,13 @@ function paramWriters2(contract, { moduleSource = () => void 0 } = {}) {
29510
30271
  else written.add(call.name);
29511
30272
  }
29512
30273
  }
30274
+ if (contract?.app?.shell?.nav === "rail") {
30275
+ for (const entry of Array.isArray(contract.app.shell.rail?.entries) ? contract.app.shell.rail.entries : []) {
30276
+ if (isPlainObject18(entry?.param)) {
30277
+ for (const name of Object.keys(entry.param)) if (named11(name)) written.add(name);
30278
+ }
30279
+ }
30280
+ }
29513
30281
  out.set(page.path, { written, unknown });
29514
30282
  }
29515
30283
  return out;
@@ -29523,7 +30291,7 @@ function unboundControls2(contract, files = {}) {
29523
30291
  for (const name of namesOf2(page)) {
29524
30292
  if (written.has(name)) continue;
29525
30293
  const param = page.params.find((one) => one?.name === name) ?? {};
29526
- const kind = named10(param.kind) ? param.kind : null;
30294
+ const kind = named11(param.kind) ? param.kind : null;
29527
30295
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
29528
30296
  const problem5 = `${page.path} declares param "${name}"${kind === null ? "" : ` (${kind})`}, and no control on the page writes it \u2014 no part in param mode, no custom that calls ui.setParam("${name}", \u2026) or declares it in declares.produces \u2014 so the page only ever shows ${opens3}.`;
29529
30297
  const fix = `Give it a control \u2014 a part in param mode (props.props.param "${name}": a slider, a number_input, a select, a date_picker or a toggle), or a custom whose module calls ui.setParam("${name}", value) \u2014 or drop the param.`;
@@ -29538,16 +30306,16 @@ function honoursOf2(sources, name) {
29538
30306
  const queue = [name];
29539
30307
  while (queue.length > 0) {
29540
30308
  const one = queue.shift();
29541
- if (seen.has(one) || !isPlainObject17(sources[one])) continue;
30309
+ if (seen.has(one) || !isPlainObject18(sources[one])) continue;
29542
30310
  seen.add(one);
29543
- for (const honored of Array.isArray(sources[one].honors) ? sources[one].honors : []) if (named10(honored) && !out.includes(honored)) out.push(honored);
30311
+ for (const honored of Array.isArray(sources[one].honors) ? sources[one].honors : []) if (named11(honored) && !out.includes(honored)) out.push(honored);
29544
30312
  if (Array.isArray(sources[one].from)) queue.push(...sources[one].from);
29545
30313
  }
29546
30314
  return out;
29547
30315
  }
29548
30316
  function unreadParams2(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
29549
- const sources = isPlainObject17(contract?.data?.sources) ? contract.data.sources : {};
29550
- const handlers = isPlainObject17(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
30317
+ const sources = isPlainObject18(contract?.data?.sources) ? contract.data.sources : {};
30318
+ const handlers = isPlainObject18(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
29551
30319
  const out = [];
29552
30320
  for (const page of paramPages2(contract)) {
29553
30321
  const read = /* @__PURE__ */ new Set();
@@ -29556,15 +30324,15 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
29556
30324
  const custom = [];
29557
30325
  let unknown = false;
29558
30326
  for (const [, element] of elementsOn2(contract, page.path)) {
29559
- const props = isPlainObject17(element?.props) ? element.props : {};
29560
- const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named10);
30327
+ const props = isPlainObject18(element?.props) ? element.props : {};
30328
+ const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []].filter(named11);
29561
30329
  for (const one of reads) {
29562
30330
  if (Object.prototype.hasOwnProperty.call(sources, one)) {
29563
30331
  if (!named_.includes(one)) named_.push(one);
29564
30332
  for (const honored of honoursOf2(sources, one)) read.add(honored);
29565
30333
  } else if (handlers[one]?.role === "custom" && !custom.includes(one)) custom.push(one);
29566
30334
  }
29567
- if (element?.type === "custom" && named10(props.module)) {
30335
+ if (element?.type === "custom" && named11(props.module)) {
29568
30336
  const text3 = moduleSource(props.module);
29569
30337
  if (typeof text3 === "string") modules.push(text3);
29570
30338
  else unknown = true;
@@ -29589,190 +30357,6 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
29589
30357
  return out;
29590
30358
  }
29591
30359
 
29592
- // ../flows/insights-builder/kit/contract/look.mjs
29593
- var look_exports = {};
29594
- __export(look_exports, {
29595
- humanise: () => humanise3,
29596
- layoutProblems: () => layoutProblems2,
29597
- lookPages: () => lookPages2,
29598
- rootsOf: () => rootsOf3,
29599
- untitledCards: () => untitledCards2
29600
- });
29601
- var isPlainObject18 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
29602
- var named11 = (value) => typeof value === "string" && value !== "";
29603
- function humanise3(name) {
29604
- const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
29605
- return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
29606
- }
29607
- var WORDING2 = {
29608
- custom: { noun: "card", fallback: (p) => [humanise3(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
29609
- chart: { noun: "card", fallback: (p) => [humanise3(String(p.y ?? "")), "its y column"] },
29610
- widget: { noun: "card", fallback: (p) => [humanise3(named11(p.source) ? p.source : "widget"), "its source"] },
29611
- stat: { noun: "tile", fallback: (p) => [humanise3(String(p.measure ?? "")), "its measure"] },
29612
- list: { noun: "card", fallback: (p) => [humanise3(named11(p.entity) ? p.entity : "list"), "its entity"] },
29613
- queue: { noun: "card", fallback: (p) => [humanise3(named11(p.entity) ? p.entity : "queue"), "its entity"] },
29614
- form: { noun: "card", fallback: () => ["New", "the runtime\u2019s default"] },
29615
- dialog: { noun: "dialog" }
29616
- };
29617
- var TITLED2 = Object.fromEntries(
29618
- TITLED_TYPES.map((type) => [type, { noun: CONTROL_PARTS.includes(type) ? "input" : "card", fallback: () => [humanise3(type), "its type"], ...WORDING2[type] }])
29619
- );
29620
- var LAYOUT2 = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
29621
- function rootsOf3(contract, page) {
29622
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29623
- if (Array.isArray(page?.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid] !== void 0);
29624
- const contained = /* @__PURE__ */ new Set();
29625
- for (const element of Object.values(elements)) for (const child of element?.children ?? []) contained.add(child);
29626
- return Object.keys(elements).filter((uid) => {
29627
- const element = elements[uid];
29628
- return element?.page === page?.path && (element.parent === null || element.parent === void 0) && !contained.has(uid);
29629
- });
29630
- }
29631
- function untitledCards2(contract) {
29632
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29633
- const out = [];
29634
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29635
- if (page?.owned === true) continue;
29636
- const seen = /* @__PURE__ */ new Set();
29637
- const drawn2 = [];
29638
- const walk4 = (uids) => {
29639
- for (const uid of uids) {
29640
- const element = elements[uid];
29641
- if (element === void 0 || seen.has(uid)) continue;
29642
- seen.add(uid);
29643
- if (LAYOUT2.has(element.type ?? "")) {
29644
- const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
29645
- walk4(children);
29646
- } else {
29647
- drawn2.push(uid);
29648
- }
29649
- }
29650
- };
29651
- walk4(rootsOf3(contract, page));
29652
- for (const uid of drawn2) {
29653
- const element = elements[uid];
29654
- const type = element.type ?? "";
29655
- const titled = TITLED2[type];
29656
- if (titled === void 0) continue;
29657
- const props = isPlainObject18(element.props) ? element.props : {};
29658
- const typed = isPlainObject18(props.props) ? props.props : {};
29659
- const card = isPlainObject18(element.card) ? element.card : isPlainObject18(props.card) ? props.card : {};
29660
- if (named11(props.label) || named11(card.title)) continue;
29661
- if (type === "dialog" ? named11(typed.title) : named11(typed.label)) continue;
29662
- const id = named11(element.id) ? element.id : named11(props.id) ? props.id : null;
29663
- const [title, from] = named11(props.field) ? [humanise3(props.field), "its field"] : id !== null ? [humanise3(id), "its id"] : titled.fallback(props);
29664
- const what = id !== null ? `${type} "${id}"` : `the ${type} ${uid}`;
29665
- const shown2 = title === "" ? "has no title at all" : `is titled "${title}" after ${from}`;
29666
- out.push({ page: page.path, uid, type, id, noun: titled.noun, title, from, message: `${what} has no label, so its ${titled.noun} ${shown2}` });
29667
- }
29668
- }
29669
- return out;
29670
- }
29671
- var BAND_LOOK5 = ["subtitle", "layout", "surface", "aside"];
29672
- function lookPages2(contract) {
29673
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29674
- const out = /* @__PURE__ */ new Set();
29675
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29676
- if (page?.owned === true || !named11(page?.path)) continue;
29677
- const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK5.some((key) => named11(section?.[key])));
29678
- const cards = Object.values(elements).some((element) => element?.page === page.path && (isPlainObject18(element.card) || named11(element.emphasis)));
29679
- if (bands || cards) out.add(page.path);
29680
- }
29681
- return out;
29682
- }
29683
- var SPAN_LAYOUTS2 = /* @__PURE__ */ new Set([void 0, "grid"]);
29684
- var CONTROL_TYPES2 = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS]);
29685
- function bandsOf3(contract, page) {
29686
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29687
- const roots = rootsOf3(contract, page);
29688
- return (Array.isArray(page.sections) ? page.sections : []).filter((section) => named11(section?.id)).map((section) => ({ section, roots: roots.filter((uid) => elements[uid]?.section === section.id) }));
29689
- }
29690
- function nameOf4(uid, element) {
29691
- const id = named11(element?.props?.id) ? element.props.id : named11(element?.id) ? element.id : null;
29692
- return id === null ? `the ${element?.type ?? "element"} ${uid}` : `${element.type} "${id}"`;
29693
- }
29694
- function isControl2(element, moduleSource) {
29695
- const props = isPlainObject18(element?.props) ? element.props : {};
29696
- if (CONTROL_TYPES2.has(element?.type)) return true;
29697
- if (element?.type !== "custom" && element?.type !== "action" && named11(props.param)) return true;
29698
- if (isPlainObject18(props.props) && named11(props.props.param)) return true;
29699
- if (element?.type === "custom" && named11(props.module)) {
29700
- const source = moduleSource(props.module);
29701
- return typeof source === "string" && scanUiSetParams(source).length > 0;
29702
- }
29703
- return false;
29704
- }
29705
- function layoutProblems2(contract, { moduleSource = () => void 0 } = {}) {
29706
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29707
- const looked = lookPages2(contract);
29708
- const out = [];
29709
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29710
- if (!looked.has(page?.path)) continue;
29711
- for (const { section, roots } of bandsOf3(contract, page)) {
29712
- const band = `band "${section.id}" on ${page.path}`;
29713
- const layout = named11(section.layout) ? section.layout : void 0;
29714
- if (layout === "kpi_strip") {
29715
- for (const uid of roots) {
29716
- const element = elements[uid];
29717
- if (element.type === "stat" || element.type === "widget" && element.props?.kind === "kpi") continue;
29718
- out.push({
29719
- rule: "L1",
29720
- page: page.path,
29721
- section: section.id,
29722
- uid,
29723
- message: `${band} is a kpi_strip, which draws equal KPI tiles, and holds ${nameOf4(uid, element)}; move it to a band of its own, or make it a stat (or a kpi widget)`
29724
- });
29725
- }
29726
- }
29727
- if (layout === "sidebar" && roots.length > 0) {
29728
- const uid = section.aside === "start" ? roots[0] : roots[roots.length - 1];
29729
- if (!isControl2(elements[uid], moduleSource)) {
29730
- out.push({
29731
- rule: "L2",
29732
- page: page.path,
29733
- section: section.id,
29734
- uid,
29735
- message: `${band} is a sidebar whose aside (its ${section.aside === "start" ? "first" : "last"} element) is ${nameOf4(uid, elements[uid])}; the narrow column holds the controls that turn the page \u2014 a filter, an input, a part in param mode, a custom that calls ui.setParam \u2014 so put one there, or pick another layout`
29736
- });
29737
- }
29738
- }
29739
- if (SPAN_LAYOUTS2.has(layout)) {
29740
- let row = [];
29741
- let used = 0;
29742
- for (const uid of roots) {
29743
- const raw = elements[uid]?.grid?.span;
29744
- const span = raw === null || raw === void 0 ? 12 : Number(raw);
29745
- if (!Number.isInteger(span) || span < 1 || span > 12) {
29746
- out.push({ rule: "L4", page: page.path, section: section.id, uid, message: `${band}: ${nameOf4(uid, elements[uid])} spans ${JSON.stringify(raw)}, and a span is a whole number of the twelve columns, 1 to 12` });
29747
- continue;
29748
- }
29749
- if (used + span > 12) {
29750
- if (used < 12) {
29751
- const spans = [...row.map((one) => one.span), span];
29752
- out.push({
29753
- rule: "L4",
29754
- page: page.path,
29755
- section: section.id,
29756
- uid,
29757
- message: `${band}: ${row.map((one) => nameOf4(one.uid, elements[one.uid])).join(" + ")} + ${nameOf4(uid, elements[uid])} span ${spans.join(" + ")} = ${spans.reduce((a, b) => a + b, 0)} on one row of twelve, so the last wraps and leaves ${12 - used} column${12 - used === 1 ? "" : "s"} of the row empty; make the row's spans add up to 12`
29758
- });
29759
- }
29760
- row = [];
29761
- used = 0;
29762
- }
29763
- row.push({ uid, span });
29764
- used += span;
29765
- if (used === 12) {
29766
- row = [];
29767
- used = 0;
29768
- }
29769
- }
29770
- }
29771
- }
29772
- }
29773
- return out;
29774
- }
29775
-
29776
30360
  // src/commands/appspace/_findings.ts
29777
30361
  var DECLARES = declares_exports;
29778
30362
  var LEVERS = levers_exports;