@datarails-ai/dr-appspace-cli 0.10.20 → 0.10.22

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 +1383 -784
  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.20",
16
+ version: "0.10.22",
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-04T13:50:45.446Z";
60
+ var BUILD_TIME = "2026-10-07T10:52:20.870Z";
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) {
@@ -18714,6 +19288,9 @@ function shelf(path3, kit, charts, contract, problems) {
18714
19288
  { path: `vendor/${kit.assistant}/insights.css`, rel: "stylesheet" },
18715
19289
  { path: `vendor/${kit.widgets}/widgets.css`, rel: "stylesheet" },
18716
19290
  ...charts ? [{ path: `vendor/${kit.charts}/echarts.min.js`, rel: "script" }] : [],
19291
+ // Before the assistant: the drawer renders a reply through `window.drMd`
19292
+ // when it is there and prints the raw markdown when it is not.
19293
+ ...kit.markdown ? [{ path: `vendor/${kit.markdown}/md.js`, rel: "script" }] : [],
18717
19294
  { path: `vendor/${kit.assistant}/insights.js`, rel: "script" },
18718
19295
  { path: `vendor/${kit.fmt}/fmt.js`, rel: "script" },
18719
19296
  { path: `vendor/${kit.widgets}/widgets.js`, rel: "script" }
@@ -18747,7 +19324,7 @@ function shelf(path3, kit, charts, contract, problems) {
18747
19324
  };
18748
19325
  }
18749
19326
  function appbar(page, elements) {
18750
- 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 };
18751
19328
  const summary = typeof header.summary === "string" && header.summary !== "" ? `${text2(header.summary)} ` : "";
18752
19329
  return [
18753
19330
  `<header class="${classesOf2(elements, "region", "header", ["dr-appbar"])}" data-dr-kit="header">`,
@@ -18807,7 +19384,7 @@ function controlRow(elements) {
18807
19384
  if (controls.length === 0) return [];
18808
19385
  const out = [`<div class="${classesOf2(elements, "region", "controls", ["dr-controlrow"])}" data-dr-kit="controls">`];
18809
19386
  for (const control of controls) {
18810
- const props = isPlainObject7(control.props) ? control.props : {};
19387
+ const props = isPlainObject8(control.props) ? control.props : {};
18811
19388
  const id = control.id;
18812
19389
  out.push(' <span class="drw-field--inline">');
18813
19390
  if (id && typeof props.label === "string") {
@@ -18853,13 +19430,13 @@ function order(element) {
18853
19430
  return Number.isInteger(element.order) ? element.order : Number.MAX_SAFE_INTEGER;
18854
19431
  }
18855
19432
  function cardBlock(card) {
18856
- const props = isPlainObject7(card.props) ? card.props : {};
19433
+ const props = isPlainObject8(card.props) ? card.props : {};
18857
19434
  const kind = kindOf2(card);
18858
19435
  const classes = (card.classes ?? []).length > 0 ? card.classes.join(" ") : kind === "kpi" ? "dr-card dr-card--kpi" : "dr-card";
18859
- 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" };
18860
19437
  const bodyClass = typeof slots.body === "string" ? slots.body : "widget-body";
18861
19438
  const attrs = [`class="${attr2(classes)}"`];
18862
- const grid = isPlainObject7(card.grid) ? card.grid : {};
19439
+ const grid = isPlainObject8(card.grid) ? card.grid : {};
18863
19440
  if (grid.span !== null && grid.span !== void 0) attrs.push(`data-dr-span="${attr2(grid.span)}"`);
18864
19441
  if (grid.rows !== null && grid.rows !== void 0) attrs.push(`data-dr-rows="${attr2(grid.rows)}"`);
18865
19442
  attrs.push(`data-dr-widget="${attr2(card.id ?? "")}"`);
@@ -18920,7 +19497,7 @@ function jsLiteral(value, depth) {
18920
19497
  if (typeof value === "string") return jsString(value);
18921
19498
  if (typeof value === "number" || typeof value === "boolean") return String(value);
18922
19499
  if (Array.isArray(value)) return `[${value.map((item) => jsLiteral(item, depth + 1)).join(", ")}]`;
18923
- if (isPlainObject7(value)) {
19500
+ if (isPlainObject8(value)) {
18924
19501
  const parts = Object.keys(value).sort(natural3).map((key) => `${/^[A-Za-z_$][\w$]*$/.test(key) ? key : jsString(key)}: ${jsLiteral(value[key], depth + 1)}`);
18925
19502
  return parts.length === 0 ? "{}" : `{ ${parts.join(", ")} }`;
18926
19503
  }
@@ -18932,7 +19509,7 @@ function pyString2(value) {
18932
19509
  function byProblem2(a, b) {
18933
19510
  return natural3(a.code, b.code) || natural3(a.where ?? "", b.where ?? "") || natural3(a.message, b.message);
18934
19511
  }
18935
- function isPlainObject7(value) {
19512
+ function isPlainObject8(value) {
18936
19513
  return value !== null && typeof value === "object" && !Array.isArray(value);
18937
19514
  }
18938
19515
 
@@ -18941,7 +19518,7 @@ var POSITIONAL_KEYS = ["line", "order", "depth", "path"];
18941
19518
  var POSITIONAL_GRID_KEYS = ["uid", "position", "of"];
18942
19519
  function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
18943
19520
  const out = clone5(contract);
18944
- const elements = isPlainObject8(out.elements) ? out.elements : {};
19521
+ const elements = isPlainObject9(out.elements) ? out.elements : {};
18945
19522
  const pagesByPath = new Map((Array.isArray(out.pages) ? out.pages : []).map((page) => [page.path, page]));
18946
19523
  const byPage = /* @__PURE__ */ new Map();
18947
19524
  for (const uid of Object.keys(elements).sort(natural3)) {
@@ -18964,7 +19541,7 @@ function stripPositional(contract, { keepTree = true, order: order2 } = {}) {
18964
19541
  element.uid = next;
18965
19542
  if (element.parent !== void 0) element.parent = element.parent === null ? null : rename2(element.parent);
18966
19543
  if (Array.isArray(element.children)) element.children = element.children.map(rename2).sort(natural3);
18967
- 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];
18968
19545
  rekeyed[next] = element;
18969
19546
  }
18970
19547
  out.elements = sortKeys2(rekeyed);
@@ -19048,7 +19625,7 @@ function rootsAmong(elements, uids) {
19048
19625
  function renameDeep(value, rename2) {
19049
19626
  if (typeof value === "string") return rename2(value);
19050
19627
  if (Array.isArray(value)) return value.map((item) => renameDeep(item, rename2));
19051
- if (isPlainObject8(value)) {
19628
+ if (isPlainObject9(value)) {
19052
19629
  const out = {};
19053
19630
  for (const [key, held] of Object.entries(value)) out[key] = renameDeep(held, rename2);
19054
19631
  return out;
@@ -19062,14 +19639,14 @@ function sortKeys2(object) {
19062
19639
  }
19063
19640
  function clone5(value) {
19064
19641
  if (Array.isArray(value)) return value.map(clone5);
19065
- if (isPlainObject8(value)) {
19642
+ if (isPlainObject9(value)) {
19066
19643
  const out = {};
19067
19644
  for (const [key, held] of Object.entries(value)) out[key] = clone5(held);
19068
19645
  return out;
19069
19646
  }
19070
19647
  return value;
19071
19648
  }
19072
- function isPlainObject8(value) {
19649
+ function isPlainObject9(value) {
19073
19650
  return value !== null && typeof value === "object" && !Array.isArray(value);
19074
19651
  }
19075
19652
 
@@ -19247,6 +19824,7 @@ var CONTROL_MARK = "data-dr-control";
19247
19824
  var CONTROL_KIND = "data-dr-control-kind";
19248
19825
  var CONTROL_BOXES = /* @__PURE__ */ new Set(["controls", "filters"]);
19249
19826
  var WIDGET_LIBRARY2 = /^vendor\/dr-widgets@\d+\.\d+\.\d+\/widgets\.js$/;
19827
+ var ASSISTANT_MODULE = /^vendor\/dr-insights@(\d+\.\d+\.\d+)\/(insights\.(?:css|js))$/;
19250
19828
  var WIDGET_ID = /^[a-z][a-z0-9_-]*$/;
19251
19829
  var REQUIRED_WIDGET_ATTRS = ["data-dr-widget-title", "data-dr-widget-description", "data-dr-widget-source"];
19252
19830
  var SPAN_MARK = "data-dr-span";
@@ -19326,12 +19904,22 @@ function checkPage2(app, page, kit) {
19326
19904
  const css = `vendor/${kit.assistant}/insights.css`;
19327
19905
  const js = `vendor/${kit.assistant}/insights.js`;
19328
19906
  const loads = (tag, attr3, target) => elements.some((el) => el.tag === tag && resolveFromPlatformRoot(page.path, el.attrs[attr3]) === target);
19329
- if (!loads("link", "href", css)) {
19907
+ const versionsLoaded = (tag, attr3, file) => elements.filter((el) => el.tag === tag).map((el) => ASSISTANT_MODULE.exec(resolveFromPlatformRoot(page.path, el.attrs[attr3]) ?? "")).filter((match) => match && match[2] === file).map((match) => match[1]);
19908
+ const cssVersions = versionsLoaded("link", "href", "insights.css");
19909
+ const jsVersions = versionsLoaded("script", "src", "insights.js");
19910
+ if (cssVersions.length === 0) {
19330
19911
  violations.push({ rule: "R3.1", where: page.path, message: `${page.path} loads no stylesheet ${climb}${css}` });
19331
19912
  }
19332
- if (!loads("script", "src", js)) {
19913
+ if (jsVersions.length === 0) {
19333
19914
  violations.push({ rule: "R3.1", where: page.path, message: `${page.path} loads no script ${climb}${js}; without it no widget has an Ask button` });
19334
19915
  }
19916
+ if (cssVersions.length > 0 && jsVersions.length > 0 && !jsVersions.some((version) => cssVersions.includes(version))) {
19917
+ violations.push({
19918
+ rule: "R3.1",
19919
+ where: page.path,
19920
+ message: `${page.path} loads the dr-insights script at ${jsVersions.join(", ")} and its stylesheet at ${cssVersions.join(", ")}; load both from one release (this kit writes ${kit.assistant})`
19921
+ });
19922
+ }
19335
19923
  const uiKit = `${kit.uiKit}/dr.css`;
19336
19924
  if (!loads("link", "href", uiKit)) {
19337
19925
  violations.push({ rule: "R2.7", where: page.path, message: `${page.path} loads no stylesheet ${climb}${uiKit}; the grid, the card, the tab bar and every token come from it` });
@@ -20212,7 +20800,7 @@ var CHART_FORMATS4 = Object.freeze(["currency", "number", "percent"]);
20212
20800
  var PLATFORM_FORMAT2 = Object.freeze({ "type": "string", "maxLength": 64, "pattern": "^(General|[^\\u0000-\\u001f]*[#0][^\\u0000-\\u001f]*)$" });
20213
20801
  var NEGATIVE_MODES3 = Object.freeze(["minus", "red_minus", "absolute", "red_absolute"]);
20214
20802
  var FORMAT_PATTERNS3 = Object.freeze({ "currency": "$#,###", "number": "#,###", "percent": '#,###.#"%"' });
20215
- 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" });
20216
20804
  var ROW_ACTION_WHEN3 = Object.freeze({ "dr-components": "1.2.0" });
20217
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" } });
20218
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" });
@@ -20966,7 +21554,7 @@ function readSpec2(handlers) {
20966
21554
  const where2 = handler.file || `handlers.json#${handler.name}`;
20967
21555
  const literals = new Literals2(handler.source, "py");
20968
21556
  const value = literals.constant("SPEC");
20969
- if (!isPlainObject9(value)) {
21557
+ if (!isPlainObject10(value)) {
20970
21558
  unresolved.push({ where: where2, key: "SPEC", text: literals.constantText("SPEC") ?? cut2("SPEC") });
20971
21559
  continue;
20972
21560
  }
@@ -20982,15 +21570,15 @@ function readSpec2(handlers) {
20982
21570
  var DEFAULTS_KEY2 = "_defaults";
20983
21571
  function effective2(table) {
20984
21572
  const defaults = table[DEFAULTS_KEY2];
20985
- if (!isPlainObject9(defaults)) return table;
21573
+ if (!isPlainObject10(defaults)) return table;
20986
21574
  const out = /* @__PURE__ */ Object.create(null);
20987
21575
  for (const [key, row] of Object.entries(table)) {
20988
21576
  if (key === DEFAULTS_KEY2) continue;
20989
- out[key] = key.startsWith("_") || !isPlainObject9(row) ? row : { ...defaults, ...row };
21577
+ out[key] = key.startsWith("_") || !isPlainObject10(row) ? row : { ...defaults, ...row };
20990
21578
  }
20991
21579
  return out;
20992
21580
  }
20993
- function isPlainObject9(value) {
21581
+ function isPlainObject10(value) {
20994
21582
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
20995
21583
  }
20996
21584
 
@@ -21082,7 +21670,7 @@ function readTransforms2(source) {
21082
21670
  if (!literals.has(TRANSFORMS_TABLE2)) return { found: false, texts: null, span: null, text: null };
21083
21671
  const value = literals.constant(TRANSFORMS_TABLE2);
21084
21672
  const span = literals.constantSpan(TRANSFORMS_TABLE2);
21085
- 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");
21086
21674
  if (!readable) return { found: true, texts: null, span, text: literals.constantText(TRANSFORMS_TABLE2) };
21087
21675
  const texts = {};
21088
21676
  for (const path3 of Object.keys(value).sort(natural4)) texts[path3] = value[path3];
@@ -21118,6 +21706,10 @@ var THEME_ATTR3 = "data-dr-theme";
21118
21706
  var DENSITY_ATTR2 = "data-dr-density";
21119
21707
  var THEME_FILE6 = "ui/theme.css";
21120
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";
21121
21713
  var EMPHASIS_ATTR2 = "data-dr-emphasis";
21122
21714
  var MUTATING_VERBS2 = /* @__PURE__ */ new Set(["insert", "update", "delete"]);
21123
21715
  var TABLE_ONLY_VERBS2 = /* @__PURE__ */ new Set([...MUTATING_VERBS2, "get"]);
@@ -21160,8 +21752,10 @@ function extract2(app, { describe: describe7 } = {}) {
21160
21752
  const elements = {};
21161
21753
  const pages = [];
21162
21754
  const pinsByPath = /* @__PURE__ */ new Map();
21755
+ let shell = null;
21163
21756
  for (const page of app.pages) {
21164
21757
  const built = readPage2(app, page, spec, elements, problems);
21758
+ if (shell === null && built.shell !== null && built.shell !== void 0) shell = built.shell;
21165
21759
  if (built.page.owned !== true && !page.text.includes(COMPONENT_ATTR2)) {
21166
21760
  readBundle2(app, page.path, built.page.scripts, elements, problems);
21167
21761
  }
@@ -21224,7 +21818,7 @@ function extract2(app, { describe: describe7 } = {}) {
21224
21818
  v: CONTRACT_V2,
21225
21819
  generated_by: GENERATED_BY2,
21226
21820
  source: describe7 ? "static+describe" : "static",
21227
- app: appSection2(app, manifest),
21821
+ app: { ...appSection2(app, manifest), ...shell === null ? {} : { shell } },
21228
21822
  pins: [...pinsByPath.values()].map((pin) => ({ ...pin, pages: [...new Set(pin.pages)].sort() })).sort((a, b) => a.order - b.order),
21229
21823
  pages,
21230
21824
  elements,
@@ -21242,7 +21836,7 @@ function appSection2(app, manifest) {
21242
21836
  for (const handler of app.handlers) {
21243
21837
  handlers[handler.name] = { http: handler.http || null, entrypoint: handler.file || null };
21244
21838
  }
21245
- const nav = isPlainObject9(manifest.nav) ? manifest.nav : {};
21839
+ const nav = isPlainObject10(manifest.nav) ? manifest.nav : {};
21246
21840
  return {
21247
21841
  id: manifest.id ?? null,
21248
21842
  name: manifest.name ?? null,
@@ -21258,7 +21852,7 @@ function appSection2(app, manifest) {
21258
21852
  api_version: manifest.apiVersion ?? manifest.api_version ?? null,
21259
21853
  routes: routesOf3(manifest),
21260
21854
  handlers,
21261
- capabilities: isPlainObject9(manifest.capabilities) ? manifest.capabilities : {},
21855
+ capabilities: isPlainObject10(manifest.capabilities) ? manifest.capabilities : {},
21262
21856
  data_model: app.dataModel ? normaliseModel2(app.dataModel) : null,
21263
21857
  theme: themeSection2(app)
21264
21858
  };
@@ -21406,14 +22000,21 @@ function readPage2(app, page, spec, elements, problems) {
21406
22000
  ...bands.map((entry, index) => ({ at: entry.node.index, section: banded[index] })),
21407
22001
  ...wrappers.map((node, index) => ({ at: node.index, section: wrapped[index] }))
21408
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
+ }
21409
22007
  const mount = readMount2(scripts, problems);
21410
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;
21411
22010
  return {
22011
+ shell: shellOf2(main2),
21412
22012
  page: {
21413
22013
  path: page.path,
21414
22014
  route: routeOf3(app.manifest, page.path),
21415
22015
  title: titleOf2(tree, page.text),
21416
22016
  header: headerOf2(tree, page.text),
22017
+ ...navOf2(tree, main2),
21417
22018
  sections,
21418
22019
  scripts: scripts.map((script) => script.path).filter((path3) => path3 !== page.path),
21419
22020
  theme: body?.attrs[THEME_ATTR3] ?? null,
@@ -21507,13 +22108,13 @@ function partCalls2(element) {
21507
22108
  const typed = element.props?.props ?? {};
21508
22109
  const out = [];
21509
22110
  for (const prop of Object.keys(PARTS2[element.type].events ?? {})) {
21510
- 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]);
21511
22112
  }
21512
22113
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
21513
- 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);
21514
22115
  }
21515
22116
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
21516
- 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);
21517
22118
  }
21518
22119
  return out;
21519
22120
  }
@@ -21673,20 +22274,43 @@ function headerOf2(tree, text3) {
21673
22274
  }
21674
22275
  const region = tree.nodes.find((node) => node.attrs[KIT_ATTR2] === "header");
21675
22276
  if (!region) return null;
21676
- const heading = firstTag2(tree, region, "h1");
22277
+ const heading = firstTag2(tree, region, "h1", true);
21677
22278
  if (!heading) return null;
21678
- const summary = firstTag2(tree, region, "p");
21679
- 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;
21680
22286
  }
21681
- function firstTag2(tree, node, tag) {
22287
+ function firstTag2(tree, node, tag, outer = false) {
21682
22288
  for (const child of node.children) {
21683
22289
  const candidate = tree.nodes[child];
21684
22290
  if (candidate.tag === tag) return candidate;
21685
- 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);
21686
22293
  if (found) return found;
21687
22294
  }
21688
22295
  return null;
21689
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
+ }
21690
22314
  function sectionOf2(node, text3, index) {
21691
22315
  const title = textOf2(text3, node);
21692
22316
  return { id: node.attrs.id ?? slug3(title) ?? `section_${index + 1}`, title };
@@ -21697,13 +22321,13 @@ function wrappedSectionOf2(tree, node, text3, index) {
21697
22321
  const out = { id: id === void 0 || id === "" ? `section_${index + 1}` : id, title: heading ? textOf2(text3, heading) : "" };
21698
22322
  const subtitle = node.children.map((child) => tree.nodes[child]).find((child) => child.tag === "p");
21699
22323
  if (subtitle) out.subtitle = textOf2(text3, subtitle);
21700
- for (const key of BAND_LOOK3) {
22324
+ for (const key of BAND_LOOK4) {
21701
22325
  const value = node.attrs[`data-dr-${key}`];
21702
22326
  if (value !== void 0 && value !== "") out[key] = value;
21703
22327
  }
21704
22328
  return out;
21705
22329
  }
21706
- var BAND_LOOK3 = ["layout", "surface", "aside"];
22330
+ var BAND_LOOK4 = ["layout", "surface", "aside"];
21707
22331
  function slug3(text3) {
21708
22332
  const clean = String(text3 ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
21709
22333
  return clean === "" ? null : clean;
@@ -21740,7 +22364,7 @@ function readMount2(scripts, problems) {
21740
22364
  const brace = script.text.indexOf("{", call);
21741
22365
  if (brace === -1) continue;
21742
22366
  const value = new Literals2(script.text, "js").valueAtChar(brace);
21743
- if (!isPlainObject9(value)) {
22367
+ if (!isPlainObject10(value)) {
21744
22368
  problems.push({
21745
22369
  code: "mount_unreadable",
21746
22370
  where: script.path,
@@ -21752,7 +22376,7 @@ function readMount2(scripts, problems) {
21752
22376
  return {
21753
22377
  handler: typeof value.handler === "string" ? value.handler : null,
21754
22378
  prefs: typeof value.prefs === "boolean" ? value.prefs : null,
21755
- defaults: isPlainObject9(value.defaults) ? value.defaults : null
22379
+ defaults: isPlainObject10(value.defaults) ? value.defaults : null
21756
22380
  };
21757
22381
  }
21758
22382
  return null;
@@ -21805,7 +22429,7 @@ function bind2(elements, spec, backend, model) {
21805
22429
  }
21806
22430
  }
21807
22431
  for (const [key, row] of Object.entries(spec.table)) {
21808
- if (!isWidgetKey2(key) || !isPlainObject9(row) || !isPlainObject9(row.params)) continue;
22432
+ if (!isWidgetKey2(key) || !isPlainObject10(row) || !isPlainObject10(row.params)) continue;
21809
22433
  for (const param of Object.keys(row.params)) {
21810
22434
  const entry = controls[param] ?? (controls[param] = { components: [], widgets: [], bound: false });
21811
22435
  if (!entry.widgets.includes(key)) entry.widgets.push(key);
@@ -21847,11 +22471,11 @@ function bindComponents2(elements, { backend = null, model = null } = {}) {
21847
22471
  if (!isComponentType2(element.type) || isDashboardCard2(element)) continue;
21848
22472
  const props = element.props ?? {};
21849
22473
  const entity = typeof props.entity === "string" && props.entity !== "" ? props.entity : null;
21850
- if (named5(props.action)) actionRow(props.action).components.push(uid);
21851
- 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);
21852
22476
  if (isPart3(element)) for (const name of partCalls2(element)) actionRow(name).components.push(uid);
21853
22477
  if (entity !== null && element.type !== "field") entityRow(entity).components.push(uid);
21854
- if (!named5(props.field)) continue;
22478
+ if (!named6(props.field)) continue;
21855
22479
  const column = entity === null ? null : (tables[entity]?.columns ?? {})[props.field] ?? null;
21856
22480
  const injected = entity !== null && (has(tables, entity) && INJECTED_COLUMNS3.includes(props.field) || platform(entity) && PLATFORM_TABLES3[entity].includes(props.field));
21857
22481
  const key = `${entity ?? ""}.${props.field}`;
@@ -21878,7 +22502,7 @@ function bindComponents2(elements, { backend = null, model = null } = {}) {
21878
22502
  unreached_mutations: Object.keys(facts).filter((name) => (facts[name].verbs ?? []).some((verb) => MUTATING_VERBS2.has(verb)) && !reached.has(name)).sort(natural5)
21879
22503
  };
21880
22504
  }
21881
- function named5(value) {
22505
+ function named6(value) {
21882
22506
  return typeof value === "string" && value !== "";
21883
22507
  }
21884
22508
  function isWidgetKey2(key) {
@@ -21916,7 +22540,7 @@ function componentProblems2(elements, bindings, { handlers = [], handlersSource
21916
22540
  for (const uid of Object.keys(elements).sort(natural5)) {
21917
22541
  const element = elements[uid];
21918
22542
  if (element.type !== "form") continue;
21919
- 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"]));
21920
22544
  if (missing.length === 0) continue;
21921
22545
  problems.push({
21922
22546
  code: "unbound_form",
@@ -21938,8 +22562,8 @@ function componentProblems2(elements, bindings, { handlers = [], handlersSource
21938
22562
  for (const uid of Object.keys(elements).sort(natural5)) {
21939
22563
  const element = elements[uid];
21940
22564
  const filter = element.props?.filter;
21941
- if (!isComponentType2(element.type) || !isPlainObject9(filter)) continue;
21942
- 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;
21943
22567
  if (entity === null || !Object.prototype.hasOwnProperty.call(tables, entity)) continue;
21944
22568
  const columns = tables[entity].columns ?? {};
21945
22569
  for (const column of Object.keys(filter).sort()) {
@@ -21987,7 +22611,7 @@ function dataSection2(app, manifest, spec, elements, describe7, problems) {
21987
22611
  ...Object.keys(sources).length > 0 ? { sources } : {}
21988
22612
  };
21989
22613
  if (describe7) {
21990
- const widgets2 = isPlainObject9(describe7.widgets) ? describe7.widgets : {};
22614
+ const widgets2 = isPlainObject10(describe7.widgets) ? describe7.widgets : {};
21991
22615
  const missing = Object.keys(spec.table).filter(isWidgetKey2).filter((key) => !(key in widgets2));
21992
22616
  if (spec.found && spec.unresolved.length === 0 && missing.length > 0) {
21993
22617
  problems.push({
@@ -22036,12 +22660,12 @@ function readSources2(handlers) {
22036
22660
  if (!SOURCES_TABLE2.test(source) || HANDLER_LITERAL2.test(source)) continue;
22037
22661
  const literals = new Literals2(source, "py");
22038
22662
  const table = literals.constant("SOURCES");
22039
- if (!isPlainObject9(table)) continue;
22663
+ if (!isPlainObject10(table)) continue;
22040
22664
  for (const [name, row] of Object.entries(table)) {
22041
- if (isPlainObject9(row) && sources[name] === void 0) sources[name] = row;
22665
+ if (isPlainObject10(row) && sources[name] === void 0) sources[name] = row;
22042
22666
  }
22043
22667
  const carried = literals.constant("TRANSFORMS");
22044
- if (!isPlainObject9(carried)) continue;
22668
+ if (!isPlainObject10(carried)) continue;
22045
22669
  for (const [name, text3] of Object.entries(carried)) {
22046
22670
  const path3 = table[name]?.transform;
22047
22671
  if (typeof path3 === "string" && typeof text3 === "string" && !texts.has(path3)) texts.set(path3, text3);
@@ -22076,7 +22700,7 @@ function specTransforms2(handlers, spec, problems) {
22076
22700
  }
22077
22701
  const unreadable = problems.some((problem5) => problem5.code === "transform_unreadable");
22078
22702
  for (const key of Object.keys(spec.table).filter(isWidgetKey2).sort(natural5)) {
22079
- 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;
22080
22704
  if (typeof path3 !== "string" || unreadable || carried !== null && carried.has(path3)) continue;
22081
22705
  problems.push({
22082
22706
  code: "transform_not_carried",
@@ -22100,7 +22724,7 @@ function readApp2(handlers, elements, manifest = null) {
22100
22724
  let row = null;
22101
22725
  if (HANDLER_LITERAL2.test(source)) {
22102
22726
  const value = new Literals2(source, "py").constant("HANDLER");
22103
- if (isPlainObject9(value)) row = value;
22727
+ if (isPlainObject10(value)) row = value;
22104
22728
  }
22105
22729
  declared[handler.name] = { role: "custom", ...row ?? {} };
22106
22730
  if (declared[handler.name].requiresRole === void 0) {
@@ -22128,9 +22752,9 @@ function screensOf2(elements, order2 = null) {
22128
22752
  screens.push({
22129
22753
  page: element.page,
22130
22754
  component: element.type,
22131
- id: named5(props.id) ? props.id : element.id ?? null,
22132
- entity: named5(props.entity) ? props.entity : entityAbove2(elements, element),
22133
- 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
22134
22758
  });
22135
22759
  }
22136
22760
  return screens;
@@ -22142,7 +22766,7 @@ function entityAbove2(elements, element) {
22142
22766
  seen.add(at);
22143
22767
  const above = elements[at];
22144
22768
  if (above === void 0) return null;
22145
- if (named5(above.props?.entity)) return above.props.entity;
22769
+ if (named6(above.props?.entity)) return above.props.entity;
22146
22770
  at = above.parent ?? null;
22147
22771
  }
22148
22772
  return null;
@@ -22171,7 +22795,7 @@ function backendSection2(app, manifest, pages, elements) {
22171
22795
  if (!isComponentType2(element.type) || isDashboardCard2(element)) continue;
22172
22796
  for (const key of ["action", "source"]) {
22173
22797
  const name = element.props?.[key];
22174
- if (!named5(name)) continue;
22798
+ if (!named6(name)) continue;
22175
22799
  record2(name, "attribute");
22176
22800
  attributed.add(name);
22177
22801
  }
@@ -22200,14 +22824,14 @@ function backendSection2(app, manifest, pages, elements) {
22200
22824
  data_model: model,
22201
22825
  handlers,
22202
22826
  roles,
22203
- capabilities: isPlainObject9(manifest.capabilities) ? manifest.capabilities : {},
22827
+ capabilities: isPlainObject10(manifest.capabilities) ? manifest.capabilities : {},
22204
22828
  ui: {
22205
22829
  entry,
22206
22830
  handlers_called: [...called].sort(),
22207
22831
  handlers_called_via: [...via.values()].sort((a, b) => natural5(a.name, b.name) || natural5(a.via, b.via)),
22208
22832
  scripts_loaded: [...new Set(pages.flatMap((page) => page.scripts))].sort()
22209
22833
  },
22210
- 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 : {},
22211
22835
  invariants: invariants2(model, roles, handlers)
22212
22836
  };
22213
22837
  }
@@ -22217,7 +22841,7 @@ function callsOnTable2(source, verb) {
22217
22841
  return holders.some((holder) => new RegExp(`(?:^|[^\\w.])(?:${holder})\\s*\\.\\s*${verb}\\s*\\(`, "m").test(source));
22218
22842
  }
22219
22843
  function declaredHandler2(manifest, name) {
22220
- return isPlainObject9(manifest?.handlers) ? manifest.handlers[name] ?? null : null;
22844
+ return isPlainObject10(manifest?.handlers) ? manifest.handlers[name] ?? null : null;
22221
22845
  }
22222
22846
  function signatureOk2(source) {
22223
22847
  const code = source.split("\n").map((line2) => /^\s*#/.test(line2) ? "" : line2).join("\n");
@@ -22244,7 +22868,7 @@ function rolesOf3(manifest) {
22244
22868
  // Declaration order IS the ladder: `authz.py` reads the manifest's list
22245
22869
  // top down, so the first role is the most junior.
22246
22870
  seniority: index,
22247
- tables: isPlainObject9(role?.tables) ? role.tables : {},
22871
+ tables: isPlainObject10(role?.tables) ? role.tables : {},
22248
22872
  // Stated only when the manifest states it, like the platform's own
22249
22873
  // derivation (`app_contract_store._roles_section`): the norm is a role
22250
22874
  // that sees only its own screens, and `[]` on every role would say
@@ -22256,16 +22880,16 @@ function rolesOf3(manifest) {
22256
22880
  }
22257
22881
  function pageRolesOf2(manifest, path3) {
22258
22882
  const pages = Array.isArray(manifest?.pages) ? manifest.pages : [];
22259
- const entry = pages.find((one) => isPlainObject9(one) && one.path === path3);
22883
+ const entry = pages.find((one) => isPlainObject10(one) && one.path === path3);
22260
22884
  const roles = Array.isArray(entry?.roles) ? entry.roles.filter((role) => typeof role === "string" && role !== "") : [];
22261
22885
  return roles.length > 0 ? { roles } : {};
22262
22886
  }
22263
22887
  function normaliseModel2(document) {
22264
22888
  const tables = {};
22265
- const raw = isPlainObject9(document?.tables) ? document.tables : {};
22889
+ const raw = isPlainObject10(document?.tables) ? document.tables : {};
22266
22890
  for (const [name, table] of Object.entries(raw)) {
22267
22891
  const columns = {};
22268
- 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 : {})) {
22269
22893
  const pk = Boolean(spec?.pk);
22270
22894
  columns[column] = {
22271
22895
  type: spec?.type ?? null,
@@ -22283,7 +22907,7 @@ function normaliseModel2(document) {
22283
22907
  // `computed` (a child total the platform selects with the row) and
22284
22908
  // `matches` (the column it should agree with), by `display`'s rule:
22285
22909
  // only where stated, so a model without them normalises to its bytes.
22286
- ...isPlainObject9(spec?.computed) ? { computed: spec.computed } : {},
22910
+ ...isPlainObject10(spec?.computed) ? { computed: spec.computed } : {},
22287
22911
  ...typeof spec?.matches === "string" && spec.matches !== "" ? { matches: spec.matches } : {}
22288
22912
  };
22289
22913
  }
@@ -22301,7 +22925,7 @@ function normaliseModel2(document) {
22301
22925
  // row reads as before it is first saved. By `audit`'s rule: only where
22302
22926
  // stated, so a model with neither keeps normalising to its bytes.
22303
22927
  ...table?.singleton === true ? { singleton: true } : {},
22304
- ...table?.singleton === true && isPlainObject9(table?.defaults) ? { defaults: table.defaults } : {},
22928
+ ...table?.singleton === true && isPlainObject10(table?.defaults) ? { defaults: table.defaults } : {},
22305
22929
  columns,
22306
22930
  indexes: Array.isArray(table?.indexes) ? table.indexes.map((index) => Array.isArray(index) ? index.map(String) : [String(index)]) : [],
22307
22931
  injected: [...INJECTED_COLUMNS3]
@@ -22634,10 +23258,10 @@ function isObject5(value) {
22634
23258
  var DECLARES_KEYS2 = Object.freeze(["reads", "calls", "produces", "shows"]);
22635
23259
  var SHOWS_KEYS2 = Object.freeze(["field", "format", "scale"]);
22636
23260
  var SHOWS_FORMATS2 = Object.freeze(["currency", "number", "percent"]);
22637
- var isPlainObject10 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
22638
- var named6 = (value) => typeof value === "string" && value !== "";
22639
- var listOfNames2 = (value) => Array.isArray(value) && value.every(named6);
22640
- 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;
22641
23265
  function pyCode2(source) {
22642
23266
  let out = "";
22643
23267
  let i = 0;
@@ -22706,7 +23330,7 @@ function handlerPayloadReads2(row, source) {
22706
23330
  return { generated: false, required: pyPayloadReads2(source).required };
22707
23331
  }
22708
23332
  if (role === "save" || role === "submit") {
22709
- 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) };
22710
23334
  }
22711
23335
  return { generated: true, required: [] };
22712
23336
  }
@@ -22876,32 +23500,32 @@ function childIndex2(elements) {
22876
23500
  };
22877
23501
  for (const [uid, element] of Object.entries(elements)) {
22878
23502
  for (const child of Array.isArray(element?.children) ? element.children : []) if (child in elements) add(uid, child);
22879
- 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);
22880
23504
  }
22881
23505
  return index;
22882
23506
  }
22883
23507
  var ROW_HOLDERS2 = /* @__PURE__ */ new Set(["list", "queue", "detail"]);
22884
23508
  var INPUT_PARTS2 = /* @__PURE__ */ new Set(["number_input", "select", "date_picker", "toggle", "slider", "checkbox", "radio_group", "file_upload"]);
22885
23509
  function fieldOf2(element) {
22886
- if (element?.type === "field" && named6(element.props?.field)) return element.props.field;
22887
- 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;
22888
23512
  return null;
22889
23513
  }
22890
23514
  function mentions2(text3, name) {
22891
23515
  return new RegExp(`(^|[^A-Za-z0-9_])${reEscape2(name)}([^A-Za-z0-9_]|$)`).test(text3);
22892
23516
  }
22893
23517
  function payloadProducers2(contract, { moduleSource = () => void 0, pageSource = () => void 0 } = {}) {
22894
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
22895
- 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 : {};
22896
23520
  const children = childIndex2(elements);
22897
23521
  const out = /* @__PURE__ */ new Map();
22898
23522
  const add = (handler, producer) => {
22899
- if (!named6(handler)) return;
23523
+ if (!named7(handler)) return;
22900
23524
  if (!out.has(handler)) out.set(handler, []);
22901
23525
  out.get(handler).push(producer);
22902
23526
  };
22903
23527
  const insideRow = (uid) => {
22904
- 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) {
22905
23529
  if (ROW_HOLDERS2.has(elements[at]?.type)) return true;
22906
23530
  }
22907
23531
  return false;
@@ -22922,16 +23546,16 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
22922
23546
  };
22923
23547
  for (const uid of Object.keys(elements).sort()) {
22924
23548
  const element = elements[uid];
22925
- const props = isPlainObject10(element?.props) ? element.props : {};
23549
+ const props = isPlainObject11(element?.props) ? element.props : {};
22926
23550
  const page = element?.page ?? null;
22927
- const typed = isPlainObject10(props.props) ? props.props : {};
22928
- 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 });
22929
23553
  switch (element?.type) {
22930
23554
  case "form": {
22931
23555
  const keys2 = fieldsUnder(uid);
22932
23556
  if (insideRow(uid) && !keys2.includes("id")) keys2.push("id");
22933
23557
  const by = handlers[props.action]?.by;
22934
- if (named6(by) && !keys2.includes(by)) keys2.push(by);
23558
+ if (named7(by) && !keys2.includes(by)) keys2.push(by);
22935
23559
  add(props.action, { page, uid, what: "form", keys: keys2 });
22936
23560
  break;
22937
23561
  }
@@ -22946,41 +23570,41 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
22946
23570
  // stepper's current step `{step}`; a file_upload's on_upload its field.
22947
23571
  case "button":
22948
23572
  add(typed.on_click, { page, uid, what: "button", keys: typed.reason === "required" || typed.reason === "optional" ? ["reason"] : [] });
22949
- 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: [] });
22950
23574
  break;
22951
23575
  case "empty_state":
22952
23576
  add(typed.on_action, { page, uid, what: "empty_state", keys: [] });
22953
23577
  break;
22954
23578
  case "stepper":
22955
- 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"] });
22956
23580
  break;
22957
23581
  case "file_upload":
22958
- 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] : [] });
22959
23583
  break;
22960
23584
  case "data_grid":
22961
23585
  add(typed.on_row, { page, uid, what: "data_grid", keys: ["row"] });
22962
23586
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
22963
- 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"] });
22964
23588
  }
22965
23589
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
22966
- 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"] });
22967
23591
  }
22968
23592
  break;
22969
23593
  case "action": {
22970
23594
  const keys2 = [];
22971
- if (insideRow(uid)) keys2.push(named6(props.param) ? props.param : "id");
22972
- 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);
22973
23597
  if (handlers[props.action]?.role === "decide") keys2.push("decision", "note");
22974
23598
  add(props.action, { page, uid, what: "action", keys: keys2 });
22975
23599
  break;
22976
23600
  }
22977
23601
  case "custom": {
22978
- const declares = isPlainObject10(props.declares) ? props.declares : {};
22979
- const produces = isPlainObject10(declares.produces) ? declares.produces : {};
23602
+ const declares = isPlainObject11(props.declares) ? props.declares : {};
23603
+ const produces = isPlainObject11(declares.produces) ? declares.produces : {};
22980
23604
  for (const [handler, keys2] of Object.entries(produces)) {
22981
23605
  add(handler, { page, uid, what: "custom", keys: listOfNames2(keys2) ? [...keys2] : null });
22982
23606
  }
22983
- const source = named6(props.module) ? moduleSource(props.module) : void 0;
23607
+ const source = named7(props.module) ? moduleSource(props.module) : void 0;
22984
23608
  if (typeof source !== "string") {
22985
23609
  add("*", { page, uid, what: "custom", keys: null });
22986
23610
  break;
@@ -23006,10 +23630,10 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
23006
23630
  }
23007
23631
  const modules = /* @__PURE__ */ new Set();
23008
23632
  for (const element of Object.values(elements)) {
23009
- 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}`);
23010
23634
  }
23011
23635
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23012
- if (!named6(page?.path)) continue;
23636
+ if (!named7(page?.path)) continue;
23013
23637
  const text3 = pageSource(page.path);
23014
23638
  const unknown = { page: page.path, uid: null, what: "script", keys: null };
23015
23639
  if (typeof text3 !== "string") {
@@ -23054,7 +23678,7 @@ function either4(items) {
23054
23678
  return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
23055
23679
  }
23056
23680
  function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, moduleSource, pageSource } = {}) {
23057
- const handlers = isPlainObject10(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23681
+ const handlers = isPlainObject11(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
23058
23682
  const producers = payloadProducers2(contract, { moduleSource, pageSource });
23059
23683
  const anywhere = producers.get("*") ?? [];
23060
23684
  const out = [];
@@ -23065,7 +23689,7 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
23065
23689
  if (own.length === 0 || anywhere.length > 0 || own.some((one) => one.keys === null)) continue;
23066
23690
  const produced = [];
23067
23691
  for (const one of own) for (const key of one.keys) if (!produced.includes(key)) produced.push(key);
23068
- const pages = [...new Set(own.map((one) => one.page).filter(named6))];
23692
+ const pages = [...new Set(own.map((one) => one.page).filter(named7))];
23069
23693
  for (const key of reads.required) {
23070
23694
  if (produced.includes(key)) continue;
23071
23695
  const there = produced.length === 0 ? "the controls there produce no payload keys" : `the controls there produce ${produced.join(", ")}`;
@@ -23077,12 +23701,12 @@ function unproducedPayloadKeys2(contract, { handlerSource = () => void 0, module
23077
23701
  return out;
23078
23702
  }
23079
23703
  function undeclaredCustomCalls2(contract, { moduleSource = () => void 0 } = {}) {
23080
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23704
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23081
23705
  const out = [];
23082
23706
  for (const uid of Object.keys(elements).sort()) {
23083
23707
  const element = elements[uid];
23084
- const props = isPlainObject10(element?.props) ? element.props : {};
23085
- 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;
23086
23710
  const source = moduleSource(props.module);
23087
23711
  if (typeof source !== "string") continue;
23088
23712
  const calls = listOfNames2(props.declares.calls) ? props.declares.calls : [];
@@ -23110,32 +23734,32 @@ function undeclaredCallMessage2(id, module, handler, calls, how) {
23110
23734
  return { problem: problem5, fix, message: `${problem5} ${fix}` };
23111
23735
  }
23112
23736
  function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
23113
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23114
- 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 : {};
23115
23739
  const parents = /* @__PURE__ */ new Map();
23116
23740
  for (const [parent, kids] of childIndex2(elements)) for (const kid of kids) if (!parents.has(kid)) parents.set(kid, parent);
23117
23741
  const out = [];
23118
23742
  for (const uid of Object.keys(elements).sort()) {
23119
23743
  const element = elements[uid];
23120
- const typed = isPlainObject10(element?.props?.props) ? element.props.props : {};
23121
- 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;
23122
23746
  const field = typed.field;
23123
23747
  const receivers = [];
23124
- if (named6(typed.on_upload)) receivers.push([typed.on_upload, "its on_upload"]);
23125
- 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)) {
23126
23750
  const holder = elements[at];
23127
- if (holder?.type === "dialog" && named6(holder.props?.props?.on_confirm)) {
23751
+ if (holder?.type === "dialog" && named7(holder.props?.props?.on_confirm)) {
23128
23752
  receivers.push([holder.props.props.on_confirm, `the dialog "${idOf3(holder, at)}" it sits in`]);
23129
23753
  break;
23130
23754
  }
23131
- if (holder?.type === "form" && named6(holder.props?.action)) {
23755
+ if (holder?.type === "form" && named7(holder.props?.action)) {
23132
23756
  receivers.push([holder.props.action, `the form "${idOf3(holder, at)}" it sits in`]);
23133
23757
  break;
23134
23758
  }
23135
23759
  }
23136
23760
  for (const [handler, via] of receivers) {
23137
23761
  const row = handlers[handler];
23138
- if (!isPlainObject10(row)) continue;
23762
+ if (!isPlainObject11(row)) continue;
23139
23763
  const role = row.role ?? "custom";
23140
23764
  let read;
23141
23765
  if (role === "custom") {
@@ -23158,16 +23782,16 @@ function unreadUploadKeys2(contract, { handlerSource = () => void 0 } = {}) {
23158
23782
  return out;
23159
23783
  }
23160
23784
  function stepsWithoutAction2(contract) {
23161
- const elements = isPlainObject10(contract?.elements) ? contract.elements : {};
23785
+ const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
23162
23786
  const out = [];
23163
23787
  for (const uid of Object.keys(elements).sort()) {
23164
23788
  const element = elements[uid];
23165
23789
  const steps = element?.type === "stepper" && Array.isArray(element.props?.props?.steps) ? element.props.props.steps : [];
23166
23790
  steps.forEach((step, index) => {
23167
- if (index === steps.length - 1 || !isPlainObject10(step) || named6(step.action)) return;
23791
+ if (index === steps.length - 1 || !isPlainObject11(step) || named7(step.action)) return;
23168
23792
  const id = idOf3(element, uid);
23169
- const name = named6(step.id) ? step.id : String(index);
23170
- 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";
23171
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.`;
23172
23796
  const fix = `Give it the handler that moves it to "${next}" (action, action_label), or make it the last step.`;
23173
23797
  out.push({ uid, id, page: element.page ?? null, step: name, index, problem: problem5, fix, message: `${problem5} ${fix}` });
@@ -23431,6 +24055,10 @@ function checkComponents(app, contract, contractError) {
23431
24055
  }
23432
24056
  };
23433
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
+ }
23434
24062
  for (const uid of uids) {
23435
24063
  const element = elements[uid];
23436
24064
  if (element.type !== "custom" || typeof element.props?.module !== "string") continue;
@@ -23572,45 +24200,45 @@ function isReferenced4(name, pageText) {
23572
24200
  }
23573
24201
 
23574
24202
  // ../flows/appspace-builder/kit/contract/levers.mjs
23575
- var isPlainObject11 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23576
- 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 !== "";
23577
24205
  function both2(items) {
23578
24206
  if (items.length <= 1) return items.join("");
23579
24207
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
23580
24208
  }
23581
24209
  function paramPages(contract) {
23582
24210
  return (Array.isArray(contract?.pages) ? contract.pages : []).filter(
23583
- (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
23584
24212
  );
23585
24213
  }
23586
24214
  function namesOf(page) {
23587
24215
  const out = [];
23588
- 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);
23589
24217
  return out;
23590
24218
  }
23591
24219
  function elementsOn(contract, path3) {
23592
- const elements = isPlainObject11(contract?.elements) ? contract.elements : {};
24220
+ const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23593
24221
  return Object.keys(elements).sort().filter((uid) => elements[uid]?.page === path3).map((uid) => [uid, elements[uid]]);
23594
24222
  }
23595
24223
  function paramModeOf(element) {
23596
- const props = isPlainObject11(element?.props) ? element.props : {};
23597
- 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;
23598
24226
  if (element?.type === "field" || element?.type === "action") return null;
23599
- if (named7(props.param) && element?.type !== "custom") return props.param;
23600
- const typed = isPlainObject11(props.props) ? props.props : {};
23601
- 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;
23602
24230
  }
23603
24231
  function paramsWrittenBy(element) {
23604
24232
  const out = [];
23605
24233
  const one = paramModeOf(element);
23606
24234
  if (one !== null) out.push(one);
23607
24235
  if (element?.type === "custom") return out;
23608
- const typed = isPlainObject11(element?.props?.props) ? element.props.props : {};
23609
- 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);
23610
24238
  for (const filter of Array.isArray(typed.filters) ? typed.filters : []) {
23611
- if (!isPlainObject11(filter)) continue;
23612
- if (named7(filter.param)) out.push(filter.param);
23613
- 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);
23614
24242
  }
23615
24243
  return out;
23616
24244
  }
@@ -23622,10 +24250,10 @@ function paramWriters(contract, { moduleSource = () => void 0 } = {}) {
23622
24250
  for (const [, element] of elementsOn(contract, page.path)) {
23623
24251
  for (const name of paramsWrittenBy(element)) written.add(name);
23624
24252
  if (element?.type !== "custom") continue;
23625
- const props = isPlainObject11(element.props) ? element.props : {};
23626
- const produces = isPlainObject11(props.declares?.produces) ? props.declares.produces : {};
23627
- for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named7(key)) written.add(key);
23628
- 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;
23629
24257
  const source = moduleSource(props.module);
23630
24258
  if (typeof source !== "string") {
23631
24259
  unknown = true;
@@ -23636,6 +24264,13 @@ function paramWriters(contract, { moduleSource = () => void 0 } = {}) {
23636
24264
  else written.add(call.name);
23637
24265
  }
23638
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
+ }
23639
24274
  out.set(page.path, { written, unknown });
23640
24275
  }
23641
24276
  return out;
@@ -23649,7 +24284,7 @@ function unboundControls(contract, files = {}) {
23649
24284
  for (const name of namesOf(page)) {
23650
24285
  if (written.has(name)) continue;
23651
24286
  const param = page.params.find((one) => one?.name === name) ?? {};
23652
- const kind = named7(param.kind) ? param.kind : null;
24287
+ const kind = named8(param.kind) ? param.kind : null;
23653
24288
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
23654
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}.`;
23655
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.`;
@@ -23664,16 +24299,16 @@ function honoursOf(sources, name) {
23664
24299
  const queue = [name];
23665
24300
  while (queue.length > 0) {
23666
24301
  const one = queue.shift();
23667
- if (seen.has(one) || !isPlainObject11(sources[one])) continue;
24302
+ if (seen.has(one) || !isPlainObject12(sources[one])) continue;
23668
24303
  seen.add(one);
23669
- 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);
23670
24305
  if (Array.isArray(sources[one].from)) queue.push(...sources[one].from);
23671
24306
  }
23672
24307
  return out;
23673
24308
  }
23674
24309
  function unreadParams(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
23675
- const sources = isPlainObject11(contract?.data?.sources) ? contract.data.sources : {};
23676
- 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 : {};
23677
24312
  const out = [];
23678
24313
  for (const page of paramPages(contract)) {
23679
24314
  const read = /* @__PURE__ */ new Set();
@@ -23682,15 +24317,15 @@ function unreadParams(contract, { moduleSource = () => void 0, handlerSource = (
23682
24317
  const custom = [];
23683
24318
  let unknown = false;
23684
24319
  for (const [, element] of elementsOn(contract, page.path)) {
23685
- const props = isPlainObject11(element?.props) ? element.props : {};
23686
- 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);
23687
24322
  for (const one of reads) {
23688
24323
  if (Object.prototype.hasOwnProperty.call(sources, one)) {
23689
24324
  if (!named_.includes(one)) named_.push(one);
23690
24325
  for (const honored of honoursOf(sources, one)) read.add(honored);
23691
24326
  } else if (handlers[one]?.role === "custom" && !custom.includes(one)) custom.push(one);
23692
24327
  }
23693
- if (element?.type === "custom" && named7(props.module)) {
24328
+ if (element?.type === "custom" && named8(props.module)) {
23694
24329
  const text3 = moduleSource(props.module);
23695
24330
  if (typeof text3 === "string") modules.push(text3);
23696
24331
  else unknown = true;
@@ -23732,28 +24367,28 @@ function checkLevers(app, contract, contractError) {
23732
24367
  }
23733
24368
 
23734
24369
  // ../flows/appspace-builder/kit/contract/look.mjs
23735
- var isPlainObject12 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
23736
- var named8 = (value) => typeof value === "string" && value !== "";
23737
- 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) {
23738
24373
  const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
23739
24374
  return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
23740
24375
  }
23741
- var WORDING = {
23742
- custom: { noun: "card", fallback: (p) => [humanise2(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
23743
- chart: { noun: "card", fallback: (p) => [humanise2(String(p.y ?? "")), "its y column"] },
23744
- widget: { noun: "card", fallback: (p) => [humanise2(named8(p.source) ? p.source : "widget"), "its source"] },
23745
- stat: { noun: "tile", fallback: (p) => [humanise2(String(p.measure ?? "")), "its measure"] },
23746
- list: { noun: "card", fallback: (p) => [humanise2(named8(p.entity) ? p.entity : "list"), "its entity"] },
23747
- 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"] },
23748
24383
  form: { noun: "card", fallback: () => ["New", "the runtime\u2019s default"] },
23749
24384
  dialog: { noun: "dialog" }
23750
24385
  };
23751
- var TITLED = Object.fromEntries(
23752
- 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] }])
23753
24388
  );
23754
- var LAYOUT = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
23755
- function rootsOf2(contract, page) {
23756
- 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 : {};
23757
24392
  if (Array.isArray(page?.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid] !== void 0);
23758
24393
  const contained = /* @__PURE__ */ new Set();
23759
24394
  for (const element of Object.values(elements)) for (const child of element?.children ?? []) contained.add(child);
@@ -23762,8 +24397,8 @@ function rootsOf2(contract, page) {
23762
24397
  return element?.page === page?.path && (element.parent === null || element.parent === void 0) && !contained.has(uid);
23763
24398
  });
23764
24399
  }
23765
- function untitledCards(contract) {
23766
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
24400
+ function untitledCards2(contract) {
24401
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
23767
24402
  const out = [];
23768
24403
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23769
24404
  if (page?.owned === true) continue;
@@ -23774,7 +24409,7 @@ function untitledCards(contract) {
23774
24409
  const element = elements[uid];
23775
24410
  if (element === void 0 || seen.has(uid)) continue;
23776
24411
  seen.add(uid);
23777
- if (LAYOUT.has(element.type ?? "")) {
24412
+ if (LAYOUT2.has(element.type ?? "")) {
23778
24413
  const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
23779
24414
  walk4(children);
23780
24415
  } else {
@@ -23782,19 +24417,19 @@ function untitledCards(contract) {
23782
24417
  }
23783
24418
  }
23784
24419
  };
23785
- walk4(rootsOf2(contract, page));
24420
+ walk4(rootsOf3(contract, page));
23786
24421
  for (const uid of drawn2) {
23787
24422
  const element = elements[uid];
23788
24423
  const type = element.type ?? "";
23789
- const titled = TITLED[type];
24424
+ const titled = TITLED2[type];
23790
24425
  if (titled === void 0) continue;
23791
- const props = isPlainObject12(element.props) ? element.props : {};
23792
- const typed = isPlainObject12(props.props) ? props.props : {};
23793
- const card = isPlainObject12(element.card) ? element.card : isPlainObject12(props.card) ? props.card : {};
23794
- if (named8(props.label) || named8(card.title)) continue;
23795
- if (type === "dialog" ? named8(typed.title) : named8(typed.label)) continue;
23796
- const id = named8(element.id) ? element.id : named8(props.id) ? props.id : null;
23797
- 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);
23798
24433
  const what = id !== null ? `${type} "${id}"` : `the ${type} ${uid}`;
23799
24434
  const shown2 = title === "" ? "has no title at all" : `is titled "${title}" after ${from}`;
23800
24435
  out.push({ page: page.path, uid, type, id, noun: titled.noun, title, from, message: `${what} has no label, so its ${titled.noun} ${shown2}` });
@@ -23802,49 +24437,49 @@ function untitledCards(contract) {
23802
24437
  }
23803
24438
  return out;
23804
24439
  }
23805
- var BAND_LOOK4 = ["subtitle", "layout", "surface", "aside"];
23806
- function lookPages(contract) {
23807
- 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 : {};
23808
24443
  const out = /* @__PURE__ */ new Set();
23809
24444
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23810
- if (page?.owned === true || !named8(page?.path)) continue;
23811
- const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK4.some((key) => named8(section?.[key])));
23812
- 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)));
23813
24448
  if (bands || cards) out.add(page.path);
23814
24449
  }
23815
24450
  return out;
23816
24451
  }
23817
- var SPAN_LAYOUTS = /* @__PURE__ */ new Set([void 0, "grid"]);
23818
- var CONTROL_TYPES = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS2]);
23819
- function bandsOf2(contract, page) {
23820
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23821
- const roots = rootsOf2(contract, page);
23822
- 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) }));
23823
24458
  }
23824
- function nameOf3(uid, element) {
23825
- 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;
23826
24461
  return id === null ? `the ${element?.type ?? "element"} ${uid}` : `${element.type} "${id}"`;
23827
24462
  }
23828
- function isControl(element, moduleSource) {
23829
- const props = isPlainObject12(element?.props) ? element.props : {};
23830
- if (CONTROL_TYPES.has(element?.type)) return true;
23831
- if (element?.type !== "custom" && element?.type !== "action" && named8(props.param)) return true;
23832
- if (isPlainObject12(props.props) && named8(props.props.param)) return true;
23833
- 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)) {
23834
24469
  const source = moduleSource(props.module);
23835
24470
  return typeof source === "string" && scanUiSetParams2(source).length > 0;
23836
24471
  }
23837
24472
  return false;
23838
24473
  }
23839
- function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23840
- const elements = isPlainObject12(contract?.elements) ? contract.elements : {};
23841
- const looked = lookPages(contract);
24474
+ function layoutProblems2(contract, { moduleSource = () => void 0 } = {}) {
24475
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
24476
+ const looked = lookPages2(contract);
23842
24477
  const out = [];
23843
24478
  for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
23844
24479
  if (!looked.has(page?.path)) continue;
23845
- for (const { section, roots } of bandsOf2(contract, page)) {
24480
+ for (const { section, roots } of bandsOf3(contract, page)) {
23846
24481
  const band = `band "${section.id}" on ${page.path}`;
23847
- const layout = named8(section.layout) ? section.layout : void 0;
24482
+ const layout = named9(section.layout) ? section.layout : void 0;
23848
24483
  if (layout === "kpi_strip") {
23849
24484
  for (const uid of roots) {
23850
24485
  const element = elements[uid];
@@ -23854,30 +24489,30 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23854
24489
  page: page.path,
23855
24490
  section: section.id,
23856
24491
  uid,
23857
- 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)`
23858
24493
  });
23859
24494
  }
23860
24495
  }
23861
24496
  if (layout === "sidebar" && roots.length > 0) {
23862
24497
  const uid = section.aside === "start" ? roots[0] : roots[roots.length - 1];
23863
- if (!isControl(elements[uid], moduleSource)) {
24498
+ if (!isControl2(elements[uid], moduleSource)) {
23864
24499
  out.push({
23865
24500
  rule: "L2",
23866
24501
  page: page.path,
23867
24502
  section: section.id,
23868
24503
  uid,
23869
- 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`
23870
24505
  });
23871
24506
  }
23872
24507
  }
23873
- if (SPAN_LAYOUTS.has(layout)) {
24508
+ if (SPAN_LAYOUTS2.has(layout)) {
23874
24509
  let row = [];
23875
24510
  let used = 0;
23876
24511
  for (const uid of roots) {
23877
24512
  const raw = elements[uid]?.grid?.span;
23878
24513
  const span = raw === null || raw === void 0 ? 12 : Number(raw);
23879
24514
  if (!Number.isInteger(span) || span < 1 || span > 12) {
23880
- 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` });
23881
24516
  continue;
23882
24517
  }
23883
24518
  if (used + span > 12) {
@@ -23888,7 +24523,7 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23888
24523
  page: page.path,
23889
24524
  section: section.id,
23890
24525
  uid,
23891
- 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`
23892
24527
  });
23893
24528
  }
23894
24529
  row = [];
@@ -23906,6 +24541,137 @@ function layoutProblems(contract, { moduleSource = () => void 0 } = {}) {
23906
24541
  }
23907
24542
  return out;
23908
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
+ }
23909
24675
 
23910
24676
  // ../flows/appspace-builder/kit/contract/platformPalette.mjs
23911
24677
  var PLATFORM_CHART2 = Object.freeze([
@@ -24127,7 +24893,7 @@ function rgbText2(parts) {
24127
24893
 
24128
24894
  // ../flows/appspace-builder/kit/checks/rules/look.mjs
24129
24895
  var THEME_FILE8 = "ui/theme.css";
24130
- var RULES3 = ["L1", "L2", "L3", "L4"];
24896
+ var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7"];
24131
24897
  function checkLook(app, contract, contractError) {
24132
24898
  const view = {
24133
24899
  ...app,
@@ -24139,14 +24905,23 @@ function checkLook(app, contract, contractError) {
24139
24905
  return { violations, skipped: RULES3.map((rule) => ({ rule, reason })) };
24140
24906
  }
24141
24907
  const moduleSource = (module) => app.ui.find((file) => file.path === `ui/${module}`)?.text;
24142
- for (const found of layoutProblems(contract, { moduleSource })) {
24908
+ for (const found of layoutProblems2(contract, { moduleSource })) {
24143
24909
  violations.push({ rule: found.rule, where: found.uid ?? found.page, message: found.message });
24144
24910
  }
24145
- const looked = lookPages(contract);
24146
- for (const found of untitledCards(contract)) {
24911
+ const looked = lookPages2(contract);
24912
+ for (const found of untitledCards2(contract)) {
24147
24913
  if (!looked.has(found.page)) continue;
24148
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}` });
24149
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
+ }
24150
24925
  return { violations, skipped: [] };
24151
24926
  }
24152
24927
 
@@ -24307,7 +25082,7 @@ function rolesOf5(manifest) {
24307
25082
  // ../flows/appspace-builder/kit/checks/rules/ux.mjs
24308
25083
  var RULES4 = ["A3.23", "A3.24", "A3.25", "A3.26", "A3.27"];
24309
25084
  var STATE_COLUMNS = ["status", "state"];
24310
- var isPlainObject13 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
25085
+ var isPlainObject14 = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
24311
25086
  var said = (v) => typeof v === "string" && v.trim() !== "" ? v : null;
24312
25087
  var OPENERS4 = /* @__PURE__ */ new Set(["dialog", "list", "queue", "detail"]);
24313
25088
  var ROWS = /* @__PURE__ */ new Set(["list", "queue", "data_grid"]);
@@ -24316,9 +25091,9 @@ function checkUx(app, contract, contractError) {
24316
25091
  const reason = `the contract could not be extracted (${contractError ?? "no reason given"}), so the forms, lists and dialogs are not knowable here`;
24317
25092
  return { violations: [], skipped: RULES4.map((rule) => ({ rule, reason })) };
24318
25093
  }
24319
- const elements = isPlainObject13(contract.elements) ? contract.elements : {};
24320
- const handlers = isPlainObject13(contract.data?.app?.handlers) ? contract.data.app.handlers : {};
24321
- 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 : {};
24322
25097
  const uids = Object.keys(elements).sort(byPageThenRank);
24323
25098
  const kidsOf = (uid) => {
24324
25099
  const el = elements[uid];
@@ -24356,15 +25131,15 @@ function checkUx(app, contract, contractError) {
24356
25131
  if (!ROWS.has(el?.type)) continue;
24357
25132
  if (ancestors(uid).some((up) => up.type === "detail")) continue;
24358
25133
  const table = tableOf2(el);
24359
- 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;
24360
25135
  const columns = tables[table].columns;
24361
- 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";
24362
25137
  const fields = Object.keys(columns).filter((c) => c !== pk && !c.startsWith("_"));
24363
25138
  let shown2;
24364
25139
  let opens3;
24365
25140
  if (el.type === "data_grid") {
24366
- const typed = isPlainObject13(el.props?.props) ? el.props.props : {};
24367
- 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));
24368
25143
  opens3 = Boolean(said(typed.on_row)) || Array.isArray(typed.row_actions) && typed.row_actions.length > 0;
24369
25144
  } else {
24370
25145
  const kids = kidsOf(uid).map((k) => elements[k]);
@@ -24384,7 +25159,7 @@ function checkUx(app, contract, contractError) {
24384
25159
  for (const uid of uids) {
24385
25160
  const el = elements[uid];
24386
25161
  if (el?.type !== "dialog") continue;
24387
- const typed = isPlainObject13(el.props?.props) ? el.props.props : {};
25162
+ const typed = isPlainObject14(el.props?.props) ? el.props.props : {};
24388
25163
  if (typed.values_from === "row") continue;
24389
25164
  if (said(typed.trigger) || said(el.props?.label) || said(typed.title)) continue;
24390
25165
  violations.push({
@@ -24397,8 +25172,8 @@ function checkUx(app, contract, contractError) {
24397
25172
  const el = elements[uid];
24398
25173
  if (!ROWS.has(el?.type) || el.type === "data_grid") continue;
24399
25174
  const table = tableOf2(el);
24400
- const columns = isPlainObject13(tables[table]?.columns) ? tables[table].columns : {};
24401
- 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));
24402
25177
  const seen = /* @__PURE__ */ new Map();
24403
25178
  for (const kid of kidsOf(uid)) {
24404
25179
  const action = elements[kid];
@@ -24439,8 +25214,8 @@ function split(uid) {
24439
25214
  }
24440
25215
 
24441
25216
  // ../flows/appspace-builder/kit/contract/workflow.mjs
24442
- var isPlainObject14 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
24443
- 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 !== "";
24444
25219
  function either5(items) {
24445
25220
  if (items.length <= 1) return items.join("");
24446
25221
  return `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
@@ -24456,16 +25231,16 @@ function roleModel2(contract) {
24456
25231
  const sees = /* @__PURE__ */ new Map();
24457
25232
  const home = /* @__PURE__ */ new Map();
24458
25233
  for (const row of rows2) {
24459
- if (!isPlainObject14(row) || !named9(row.name) || names.includes(row.name)) continue;
25234
+ if (!isPlainObject15(row) || !named10(row.name) || names.includes(row.name)) continue;
24460
25235
  names.push(row.name);
24461
- sees.set(row.name, new Set((Array.isArray(row.sees) ? row.sees : []).filter(named9)));
24462
- 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);
24463
25238
  }
24464
25239
  return { names, sees, home };
24465
25240
  }
24466
25241
  function gateRoles2(gate) {
24467
- if (Array.isArray(gate)) return gate.filter(named9);
24468
- 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) : [];
24469
25244
  }
24470
25245
  function seesGate2(model, role, gate) {
24471
25246
  const wanted = gateRoles2(gate);
@@ -24483,7 +25258,7 @@ var HREF2 = /\bhref\s*=\s*["']([^"']*)["']/i;
24483
25258
  var ROLE_ATTR4 = /\bdata-dr-role\s*=\s*["']([^"']*)["']/i;
24484
25259
  var PAGE_LITERAL2 = /["'`](?:\.\/)?([A-Za-z0-9_-]+\.html)(?:[?#][^"'`]*)?["'`]/g;
24485
25260
  function targetOf2(href) {
24486
- 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;
24487
25262
  const file = href.replace(/^\.\//, "").replace(/[?#].*$/, "");
24488
25263
  if (!/^[A-Za-z0-9_-]+\.html$/.test(file)) return null;
24489
25264
  return `ui/${file}`;
@@ -24521,7 +25296,7 @@ function moduleLinks2(source) {
24521
25296
  return out;
24522
25297
  }
24523
25298
  function pagesOf2(contract) {
24524
- 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));
24525
25300
  }
24526
25301
  function entryPage2(contract) {
24527
25302
  const pages = pagesOf2(contract);
@@ -24540,10 +25315,10 @@ function reachByRole2(contract, { pageSource = () => void 0, moduleSource = () =
24540
25315
  const model = roleModel2(contract);
24541
25316
  const pages = pagesOf2(contract);
24542
25317
  const byPath = new Map(pages.map((page) => [page.path, page]));
24543
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
25318
+ const elements = isPlainObject15(contract?.elements) ? contract.elements : {};
24544
25319
  const modulesOn = /* @__PURE__ */ new Map();
24545
25320
  for (const element of Object.values(elements)) {
24546
- 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;
24547
25322
  if (!modulesOn.has(element.page)) modulesOn.set(element.page, /* @__PURE__ */ new Set());
24548
25323
  modulesOn.get(element.page).add(element.props.module);
24549
25324
  }
@@ -24668,23 +25443,23 @@ function moduleAsks2(source) {
24668
25443
  return ASKS2.some((pattern) => pattern.test(String(source ?? "")));
24669
25444
  }
24670
25445
  function decideAsks2(row) {
24671
- if (row?.role !== "decide" || !isPlainObject14(row.decision)) return false;
25446
+ if (row?.role !== "decide" || !isPlainObject15(row.decision)) return false;
24672
25447
  const names = Object.keys(row.decision);
24673
25448
  if (names.length !== 1) return false;
24674
25449
  const spec = row.decision[names[0]];
24675
- return isPlainObject14(spec) && (spec.note === "required" || spec.note === "optional");
25450
+ return isPlainObject15(spec) && (spec.note === "required" || spec.note === "optional");
24676
25451
  }
24677
25452
  function insideDialog2(elements, uid) {
24678
25453
  const seen = /* @__PURE__ */ new Set();
24679
- 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) {
24680
25455
  seen.add(at);
24681
25456
  if (elements[at]?.type === "dialog") return true;
24682
25457
  }
24683
25458
  return false;
24684
25459
  }
24685
25460
  function stateCallers2(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
24686
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
24687
- 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 : {};
24688
25463
  const changing = new Set(Object.keys(handlers).filter((name) => changesState2(name, handlers[name], handlerSource(name))));
24689
25464
  const out = /* @__PURE__ */ new Map();
24690
25465
  const add = (handler, caller) => {
@@ -24694,10 +25469,10 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24694
25469
  };
24695
25470
  for (const uid of Object.keys(elements).sort()) {
24696
25471
  const element = elements[uid];
24697
- const props = isPlainObject14(element?.props) ? element.props : {};
24698
- const typed = isPlainObject14(props.props) ? props.props : {};
25472
+ const props = isPlainObject15(element?.props) ? element.props : {};
25473
+ const typed = isPlainObject15(props.props) ? props.props : {};
24699
25474
  const page = element?.page ?? null;
24700
- const id = named9(props.id) ? props.id : uid;
25475
+ const id = named10(props.id) ? props.id : uid;
24701
25476
  switch (element?.type) {
24702
25477
  case "form":
24703
25478
  add(props.action, { page, uid, id, what: "form", asks: true });
@@ -24708,27 +25483,27 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24708
25483
  case "data_grid":
24709
25484
  add(typed.on_row, { page, uid, id, what: "grid row click", asks: false });
24710
25485
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
24711
- if (!isPlainObject14(action) || !named9(action.call)) continue;
24712
- 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);
24713
25488
  add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
24714
25489
  }
24715
25490
  break;
24716
25491
  case "action":
24717
25492
  case "detail": {
24718
- if (!named9(props.action)) break;
24719
- 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);
24720
25495
  add(props.action, { page, uid, id, what: element.type === "action" ? "button" : "detail button", asks });
24721
25496
  break;
24722
25497
  }
24723
25498
  case "custom": {
24724
- const declares = isPlainObject14(props.declares) ? props.declares : {};
25499
+ const declares = isPlainObject15(props.declares) ? props.declares : {};
24725
25500
  const called = /* @__PURE__ */ new Set([
24726
- ...Array.isArray(declares.calls) ? declares.calls.filter(named9) : [],
24727
- ...Object.keys(isPlainObject14(declares.produces) ? declares.produces : {})
25501
+ ...Array.isArray(declares.calls) ? declares.calls.filter(named10) : [],
25502
+ ...Object.keys(isPlainObject15(declares.produces) ? declares.produces : {})
24728
25503
  ]);
24729
- const source = named9(props.module) ? moduleSource(props.module) : void 0;
25504
+ const source = named10(props.module) ? moduleSource(props.module) : void 0;
24730
25505
  if (typeof source === "string") {
24731
- 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);
24732
25507
  }
24733
25508
  const asks = typeof source === "string" ? moduleAsks2(source) : null;
24734
25509
  for (const handler of called) add(handler, { page, uid, id, what: `custom ${props.module ?? ""}`.trim(), asks, module: props.module });
@@ -24741,7 +25516,7 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
24741
25516
  return out;
24742
25517
  }
24743
25518
  function unconfirmedStateChanges2(contract, files = {}) {
24744
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25519
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24745
25520
  const out = [];
24746
25521
  for (const [handler, callers] of [...stateCallers2(contract, files).entries()].sort((a, b) => a[0] < b[0] ? -1 : 1)) {
24747
25522
  const bare = callers.filter((one) => one.asks === false);
@@ -24813,7 +25588,7 @@ function requiresReason2(lines, key, vars) {
24813
25588
  return false;
24814
25589
  }
24815
25590
  function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource, pageSource } = {}) {
24816
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25591
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24817
25592
  const producers = payloadProducers2(contract, { moduleSource, pageSource });
24818
25593
  const out = [];
24819
25594
  for (const name of Object.keys(handlers).sort()) {
@@ -24829,7 +25604,7 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
24829
25604
  for (const key of one.keys) {
24830
25605
  if (!REASON_KEYS2.includes(key)) continue;
24831
25606
  if (!sent.has(key)) sent.set(key, /* @__PURE__ */ new Set());
24832
- if (named9(one.page)) sent.get(key).add(one.page);
25607
+ if (named10(one.page)) sent.get(key).add(one.page);
24833
25608
  }
24834
25609
  }
24835
25610
  for (const [key, pages] of [...sent.entries()].sort()) {
@@ -24852,7 +25627,7 @@ function unreadReasons2(contract, { handlerSource = () => void 0, moduleSource,
24852
25627
  return out;
24853
25628
  }
24854
25629
  function unliftableRefusals2(contract, { handlerSource = () => void 0 } = {}) {
24855
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25630
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24856
25631
  const out = [];
24857
25632
  for (const name of Object.keys(handlers).sort()) {
24858
25633
  if ((handlers[name]?.role ?? "custom") !== "custom") continue;
@@ -24910,13 +25685,13 @@ function rolesLandingNowhere2(contract) {
24910
25685
  function sourceRoles2(contract, name) {
24911
25686
  const source = contract?.data?.sources?.[name];
24912
25687
  const stated2 = gateRoles2(source?.requiresRole);
24913
- const elements = isPlainObject14(contract?.elements) ? contract.elements : {};
25688
+ const elements = isPlainObject15(contract?.elements) ? contract.elements : {};
24914
25689
  const pages = new Map(pagesOf2(contract).map((page) => [page.path, page]));
24915
25690
  const union = [];
24916
25691
  let readers = 0;
24917
25692
  let open2 = false;
24918
25693
  for (const element of Object.values(elements)) {
24919
- const props = isPlainObject14(element?.props) ? element.props : {};
25694
+ const props = isPlainObject15(element?.props) ? element.props : {};
24920
25695
  const reads = [props.source, ...Array.isArray(props.declares?.reads) ? props.declares.reads : []];
24921
25696
  if (!reads.includes(name)) continue;
24922
25697
  readers += 1;
@@ -24951,14 +25726,14 @@ function openToEveryRole2(contract) {
24951
25726
  { one: "page", many: "pages", verb: "open" },
24952
25727
  "State pages[].roles on each: the roles it is for (all of them, if that is what you mean)."
24953
25728
  );
24954
- const handlers = isPlainObject14(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25729
+ const handlers = isPlainObject15(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
24955
25730
  say(
24956
25731
  "handler",
24957
25732
  Object.keys(handlers).sort().filter((name) => gateRoles2(handlers[name]?.requiresRole).length === 0).map((name) => `"${name}"`),
24958
25733
  { one: "handler", many: "handlers", verb: "call" },
24959
25734
  "State requiresRole on each: the roles that may call it (all of them, if that is what you mean)."
24960
25735
  );
24961
- const sources = isPlainObject14(contract?.data?.sources) ? contract.data.sources : {};
25736
+ const sources = isPlainObject15(contract?.data?.sources) ? contract.data.sources : {};
24962
25737
  say(
24963
25738
  "source",
24964
25739
  Object.keys(sources).sort().filter((name) => {
@@ -25005,7 +25780,7 @@ function kitFromDisk2() {
25005
25780
  }
25006
25781
  return kitOnDisk2;
25007
25782
  }
25008
- 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"]);
25009
25784
  function verify2({ app: appRef }, { extractContract: extractContract2 = extract2, kit } = {}) {
25010
25785
  const KIT = kit ?? kitFromDisk2();
25011
25786
  const app = loadApp2(resolve12(appRef.dir));
@@ -25107,6 +25882,7 @@ var kit_default = {
25107
25882
  assistant: "dr-insights@1.4.0",
25108
25883
  charts: "echarts@5.6.0",
25109
25884
  fmt: "dr-fmt@1.0.0",
25885
+ markdown: "dr-md@1.0.0",
25110
25886
  uiKit: "ui-kit/v3",
25111
25887
  widgets: "dr-widgets@1.1.2"
25112
25888
  };
@@ -27077,18 +27853,18 @@ async function resolveFile(path3, base, { read = readFile11, confine = false, re
27077
27853
  const filename = basename2(full);
27078
27854
  return { bytes, filename, mimeType: mimeTypeOf(filename) };
27079
27855
  }
27080
- function isPlainObject15(value) {
27856
+ function isPlainObject16(value) {
27081
27857
  return typeof value === "object" && value !== null && !Array.isArray(value);
27082
27858
  }
27083
27859
  function fileColumnsOf(model) {
27084
27860
  const out = /* @__PURE__ */ new Set();
27085
- const tables = isPlainObject15(model) ? model["tables"] : void 0;
27086
- if (!isPlainObject15(tables)) return out;
27861
+ const tables = isPlainObject16(model) ? model["tables"] : void 0;
27862
+ if (!isPlainObject16(tables)) return out;
27087
27863
  for (const table of Object.values(tables)) {
27088
- const columns = isPlainObject15(table) ? table["columns"] : void 0;
27089
- if (!isPlainObject15(columns)) continue;
27864
+ const columns = isPlainObject16(table) ? table["columns"] : void 0;
27865
+ if (!isPlainObject16(columns)) continue;
27090
27866
  for (const [name, column] of Object.entries(columns)) {
27091
- 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;
27092
27868
  if (type === "file") out.add(name);
27093
27869
  }
27094
27870
  }
@@ -27152,7 +27928,7 @@ function typeOf(value) {
27152
27928
  if (Array.isArray(value)) return "array";
27153
27929
  return typeof value;
27154
27930
  }
27155
- function isPlainObject16(value) {
27931
+ function isPlainObject17(value) {
27156
27932
  return typeof value === "object" && value !== null && !Array.isArray(value);
27157
27933
  }
27158
27934
  function describe6(value) {
@@ -27179,7 +27955,7 @@ function validateShape(shape, path3 = "$") {
27179
27955
  }
27180
27956
  return shape.length === 0 ? [] : validateShape(shape[0], `${path3}[]`);
27181
27957
  }
27182
- if (isPlainObject16(shape)) {
27958
+ if (isPlainObject17(shape)) {
27183
27959
  return Object.entries(shape).flatMap(
27184
27960
  ([key, sub]) => validateShape(sub, `${path3}.${key}`)
27185
27961
  );
@@ -27208,8 +27984,8 @@ function matchShape(value, shape, path3 = "$") {
27208
27984
  if (shape.length === 0) return [];
27209
27985
  return value.flatMap((item, i) => matchShape(item, shape[0], `${path3}[${String(i)}]`));
27210
27986
  }
27211
- if (isPlainObject16(shape)) {
27212
- if (!isPlainObject16(value)) {
27987
+ if (isPlainObject17(shape)) {
27988
+ if (!isPlainObject17(value)) {
27213
27989
  return [`${path3}: expected object, got ${typeOf(value)} (${describe6(value)}).`];
27214
27990
  }
27215
27991
  return Object.entries(shape).flatMap(
@@ -27219,8 +27995,8 @@ function matchShape(value, shape, path3 = "$") {
27219
27995
  return [`${path3}: ${describe6(shape)} is not a shape.`];
27220
27996
  }
27221
27997
  function matchExpect(value, expected, path3 = "$") {
27222
- if (isPlainObject16(expected)) {
27223
- if (!isPlainObject16(value)) {
27998
+ if (isPlainObject17(expected)) {
27999
+ if (!isPlainObject17(value)) {
27224
28000
  return [`${path3}: expected an object, got ${typeOf(value)} (${describe6(value)}).`];
27225
28001
  }
27226
28002
  return Object.entries(expected).flatMap(
@@ -27251,7 +28027,7 @@ function isUiEntry(entry) {
27251
28027
  }
27252
28028
  function parseError(raw, where2) {
27253
28029
  if (raw === true) return {};
27254
- if (!isPlainObject16(raw)) {
28030
+ if (!isPlainObject17(raw)) {
27255
28031
  throw new FixtureError(
27256
28032
  `${where2}: "error" is true, or an object with "type" and/or "contains" \u2014 not ${describe6(raw)}.`
27257
28033
  );
@@ -27269,12 +28045,12 @@ function parseError(raw, where2) {
27269
28045
  }
27270
28046
  var FILE_FIELD = /^[A-Za-z_][A-Za-z0-9_]*$/;
27271
28047
  function parseFiles(raw, payload, where2) {
27272
- if (!isPlainObject16(raw)) {
28048
+ if (!isPlainObject17(raw)) {
27273
28049
  throw new FixtureError(
27274
28050
  `${where2}: "files" maps a payload key to "sample" or a path in the app folder \u2014 not ${describe6(raw)}.`
27275
28051
  );
27276
28052
  }
27277
- if (!isPlainObject16(payload)) {
28053
+ if (!isPlainObject17(payload)) {
27278
28054
  throw new FixtureError(
27279
28055
  `${where2}: a case with "files" puts {"upload": \u2026} under a payload key, so its "payload" is an object.`
27280
28056
  );
@@ -27300,7 +28076,7 @@ function parseFiles(raw, payload, where2) {
27300
28076
  }
27301
28077
  function parseCase(handler, raw, index) {
27302
28078
  const where2 = `handlers.${handler}[${String(index)}]`;
27303
- if (!isPlainObject16(raw)) {
28079
+ if (!isPlainObject17(raw)) {
27304
28080
  throw new FixtureError(`${where2}: a case is an object, not ${describe6(raw)}.`);
27305
28081
  }
27306
28082
  const name = raw["name"];
@@ -27333,11 +28109,11 @@ function parseCase(handler, raw, index) {
27333
28109
  return kase;
27334
28110
  }
27335
28111
  function parseFixtures(raw, source) {
27336
- if (!isPlainObject16(raw)) {
28112
+ if (!isPlainObject17(raw)) {
27337
28113
  throw new FixtureError(`${source}: the fixtures file is a JSON object, not ${describe6(raw)}.`);
27338
28114
  }
27339
28115
  const handlersRaw = raw["handlers"] ?? {};
27340
- if (!isPlainObject16(handlersRaw)) {
28116
+ if (!isPlainObject17(handlersRaw)) {
27341
28117
  throw new FixtureError(`${source}: "handlers" is an object keyed by handler name.`);
27342
28118
  }
27343
28119
  const cases = [];
@@ -27357,7 +28133,7 @@ function parseFixtures(raw, source) {
27357
28133
  });
27358
28134
  }
27359
28135
  const uiRaw = raw["ui"] ?? {};
27360
- if (!isPlainObject16(uiRaw)) {
28136
+ if (!isPlainObject17(uiRaw)) {
27361
28137
  throw new FixtureError(`${source}: "ui" is an object.`);
27362
28138
  }
27363
28139
  const entry = uiRaw["entry"] ?? "index.html";
@@ -29431,45 +30207,45 @@ __export(levers_exports, {
29431
30207
  unboundControls: () => unboundControls2,
29432
30208
  unreadParams: () => unreadParams2
29433
30209
  });
29434
- var isPlainObject17 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
29435
- 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 !== "";
29436
30212
  function both4(items) {
29437
30213
  if (items.length <= 1) return items.join("");
29438
30214
  return `${items.slice(0, -1).join(", ")} and ${items.at(-1)}`;
29439
30215
  }
29440
30216
  function paramPages2(contract) {
29441
30217
  return (Array.isArray(contract?.pages) ? contract.pages : []).filter(
29442
- (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
29443
30219
  );
29444
30220
  }
29445
30221
  function namesOf2(page) {
29446
30222
  const out = [];
29447
- 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);
29448
30224
  return out;
29449
30225
  }
29450
30226
  function elementsOn2(contract, path3) {
29451
- const elements = isPlainObject17(contract?.elements) ? contract.elements : {};
30227
+ const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29452
30228
  return Object.keys(elements).sort().filter((uid) => elements[uid]?.page === path3).map((uid) => [uid, elements[uid]]);
29453
30229
  }
29454
30230
  function paramModeOf2(element) {
29455
- const props = isPlainObject17(element?.props) ? element.props : {};
29456
- 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;
29457
30233
  if (element?.type === "field" || element?.type === "action") return null;
29458
- if (named10(props.param) && element?.type !== "custom") return props.param;
29459
- const typed = isPlainObject17(props.props) ? props.props : {};
29460
- 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;
29461
30237
  }
29462
30238
  function paramsWrittenBy2(element) {
29463
30239
  const out = [];
29464
30240
  const one = paramModeOf2(element);
29465
30241
  if (one !== null) out.push(one);
29466
30242
  if (element?.type === "custom") return out;
29467
- const typed = isPlainObject17(element?.props?.props) ? element.props.props : {};
29468
- 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);
29469
30245
  for (const filter of Array.isArray(typed.filters) ? typed.filters : []) {
29470
- if (!isPlainObject17(filter)) continue;
29471
- if (named10(filter.param)) out.push(filter.param);
29472
- 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);
29473
30249
  }
29474
30250
  return out;
29475
30251
  }
@@ -29481,10 +30257,10 @@ function paramWriters2(contract, { moduleSource = () => void 0 } = {}) {
29481
30257
  for (const [, element] of elementsOn2(contract, page.path)) {
29482
30258
  for (const name of paramsWrittenBy2(element)) written.add(name);
29483
30259
  if (element?.type !== "custom") continue;
29484
- const props = isPlainObject17(element.props) ? element.props : {};
29485
- const produces = isPlainObject17(props.declares?.produces) ? props.declares.produces : {};
29486
- for (const keys2 of Object.values(produces)) for (const key of Array.isArray(keys2) ? keys2 : []) if (named10(key)) written.add(key);
29487
- 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;
29488
30264
  const source = moduleSource(props.module);
29489
30265
  if (typeof source !== "string") {
29490
30266
  unknown = true;
@@ -29495,6 +30271,13 @@ function paramWriters2(contract, { moduleSource = () => void 0 } = {}) {
29495
30271
  else written.add(call.name);
29496
30272
  }
29497
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
+ }
29498
30281
  out.set(page.path, { written, unknown });
29499
30282
  }
29500
30283
  return out;
@@ -29508,7 +30291,7 @@ function unboundControls2(contract, files = {}) {
29508
30291
  for (const name of namesOf2(page)) {
29509
30292
  if (written.has(name)) continue;
29510
30293
  const param = page.params.find((one) => one?.name === name) ?? {};
29511
- const kind = named10(param.kind) ? param.kind : null;
30294
+ const kind = named11(param.kind) ? param.kind : null;
29512
30295
  const opens3 = param.default === void 0 ? "no value at all" : `its default (${JSON.stringify(param.default)})`;
29513
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}.`;
29514
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.`;
@@ -29523,16 +30306,16 @@ function honoursOf2(sources, name) {
29523
30306
  const queue = [name];
29524
30307
  while (queue.length > 0) {
29525
30308
  const one = queue.shift();
29526
- if (seen.has(one) || !isPlainObject17(sources[one])) continue;
30309
+ if (seen.has(one) || !isPlainObject18(sources[one])) continue;
29527
30310
  seen.add(one);
29528
- 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);
29529
30312
  if (Array.isArray(sources[one].from)) queue.push(...sources[one].from);
29530
30313
  }
29531
30314
  return out;
29532
30315
  }
29533
30316
  function unreadParams2(contract, { moduleSource = () => void 0, handlerSource = () => void 0 } = {}) {
29534
- const sources = isPlainObject17(contract?.data?.sources) ? contract.data.sources : {};
29535
- 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 : {};
29536
30319
  const out = [];
29537
30320
  for (const page of paramPages2(contract)) {
29538
30321
  const read = /* @__PURE__ */ new Set();
@@ -29541,15 +30324,15 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
29541
30324
  const custom = [];
29542
30325
  let unknown = false;
29543
30326
  for (const [, element] of elementsOn2(contract, page.path)) {
29544
- const props = isPlainObject17(element?.props) ? element.props : {};
29545
- 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);
29546
30329
  for (const one of reads) {
29547
30330
  if (Object.prototype.hasOwnProperty.call(sources, one)) {
29548
30331
  if (!named_.includes(one)) named_.push(one);
29549
30332
  for (const honored of honoursOf2(sources, one)) read.add(honored);
29550
30333
  } else if (handlers[one]?.role === "custom" && !custom.includes(one)) custom.push(one);
29551
30334
  }
29552
- if (element?.type === "custom" && named10(props.module)) {
30335
+ if (element?.type === "custom" && named11(props.module)) {
29553
30336
  const text3 = moduleSource(props.module);
29554
30337
  if (typeof text3 === "string") modules.push(text3);
29555
30338
  else unknown = true;
@@ -29574,190 +30357,6 @@ function unreadParams2(contract, { moduleSource = () => void 0, handlerSource =
29574
30357
  return out;
29575
30358
  }
29576
30359
 
29577
- // ../flows/insights-builder/kit/contract/look.mjs
29578
- var look_exports = {};
29579
- __export(look_exports, {
29580
- humanise: () => humanise3,
29581
- layoutProblems: () => layoutProblems2,
29582
- lookPages: () => lookPages2,
29583
- rootsOf: () => rootsOf3,
29584
- untitledCards: () => untitledCards2
29585
- });
29586
- var isPlainObject18 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
29587
- var named11 = (value) => typeof value === "string" && value !== "";
29588
- function humanise3(name) {
29589
- const words = String(name).replace(/^_+/, "").replace(/_/g, " ").trim();
29590
- return words === "" ? "" : words[0].toUpperCase() + words.slice(1);
29591
- }
29592
- var WORDING2 = {
29593
- custom: { noun: "card", fallback: (p) => [humanise3(String(p.module ?? "").replace(/^.*\/|\.js$/g, "")), "its module"] },
29594
- chart: { noun: "card", fallback: (p) => [humanise3(String(p.y ?? "")), "its y column"] },
29595
- widget: { noun: "card", fallback: (p) => [humanise3(named11(p.source) ? p.source : "widget"), "its source"] },
29596
- stat: { noun: "tile", fallback: (p) => [humanise3(String(p.measure ?? "")), "its measure"] },
29597
- list: { noun: "card", fallback: (p) => [humanise3(named11(p.entity) ? p.entity : "list"), "its entity"] },
29598
- queue: { noun: "card", fallback: (p) => [humanise3(named11(p.entity) ? p.entity : "queue"), "its entity"] },
29599
- form: { noun: "card", fallback: () => ["New", "the runtime\u2019s default"] },
29600
- dialog: { noun: "dialog" }
29601
- };
29602
- var TITLED2 = Object.fromEntries(
29603
- TITLED_TYPES.map((type) => [type, { noun: CONTROL_PARTS.includes(type) ? "input" : "card", fallback: () => [humanise3(type), "its type"], ...WORDING2[type] }])
29604
- );
29605
- var LAYOUT2 = /* @__PURE__ */ new Set(["region", "tabs", "tab_panel"]);
29606
- function rootsOf3(contract, page) {
29607
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29608
- if (Array.isArray(page?.tree) && page.tree.length > 0) return page.tree.filter((uid) => elements[uid] !== void 0);
29609
- const contained = /* @__PURE__ */ new Set();
29610
- for (const element of Object.values(elements)) for (const child of element?.children ?? []) contained.add(child);
29611
- return Object.keys(elements).filter((uid) => {
29612
- const element = elements[uid];
29613
- return element?.page === page?.path && (element.parent === null || element.parent === void 0) && !contained.has(uid);
29614
- });
29615
- }
29616
- function untitledCards2(contract) {
29617
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29618
- const out = [];
29619
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29620
- if (page?.owned === true) continue;
29621
- const seen = /* @__PURE__ */ new Set();
29622
- const drawn2 = [];
29623
- const walk4 = (uids) => {
29624
- for (const uid of uids) {
29625
- const element = elements[uid];
29626
- if (element === void 0 || seen.has(uid)) continue;
29627
- seen.add(uid);
29628
- if (LAYOUT2.has(element.type ?? "")) {
29629
- const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
29630
- walk4(children);
29631
- } else {
29632
- drawn2.push(uid);
29633
- }
29634
- }
29635
- };
29636
- walk4(rootsOf3(contract, page));
29637
- for (const uid of drawn2) {
29638
- const element = elements[uid];
29639
- const type = element.type ?? "";
29640
- const titled = TITLED2[type];
29641
- if (titled === void 0) continue;
29642
- const props = isPlainObject18(element.props) ? element.props : {};
29643
- const typed = isPlainObject18(props.props) ? props.props : {};
29644
- const card = isPlainObject18(element.card) ? element.card : isPlainObject18(props.card) ? props.card : {};
29645
- if (named11(props.label) || named11(card.title)) continue;
29646
- if (type === "dialog" ? named11(typed.title) : named11(typed.label)) continue;
29647
- const id = named11(element.id) ? element.id : named11(props.id) ? props.id : null;
29648
- const [title, from] = named11(props.field) ? [humanise3(props.field), "its field"] : id !== null ? [humanise3(id), "its id"] : titled.fallback(props);
29649
- const what = id !== null ? `${type} "${id}"` : `the ${type} ${uid}`;
29650
- const shown2 = title === "" ? "has no title at all" : `is titled "${title}" after ${from}`;
29651
- out.push({ page: page.path, uid, type, id, noun: titled.noun, title, from, message: `${what} has no label, so its ${titled.noun} ${shown2}` });
29652
- }
29653
- }
29654
- return out;
29655
- }
29656
- var BAND_LOOK5 = ["subtitle", "layout", "surface", "aside"];
29657
- function lookPages2(contract) {
29658
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29659
- const out = /* @__PURE__ */ new Set();
29660
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29661
- if (page?.owned === true || !named11(page?.path)) continue;
29662
- const bands = (Array.isArray(page.sections) ? page.sections : []).some((section) => BAND_LOOK5.some((key) => named11(section?.[key])));
29663
- const cards = Object.values(elements).some((element) => element?.page === page.path && (isPlainObject18(element.card) || named11(element.emphasis)));
29664
- if (bands || cards) out.add(page.path);
29665
- }
29666
- return out;
29667
- }
29668
- var SPAN_LAYOUTS2 = /* @__PURE__ */ new Set([void 0, "grid"]);
29669
- var CONTROL_TYPES2 = /* @__PURE__ */ new Set(["filter", "control", ...CONTROL_PARTS]);
29670
- function bandsOf3(contract, page) {
29671
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29672
- const roots = rootsOf3(contract, page);
29673
- return (Array.isArray(page.sections) ? page.sections : []).filter((section) => named11(section?.id)).map((section) => ({ section, roots: roots.filter((uid) => elements[uid]?.section === section.id) }));
29674
- }
29675
- function nameOf4(uid, element) {
29676
- const id = named11(element?.props?.id) ? element.props.id : named11(element?.id) ? element.id : null;
29677
- return id === null ? `the ${element?.type ?? "element"} ${uid}` : `${element.type} "${id}"`;
29678
- }
29679
- function isControl2(element, moduleSource) {
29680
- const props = isPlainObject18(element?.props) ? element.props : {};
29681
- if (CONTROL_TYPES2.has(element?.type)) return true;
29682
- if (element?.type !== "custom" && element?.type !== "action" && named11(props.param)) return true;
29683
- if (isPlainObject18(props.props) && named11(props.props.param)) return true;
29684
- if (element?.type === "custom" && named11(props.module)) {
29685
- const source = moduleSource(props.module);
29686
- return typeof source === "string" && scanUiSetParams(source).length > 0;
29687
- }
29688
- return false;
29689
- }
29690
- function layoutProblems2(contract, { moduleSource = () => void 0 } = {}) {
29691
- const elements = isPlainObject18(contract?.elements) ? contract.elements : {};
29692
- const looked = lookPages2(contract);
29693
- const out = [];
29694
- for (const page of Array.isArray(contract?.pages) ? contract.pages : []) {
29695
- if (!looked.has(page?.path)) continue;
29696
- for (const { section, roots } of bandsOf3(contract, page)) {
29697
- const band = `band "${section.id}" on ${page.path}`;
29698
- const layout = named11(section.layout) ? section.layout : void 0;
29699
- if (layout === "kpi_strip") {
29700
- for (const uid of roots) {
29701
- const element = elements[uid];
29702
- if (element.type === "stat" || element.type === "widget" && element.props?.kind === "kpi") continue;
29703
- out.push({
29704
- rule: "L1",
29705
- page: page.path,
29706
- section: section.id,
29707
- uid,
29708
- 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)`
29709
- });
29710
- }
29711
- }
29712
- if (layout === "sidebar" && roots.length > 0) {
29713
- const uid = section.aside === "start" ? roots[0] : roots[roots.length - 1];
29714
- if (!isControl2(elements[uid], moduleSource)) {
29715
- out.push({
29716
- rule: "L2",
29717
- page: page.path,
29718
- section: section.id,
29719
- uid,
29720
- 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`
29721
- });
29722
- }
29723
- }
29724
- if (SPAN_LAYOUTS2.has(layout)) {
29725
- let row = [];
29726
- let used = 0;
29727
- for (const uid of roots) {
29728
- const raw = elements[uid]?.grid?.span;
29729
- const span = raw === null || raw === void 0 ? 12 : Number(raw);
29730
- if (!Number.isInteger(span) || span < 1 || span > 12) {
29731
- 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` });
29732
- continue;
29733
- }
29734
- if (used + span > 12) {
29735
- if (used < 12) {
29736
- const spans = [...row.map((one) => one.span), span];
29737
- out.push({
29738
- rule: "L4",
29739
- page: page.path,
29740
- section: section.id,
29741
- uid,
29742
- 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`
29743
- });
29744
- }
29745
- row = [];
29746
- used = 0;
29747
- }
29748
- row.push({ uid, span });
29749
- used += span;
29750
- if (used === 12) {
29751
- row = [];
29752
- used = 0;
29753
- }
29754
- }
29755
- }
29756
- }
29757
- }
29758
- return out;
29759
- }
29760
-
29761
30360
  // src/commands/appspace/_findings.ts
29762
30361
  var DECLARES = declares_exports;
29763
30362
  var LEVERS = levers_exports;