@datarails-ai/dr-appspace-cli 0.12.2 → 0.12.3

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 +453 -39
  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.12.2",
16
+ version: "0.12.3",
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-08T08:05:29.934Z";
60
+ var BUILD_TIME = "2026-10-08T11:11:55.601Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -6862,6 +6862,10 @@ function partCalls(element) {
6862
6862
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
6863
6863
  if (action && typeof action === "object" && named(action.call)) out.push(action.call);
6864
6864
  }
6865
+ const bulk = typed.selection && typeof typed.selection === "object" ? typed.selection.bulk_actions : void 0;
6866
+ for (const action of Array.isArray(bulk) ? bulk : []) {
6867
+ if (action && typeof action === "object" && named(action.call)) out.push(action.call);
6868
+ }
6865
6869
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
6866
6870
  if (column && typeof column === "object" && column.cell === "toggle" && named(column.call)) out.push(column.call);
6867
6871
  }
@@ -7570,9 +7574,7 @@ function backendSection(app, manifest, pages, elements) {
7570
7574
  signature_ok: signatureOk(source),
7571
7575
  reads_ctx_role: CTX_ROLE.test(source),
7572
7576
  tables_touched: [...new Set([...source.matchAll(TABLE_CALL)].map((match) => match[2]))].sort(),
7573
- verbs: DATA_VERBS.filter(
7574
- (verb) => TABLE_ONLY_VERBS.has(verb) ? callsOnTable(source, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(source)
7575
- ),
7577
+ verbs: handlerVerbs(source),
7576
7578
  referenced_by_page: isReferenced(handler.name, scriptText) || attributed.has(handler.name)
7577
7579
  };
7578
7580
  }
@@ -7591,6 +7593,10 @@ function backendSection(app, manifest, pages, elements) {
7591
7593
  invariants: invariants(model, roles, handlers)
7592
7594
  };
7593
7595
  }
7596
+ function handlerVerbs(source) {
7597
+ const text3 = typeof source === "string" ? source : "";
7598
+ return DATA_VERBS.filter((verb) => TABLE_ONLY_VERBS.has(verb) ? callsOnTable(text3, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(text3));
7599
+ }
7594
7600
  function callsOnTable(source, verb) {
7595
7601
  const holders = [TABLE_INLINE];
7596
7602
  for (const match of source.matchAll(TABLE_LOCAL)) holders.push(match[1]);
@@ -11298,6 +11304,9 @@ function payloadProducers(contract, { moduleSource = () => void 0, pageSource =
11298
11304
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
11299
11305
  if (isPlainObject3(action) && named2(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
11300
11306
  }
11307
+ for (const action of isPlainObject3(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
11308
+ if (isPlainObject3(action) && named2(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["ids"] });
11309
+ }
11301
11310
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
11302
11311
  if (isPlainObject3(column) && column.cell === "toggle" && named2(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
11303
11312
  }
@@ -12092,6 +12101,11 @@ function stateCallers(contract, { moduleSource = () => void 0, handlerSource = (
12092
12101
  const asks = action.confirm === true || named3(action.confirm);
12093
12102
  add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
12094
12103
  }
12104
+ for (const action of isPlainObject4(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
12105
+ if (!isPlainObject4(action) || !named3(action.call)) continue;
12106
+ const asks = action.confirm === true || named3(action.confirm);
12107
+ add(action.call, { page, uid, id, what: `bulk action "${action.label ?? action.call}"`, asks });
12108
+ }
12095
12109
  break;
12096
12110
  case "action":
12097
12111
  case "detail": {
@@ -12421,13 +12435,18 @@ function isPlatformChartColour(n, value) {
12421
12435
  var look_exports = {};
12422
12436
  __export(look_exports, {
12423
12437
  HEADER_ACTION_TYPES: () => HEADER_ACTION_TYPES,
12438
+ STATE_NAMES: () => STATE_NAMES,
12439
+ folderParams: () => folderParams,
12424
12440
  headerActionProblems: () => headerActionProblems,
12425
12441
  humanise: () => humanise,
12442
+ isAppContract: () => isAppContract,
12443
+ kpiStripWorkflowProblems: () => kpiStripWorkflowProblems,
12426
12444
  layoutProblems: () => layoutProblems,
12427
12445
  lookPages: () => lookPages,
12428
12446
  navCountProblems: () => navCountProblems,
12429
12447
  railReachProblems: () => railReachProblems,
12430
12448
  rootsOf: () => rootsOf,
12449
+ shellExpectedProblems: () => shellExpectedProblems,
12431
12450
  untitledCards: () => untitledCards
12432
12451
  });
12433
12452
  var isPlainObject5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
@@ -12735,6 +12754,181 @@ function railReachProblems(contract) {
12735
12754
  }
12736
12755
  return out;
12737
12756
  }
12757
+ var STATE_NAMES = ["folder", "status", "stage", "state", "queue"];
12758
+ var CATCH_ALL = /* @__PURE__ */ new Set(["", "all", "any", "*", "everything"]);
12759
+ var PICK_KINDS = /* @__PURE__ */ new Set(["select", "multi"]);
12760
+ var ROW_TYPES = /* @__PURE__ */ new Set(["data_grid", "list", "queue"]);
12761
+ var NOT_COUNTS = ["sum", "avg", "avg_days", "count_events"];
12762
+ var VALUES_FIX = "state `app.shell` with `nav: rail` and one entry per value, counts from a `summary` handler; look.md rule 1";
12763
+ var PAGES_FIX = "state `app.shell` with `nav: rail`; with no `rail.entries` the rail lists the pages; look.md rule 1";
12764
+ var COUNTS_FIX = "state `app.shell` with `nav: rail` and one entry per state, each counting a measure of that summary (`pages[].nav.counts.source`); look.md rule 1";
12765
+ function isAppContract(contract) {
12766
+ const kind = contract?.app?.kind;
12767
+ return kind !== "dashboard" && (kind === "app" || isPlainObject5(contract?.data?.app));
12768
+ }
12769
+ function tablesOf(contract) {
12770
+ const tables = contract?.app?.data_model?.tables;
12771
+ return isPlainObject5(tables) ? tables : {};
12772
+ }
12773
+ function enumOf(column) {
12774
+ return Array.isArray(column?.enum) ? column.enum.filter((one) => typeof one === "string") : [];
12775
+ }
12776
+ function stateColumn(contract, table) {
12777
+ const columns = isPlainObject5(tablesOf(contract)[table]?.columns) ? tablesOf(contract)[table].columns : {};
12778
+ return Object.keys(columns).find((name) => STATE_NAMES.includes(name) && enumOf(columns[name]).length >= 3) ?? null;
12779
+ }
12780
+ function realValues(param) {
12781
+ return Array.isArray(param?.values) ? param.values.map(String).filter((value) => !CATCH_ALL.has(value.toLowerCase())) : [];
12782
+ }
12783
+ function ownPages(contract) {
12784
+ return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true && named4(page?.path));
12785
+ }
12786
+ function folderParams(contract) {
12787
+ const enums = [];
12788
+ for (const [table, spec] of Object.entries(tablesOf(contract))) {
12789
+ const columns = isPlainObject5(spec?.columns) ? spec.columns : {};
12790
+ for (const name of STATE_NAMES) if (enumOf(columns[name]).length >= 3) enums.push({ where: `${table}.${name}`, values: new Set(enumOf(columns[name])) });
12791
+ }
12792
+ const out = [];
12793
+ for (const page of ownPages(contract)) {
12794
+ for (const param of Array.isArray(page.params) ? page.params : []) {
12795
+ if (!PICK_KINDS.has(param?.kind) || !named4(param?.name)) continue;
12796
+ const values = realValues(param);
12797
+ if (STATE_NAMES.includes(param.name) && (values.length >= 3 || !Array.isArray(param.values))) {
12798
+ out.push({ page, param, values, why: `named "${param.name}"` });
12799
+ continue;
12800
+ }
12801
+ if (values.length < 3) continue;
12802
+ const match = enums.find((one) => values.every((value) => one.values.has(value)));
12803
+ if (match !== void 0) out.push({ page, param, values, why: `its values are the enum of ${match.where}` });
12804
+ }
12805
+ }
12806
+ return out;
12807
+ }
12808
+ function tableOf2(contract, element) {
12809
+ const props = isPlainObject5(element?.props) ? element.props : {};
12810
+ if (named4(props.entity)) return props.entity;
12811
+ const source = props.source;
12812
+ if (!named4(source)) return null;
12813
+ const handler = contract?.data?.app?.handlers?.[source];
12814
+ if (isPlainObject5(handler) && named4(handler.table)) return handler.table;
12815
+ const read = contract?.data?.sources?.[source];
12816
+ if (isPlainObject5(read?.from) && named4(read.from.table)) return read.from.table;
12817
+ return null;
12818
+ }
12819
+ function listingOn(contract, page, test) {
12820
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12821
+ for (const [uid, element] of Object.entries(elements)) {
12822
+ if (element?.page !== page.path || !ROW_TYPES.has(element.type)) continue;
12823
+ const table = tableOf2(contract, element);
12824
+ if (table !== null && test(table)) return [uid, element, table];
12825
+ }
12826
+ return null;
12827
+ }
12828
+ function stateCountColumn(spec) {
12829
+ if (!isPlainObject5(spec) || NOT_COUNTS.some((key) => spec[key] !== void 0)) return null;
12830
+ const where2 = spec.count === true ? isPlainObject5(spec.where) ? spec.where : {} : spec.count === void 0 ? spec : null;
12831
+ if (where2 === null) return null;
12832
+ return Object.keys(where2).map((key) => key.split("__")[0]).find((column) => STATE_NAMES.includes(column)) ?? null;
12833
+ }
12834
+ function stateCounts(handler) {
12835
+ const out = {};
12836
+ for (const [measure, spec] of Object.entries(isPlainObject5(handler?.measures) ? handler.measures : {})) {
12837
+ const column = stateCountColumn(spec);
12838
+ if (column !== null) out[measure] = column;
12839
+ }
12840
+ return out;
12841
+ }
12842
+ function pageReads(contract, page, name) {
12843
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12844
+ if (page?.nav?.counts?.source === name) return true;
12845
+ return Object.values(elements).some((element) => element?.page === page.path && element.props?.source === name);
12846
+ }
12847
+ function shellExpectedProblems(contract) {
12848
+ if (!isAppContract(contract)) return [];
12849
+ const shell = isPlainObject5(contract?.app?.shell) ? contract.app.shell : {};
12850
+ if (shell.nav === "rail") return [];
12851
+ const out = [];
12852
+ const say = (page, param, where2, message, fix) => out.push({ rule: "L8", code: "shell-expected", page, param, where: where2, message, fix });
12853
+ const has = shell.nav === void 0 || shell.nav === null ? "states no app.shell" : `states app.shell.nav ${JSON.stringify(shell.nav)}`;
12854
+ const pages = ownPages(contract);
12855
+ if (pages.length > 4) say(null, null, "pages", `the app has ${pages.length} pages and ${has}, so its screens are a row of tabs too long to scan; an app this size is a rail`, PAGES_FIX);
12856
+ const named_ = /* @__PURE__ */ new Set();
12857
+ for (const { page, param, values, why } of folderParams(contract)) {
12858
+ named_.add(page.path);
12859
+ const listed = values.length > 0 ? ` with ${values.length} values (${values.map((one) => JSON.stringify(one)).join(", ")})` : " (its values from elsewhere)";
12860
+ say(page.path, param.name, `${page.path} params.${param.name}`, `${page.path} declares ${param.kind} param "${param.name}" (${why})${listed} and the app ${has}, so its folders are a dropdown with no counts; they are the rail's entries`, VALUES_FIX);
12861
+ }
12862
+ const handlers = isPlainObject5(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
12863
+ for (const [name, handler] of Object.entries(handlers)) {
12864
+ if (handler?.role !== "summary" || !named4(handler.table)) continue;
12865
+ const column = stateColumn(contract, handler.table);
12866
+ const counts = Object.fromEntries(Object.entries(stateCounts(handler)).filter(([, by]) => by === column));
12867
+ if (column === null || Object.keys(counts).length < 3) continue;
12868
+ for (const page of pages) {
12869
+ if (named_.has(page.path) || !pageReads(contract, page, name)) continue;
12870
+ const listing = listingOn(contract, page, (table) => table === handler.table);
12871
+ if (listing === null) continue;
12872
+ const columns = [column];
12873
+ say(
12874
+ page.path,
12875
+ null,
12876
+ `${page.path} ${name}`,
12877
+ `${page.path} reads the summary "${name}", whose measures count ${handler.table} by ${columns.join(", ")} (${Object.keys(counts).map((one) => JSON.stringify(one)).join(", ")}), beside ${nameOf3(listing[0], listing[1])} listing ${handler.table}, and the app ${has}: those counts are folders, and folders are the rail's entries`,
12878
+ COUNTS_FIX
12879
+ );
12880
+ }
12881
+ }
12882
+ return out;
12883
+ }
12884
+ function kpiStripWorkflowProblems(contract) {
12885
+ if (!isAppContract(contract)) return [];
12886
+ const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
12887
+ const handlers = isPlainObject5(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
12888
+ const folderPages = /* @__PURE__ */ new Map();
12889
+ for (const { page, param } of folderParams(contract)) if (!folderPages.has(page.path)) folderPages.set(page.path, param.name);
12890
+ const out = [];
12891
+ for (const page of ownPages(contract)) {
12892
+ const listing = listingOn(contract, page, (table) => stateColumn(contract, table) !== null);
12893
+ const workflow = listing !== null ? `${nameOf3(listing[0], listing[1])} lists ${listing[2]}, whose ${stateColumn(contract, listing[2])} column is a set of states` : folderPages.has(page.path) ? `the page declares the folder-like param "${folderPages.get(page.path)}"` : null;
12894
+ if (workflow === null) continue;
12895
+ const banded = bandsOf(contract, page);
12896
+ const inBand = new Set(banded.flatMap((band) => band.roots));
12897
+ const loose = rootsOf(contract, page).filter((uid) => !inBand.has(uid));
12898
+ for (const { section, roots } of [...banded, { section: null, roots: loose }]) {
12899
+ const stats = roots.filter((uid) => elements[uid]?.type === "stat");
12900
+ if (stats.length < 2) continue;
12901
+ const bySource = /* @__PURE__ */ new Map();
12902
+ for (const uid of stats) {
12903
+ const source = elements[uid]?.props?.source;
12904
+ if (!named4(source) || handlers[source]?.role !== "summary") continue;
12905
+ if (!bySource.has(source)) bySource.set(source, []);
12906
+ bySource.get(source).push(uid);
12907
+ }
12908
+ for (const [source, group] of bySource) {
12909
+ if (group.length < 2) continue;
12910
+ const measures = handlers[source].measures;
12911
+ const known = isPlainObject5(measures) && group.every((uid) => Object.prototype.hasOwnProperty.call(measures, elements[uid]?.props?.measure));
12912
+ const counts = stateCounts(handlers[source]);
12913
+ if (known ? !group.every((uid) => counts[elements[uid].props.measure] !== void 0) : group.length !== stats.length) continue;
12914
+ const where2 = section === null ? page.path : `band "${section.id}" on ${page.path}`;
12915
+ const shape = section?.layout === "kpi_strip" ? "is a kpi_strip of" : "holds a row of";
12916
+ const what = known ? `${group.length} counts of rows by ${[...new Set(group.map((uid) => counts[elements[uid].props.measure]))].join(", ")}` : `${group.length} counts`;
12917
+ out.push({
12918
+ rule: "L9",
12919
+ code: "kpi-strip-on-workflow",
12920
+ page: page.path,
12921
+ section: section?.id ?? null,
12922
+ where: where2,
12923
+ message: `${where2} ${shape} ${what} from the summary "${source}", and ${workflow}: folder or state counts belong on the rail, not in a KPI strip (workflow.md \xA78)`,
12924
+ fix: VALUES_FIX
12925
+ });
12926
+ break;
12927
+ }
12928
+ }
12929
+ }
12930
+ return out;
12931
+ }
12738
12932
 
12739
12933
  // ../flows/insights-builder/kit/contract/platformBadges.mjs
12740
12934
  var PLATFORM_BADGE_COLOURS = Object.freeze([
@@ -13886,8 +14080,9 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
13886
14080
  const warnings = [...sourceFindings, ...widgetFindings].filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one);
13887
14081
  problems.push(...widgetFindings.filter((one) => !one.advisory));
13888
14082
  if (tree.problems.length === 0) problems.push(...partProblems(declared, tree, handlers, sources, contract?.pages, model, { "dr-components": appComponents }));
13889
- if (tree.problems.length === 0) problems.push(...workflowProblems(declared, tree, handlers, sources, contract?.backend?.handlers, model));
13890
- const selectedFindings = tree.problems.length === 0 ? selectedProblems(declared, tree, handlers, sources, contract?.backend?.handlers, contract?.pages) : [];
14083
+ const backendFacts = inferredBackendHandlers(contract?.backend?.handlers, handlers, extra);
14084
+ if (tree.problems.length === 0) problems.push(...workflowProblems(declared, tree, handlers, sources, backendFacts, model));
14085
+ const selectedFindings = tree.problems.length === 0 ? selectedProblems(declared, tree, handlers, sources, backendFacts, contract?.pages) : [];
13891
14086
  problems.push(...selectedFindings.filter((one) => !one.advisory));
13892
14087
  warnings.push(...selectedFindings.filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one));
13893
14088
  if (problems.length > 0) return { files: {}, problems, contract, warnings };
@@ -14941,7 +15136,7 @@ function workflowProblems(elements, tree, handlers = {}, sources = {}, backendHa
14941
15136
  const say = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message: `${type} "${props.id ?? uid}" ${message}` });
14942
15137
  const readsFrom = (source, pointer2, what) => {
14943
15138
  if (!stated(source)) say("part_source_shape", pointer2, `names no source; a ${type} reads ${what} from a reader \u2014 a ${either3(WORKFLOW_READERS)} handler, or a data.sources entry`);
14944
- else if (!reader(source)) say("part_source_shape", pointer2, `reads "${source}", which ${roleOf(source) === "custom" || roleOf(source) === void 0 ? `is no reader (a custom handler is one only when its backend.handlers verbs are all query, get or aggregate${Array.isArray(verbsOf(source)) ? `; it states ${JSON.stringify(verbsOf(source))}` : ""})` : `has role ${roleOf(source)}`}; a ${type} reads ${what} from a list, inbox, history or summary handler, a data.sources entry, or such a custom handler, and never calls a writer`);
15139
+ else if (!reader(source)) say("part_source_shape", pointer2, `reads "${source}", which ${roleOf(source) === "custom" || roleOf(source) === void 0 ? `is no reader (a custom handler is one only when its verbs \u2014 stated in backend.handlers, else read off handlers/${source}.py, a guess you settle by stating them \u2014 are all query, get or aggregate${Array.isArray(verbsOf(source)) ? `; ${backendHandlers?.[source]?.inferred === true ? "its file reads as" : "it states"} ${JSON.stringify(verbsOf(source))}` : ""})` : `has role ${roleOf(source)}`}; a ${type} reads ${what} from a list, inbox, history or summary handler, a data.sources entry, or such a custom handler, and never calls a writer`);
14945
15140
  };
14946
15141
  if (type !== "split_pane" && typed.bind === "selected" && !bindReaches(uid)) {
14947
15142
  say("part_bind", `${at}/props/props/bind`, `binds the selected row where no row is ever picked for it: put it at the page root, in a root split_pane or panel, or inside a form or detail with bind: "selected"`);
@@ -15024,6 +15219,21 @@ function workflowProblems(elements, tree, handlers = {}, sources = {}, backendHa
15024
15219
  return problems;
15025
15220
  }
15026
15221
  var ROW_READERS = ["list", "inbox", "history", "summary"];
15222
+ function inferredBackendHandlers(stated2, handlers = {}, extra = {}) {
15223
+ const out = isPlainObject6(stated2) ? { ...stated2 } : {};
15224
+ for (const [name, row] of Object.entries(isPlainObject6(handlers) ? handlers : {})) {
15225
+ if (row?.role !== "custom") continue;
15226
+ if (isPlainObject6(out[name]) && Array.isArray(out[name].verbs)) continue;
15227
+ const source = extra?.[`handlers/${name}.py`];
15228
+ if (typeof source !== "string") continue;
15229
+ const verbs = handlerVerbs(source);
15230
+ const hidden = !verbs.some((verb) => INFER_WRITES.includes(verb)) && WRITE_ANYWHERE.test(source);
15231
+ out[name] = { ...isPlainObject6(out[name]) ? out[name] : {}, verbs: hidden ? [...verbs, "update"] : verbs, inferred: true };
15232
+ }
15233
+ return out;
15234
+ }
15235
+ var INFER_WRITES = ["insert", "update", "delete"];
15236
+ var WRITE_ANYWHERE = /\.\s*(?:insert|update|delete)\s*\(/;
15027
15237
  var READ_VERBS = ["query", "get", "aggregate"];
15028
15238
  var ROOT_HOLDERS = ["panel", "split_pane"];
15029
15239
  var BOUND_TYPES = ["form", "detail"];
@@ -15115,7 +15325,7 @@ function selectedProblems(elements, tree, handlers = {}, sources = {}, backendHa
15115
15325
  "bind_source",
15116
15326
  uid,
15117
15327
  "/props/source",
15118
- `${element.type} "${nameOf5(uid)}" binds the selected row and reads it from "${source}", ${role !== void 0 && role !== "custom" ? `a ${role} handler` : "a custom handler whose backend.handlers verbs are not all reads"}; a pick would call a writer, so the runtime never calls it and draws an error \u2014 read through a list, inbox, history or summary handler, a data.sources entry, or a custom handler whose verbs are all query, get or aggregate`
15328
+ `${element.type} "${nameOf5(uid)}" binds the selected row and reads it from "${source}", ${role !== void 0 && role !== "custom" ? `a ${role} handler` : `a custom handler whose verbs (${backendHandlers?.[source]?.inferred === true ? `read off handlers/${source}.py \u2014 state them in backend.handlers if that guess is wrong` : "stated in backend.handlers"}) are not all reads`}; a pick would call a writer, so the runtime never calls it and draws an error \u2014 read through a list, inbox, history or summary handler, a data.sources entry, or a custom handler whose verbs are all query, get or aggregate`
15119
15329
  );
15120
15330
  } else if (named5(props.entity) && named5(tableOfSource(source, handlers, sources)) && tableOfSource(source, handlers, sources) !== props.entity) {
15121
15331
  say("bind_source", uid, "/props/source", `${element.type} "${nameOf5(uid)}" is over "${props.entity}" and reads the selected row from "${source}", whose rows are "${tableOfSource(source, handlers, sources)}"; it would show (and a Save would post) another table's row \u2014 read the row from a reader over "${props.entity}"`);
@@ -19402,7 +19612,7 @@ function interpret(status, body, path3, contract) {
19402
19612
  }
19403
19613
  function fromRefusal(status, detail, contract) {
19404
19614
  const refused = `the push would be refused (${String(status)})`;
19405
- const tables = tablesOf(contract);
19615
+ const tables = tablesOf2(contract);
19406
19616
  const conflicts = conflictTables(detail);
19407
19617
  if (conflicts !== void 0) {
19408
19618
  return conflicts.map(([table, owners]) => {
@@ -19486,7 +19696,7 @@ async function stage(dir, scratch, files) {
19486
19696
  }
19487
19697
  return null;
19488
19698
  }
19489
- function tablesOf(contract) {
19699
+ function tablesOf2(contract) {
19490
19700
  const out = /* @__PURE__ */ new Map();
19491
19701
  const app = isObj3(contract) && isObj3(contract["app"]) ? contract["app"] : {};
19492
19702
  const model = isObj3(app["data_model"]) ? app["data_model"] : {};
@@ -22876,6 +23086,10 @@ function partCalls2(element) {
22876
23086
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
22877
23087
  if (action && typeof action === "object" && named6(action.call)) out.push(action.call);
22878
23088
  }
23089
+ const bulk = typed.selection && typeof typed.selection === "object" ? typed.selection.bulk_actions : void 0;
23090
+ for (const action of Array.isArray(bulk) ? bulk : []) {
23091
+ if (action && typeof action === "object" && named6(action.call)) out.push(action.call);
23092
+ }
22879
23093
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
22880
23094
  if (column && typeof column === "object" && column.cell === "toggle" && named6(column.call)) out.push(column.call);
22881
23095
  }
@@ -23584,9 +23798,7 @@ function backendSection2(app, manifest, pages, elements) {
23584
23798
  signature_ok: signatureOk2(source),
23585
23799
  reads_ctx_role: CTX_ROLE2.test(source),
23586
23800
  tables_touched: [...new Set([...source.matchAll(TABLE_CALL2)].map((match) => match[2]))].sort(),
23587
- verbs: DATA_VERBS2.filter(
23588
- (verb) => TABLE_ONLY_VERBS2.has(verb) ? callsOnTable2(source, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(source)
23589
- ),
23801
+ verbs: handlerVerbs2(source),
23590
23802
  referenced_by_page: isReferenced3(handler.name, scriptText) || attributed.has(handler.name)
23591
23803
  };
23592
23804
  }
@@ -23605,6 +23817,10 @@ function backendSection2(app, manifest, pages, elements) {
23605
23817
  invariants: invariants2(model, roles, handlers)
23606
23818
  };
23607
23819
  }
23820
+ function handlerVerbs2(source) {
23821
+ const text3 = typeof source === "string" ? source : "";
23822
+ return DATA_VERBS2.filter((verb) => TABLE_ONLY_VERBS2.has(verb) ? callsOnTable2(text3, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(text3));
23823
+ }
23608
23824
  function callsOnTable2(source, verb) {
23609
23825
  const holders = [TABLE_INLINE2];
23610
23826
  for (const match of source.matchAll(TABLE_LOCAL2)) holders.push(match[1]);
@@ -24356,6 +24572,9 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
24356
24572
  for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
24357
24573
  if (isPlainObject11(action) && named7(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
24358
24574
  }
24575
+ for (const action of isPlainObject11(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
24576
+ if (isPlainObject11(action) && named7(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["ids"] });
24577
+ }
24359
24578
  for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
24360
24579
  if (isPlainObject11(column) && column.cell === "toggle" && named7(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
24361
24580
  }
@@ -25451,6 +25670,181 @@ function railReachProblems2(contract) {
25451
25670
  }
25452
25671
  return out;
25453
25672
  }
25673
+ var STATE_NAMES2 = ["folder", "status", "stage", "state", "queue"];
25674
+ var CATCH_ALL2 = /* @__PURE__ */ new Set(["", "all", "any", "*", "everything"]);
25675
+ var PICK_KINDS2 = /* @__PURE__ */ new Set(["select", "multi"]);
25676
+ var ROW_TYPES2 = /* @__PURE__ */ new Set(["data_grid", "list", "queue"]);
25677
+ var NOT_COUNTS2 = ["sum", "avg", "avg_days", "count_events"];
25678
+ var VALUES_FIX2 = "state `app.shell` with `nav: rail` and one entry per value, counts from a `summary` handler; look.md rule 1";
25679
+ var PAGES_FIX2 = "state `app.shell` with `nav: rail`; with no `rail.entries` the rail lists the pages; look.md rule 1";
25680
+ var COUNTS_FIX2 = "state `app.shell` with `nav: rail` and one entry per state, each counting a measure of that summary (`pages[].nav.counts.source`); look.md rule 1";
25681
+ function isAppContract2(contract) {
25682
+ const kind = contract?.app?.kind;
25683
+ return kind !== "dashboard" && (kind === "app" || isPlainObject13(contract?.data?.app));
25684
+ }
25685
+ function tablesOf3(contract) {
25686
+ const tables = contract?.app?.data_model?.tables;
25687
+ return isPlainObject13(tables) ? tables : {};
25688
+ }
25689
+ function enumOf2(column) {
25690
+ return Array.isArray(column?.enum) ? column.enum.filter((one) => typeof one === "string") : [];
25691
+ }
25692
+ function stateColumn2(contract, table) {
25693
+ const columns = isPlainObject13(tablesOf3(contract)[table]?.columns) ? tablesOf3(contract)[table].columns : {};
25694
+ return Object.keys(columns).find((name) => STATE_NAMES2.includes(name) && enumOf2(columns[name]).length >= 3) ?? null;
25695
+ }
25696
+ function realValues2(param) {
25697
+ return Array.isArray(param?.values) ? param.values.map(String).filter((value) => !CATCH_ALL2.has(value.toLowerCase())) : [];
25698
+ }
25699
+ function ownPages2(contract) {
25700
+ return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true && named9(page?.path));
25701
+ }
25702
+ function folderParams2(contract) {
25703
+ const enums = [];
25704
+ for (const [table, spec] of Object.entries(tablesOf3(contract))) {
25705
+ const columns = isPlainObject13(spec?.columns) ? spec.columns : {};
25706
+ for (const name of STATE_NAMES2) if (enumOf2(columns[name]).length >= 3) enums.push({ where: `${table}.${name}`, values: new Set(enumOf2(columns[name])) });
25707
+ }
25708
+ const out = [];
25709
+ for (const page of ownPages2(contract)) {
25710
+ for (const param of Array.isArray(page.params) ? page.params : []) {
25711
+ if (!PICK_KINDS2.has(param?.kind) || !named9(param?.name)) continue;
25712
+ const values = realValues2(param);
25713
+ if (STATE_NAMES2.includes(param.name) && (values.length >= 3 || !Array.isArray(param.values))) {
25714
+ out.push({ page, param, values, why: `named "${param.name}"` });
25715
+ continue;
25716
+ }
25717
+ if (values.length < 3) continue;
25718
+ const match = enums.find((one) => values.every((value) => one.values.has(value)));
25719
+ if (match !== void 0) out.push({ page, param, values, why: `its values are the enum of ${match.where}` });
25720
+ }
25721
+ }
25722
+ return out;
25723
+ }
25724
+ function tableOf3(contract, element) {
25725
+ const props = isPlainObject13(element?.props) ? element.props : {};
25726
+ if (named9(props.entity)) return props.entity;
25727
+ const source = props.source;
25728
+ if (!named9(source)) return null;
25729
+ const handler = contract?.data?.app?.handlers?.[source];
25730
+ if (isPlainObject13(handler) && named9(handler.table)) return handler.table;
25731
+ const read = contract?.data?.sources?.[source];
25732
+ if (isPlainObject13(read?.from) && named9(read.from.table)) return read.from.table;
25733
+ return null;
25734
+ }
25735
+ function listingOn2(contract, page, test) {
25736
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
25737
+ for (const [uid, element] of Object.entries(elements)) {
25738
+ if (element?.page !== page.path || !ROW_TYPES2.has(element.type)) continue;
25739
+ const table = tableOf3(contract, element);
25740
+ if (table !== null && test(table)) return [uid, element, table];
25741
+ }
25742
+ return null;
25743
+ }
25744
+ function stateCountColumn2(spec) {
25745
+ if (!isPlainObject13(spec) || NOT_COUNTS2.some((key) => spec[key] !== void 0)) return null;
25746
+ const where2 = spec.count === true ? isPlainObject13(spec.where) ? spec.where : {} : spec.count === void 0 ? spec : null;
25747
+ if (where2 === null) return null;
25748
+ return Object.keys(where2).map((key) => key.split("__")[0]).find((column) => STATE_NAMES2.includes(column)) ?? null;
25749
+ }
25750
+ function stateCounts2(handler) {
25751
+ const out = {};
25752
+ for (const [measure, spec] of Object.entries(isPlainObject13(handler?.measures) ? handler.measures : {})) {
25753
+ const column = stateCountColumn2(spec);
25754
+ if (column !== null) out[measure] = column;
25755
+ }
25756
+ return out;
25757
+ }
25758
+ function pageReads2(contract, page, name) {
25759
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
25760
+ if (page?.nav?.counts?.source === name) return true;
25761
+ return Object.values(elements).some((element) => element?.page === page.path && element.props?.source === name);
25762
+ }
25763
+ function shellExpectedProblems2(contract) {
25764
+ if (!isAppContract2(contract)) return [];
25765
+ const shell = isPlainObject13(contract?.app?.shell) ? contract.app.shell : {};
25766
+ if (shell.nav === "rail") return [];
25767
+ const out = [];
25768
+ const say = (page, param, where2, message, fix) => out.push({ rule: "L8", code: "shell-expected", page, param, where: where2, message, fix });
25769
+ const has = shell.nav === void 0 || shell.nav === null ? "states no app.shell" : `states app.shell.nav ${JSON.stringify(shell.nav)}`;
25770
+ const pages = ownPages2(contract);
25771
+ if (pages.length > 4) say(null, null, "pages", `the app has ${pages.length} pages and ${has}, so its screens are a row of tabs too long to scan; an app this size is a rail`, PAGES_FIX2);
25772
+ const named_ = /* @__PURE__ */ new Set();
25773
+ for (const { page, param, values, why } of folderParams2(contract)) {
25774
+ named_.add(page.path);
25775
+ const listed = values.length > 0 ? ` with ${values.length} values (${values.map((one) => JSON.stringify(one)).join(", ")})` : " (its values from elsewhere)";
25776
+ say(page.path, param.name, `${page.path} params.${param.name}`, `${page.path} declares ${param.kind} param "${param.name}" (${why})${listed} and the app ${has}, so its folders are a dropdown with no counts; they are the rail's entries`, VALUES_FIX2);
25777
+ }
25778
+ const handlers = isPlainObject13(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25779
+ for (const [name, handler] of Object.entries(handlers)) {
25780
+ if (handler?.role !== "summary" || !named9(handler.table)) continue;
25781
+ const column = stateColumn2(contract, handler.table);
25782
+ const counts = Object.fromEntries(Object.entries(stateCounts2(handler)).filter(([, by]) => by === column));
25783
+ if (column === null || Object.keys(counts).length < 3) continue;
25784
+ for (const page of pages) {
25785
+ if (named_.has(page.path) || !pageReads2(contract, page, name)) continue;
25786
+ const listing = listingOn2(contract, page, (table) => table === handler.table);
25787
+ if (listing === null) continue;
25788
+ const columns = [column];
25789
+ say(
25790
+ page.path,
25791
+ null,
25792
+ `${page.path} ${name}`,
25793
+ `${page.path} reads the summary "${name}", whose measures count ${handler.table} by ${columns.join(", ")} (${Object.keys(counts).map((one) => JSON.stringify(one)).join(", ")}), beside ${nameOf4(listing[0], listing[1])} listing ${handler.table}, and the app ${has}: those counts are folders, and folders are the rail's entries`,
25794
+ COUNTS_FIX2
25795
+ );
25796
+ }
25797
+ }
25798
+ return out;
25799
+ }
25800
+ function kpiStripWorkflowProblems2(contract) {
25801
+ if (!isAppContract2(contract)) return [];
25802
+ const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
25803
+ const handlers = isPlainObject13(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
25804
+ const folderPages = /* @__PURE__ */ new Map();
25805
+ for (const { page, param } of folderParams2(contract)) if (!folderPages.has(page.path)) folderPages.set(page.path, param.name);
25806
+ const out = [];
25807
+ for (const page of ownPages2(contract)) {
25808
+ const listing = listingOn2(contract, page, (table) => stateColumn2(contract, table) !== null);
25809
+ const workflow = listing !== null ? `${nameOf4(listing[0], listing[1])} lists ${listing[2]}, whose ${stateColumn2(contract, listing[2])} column is a set of states` : folderPages.has(page.path) ? `the page declares the folder-like param "${folderPages.get(page.path)}"` : null;
25810
+ if (workflow === null) continue;
25811
+ const banded = bandsOf3(contract, page);
25812
+ const inBand = new Set(banded.flatMap((band) => band.roots));
25813
+ const loose = rootsOf3(contract, page).filter((uid) => !inBand.has(uid));
25814
+ for (const { section, roots } of [...banded, { section: null, roots: loose }]) {
25815
+ const stats = roots.filter((uid) => elements[uid]?.type === "stat");
25816
+ if (stats.length < 2) continue;
25817
+ const bySource = /* @__PURE__ */ new Map();
25818
+ for (const uid of stats) {
25819
+ const source = elements[uid]?.props?.source;
25820
+ if (!named9(source) || handlers[source]?.role !== "summary") continue;
25821
+ if (!bySource.has(source)) bySource.set(source, []);
25822
+ bySource.get(source).push(uid);
25823
+ }
25824
+ for (const [source, group] of bySource) {
25825
+ if (group.length < 2) continue;
25826
+ const measures = handlers[source].measures;
25827
+ const known = isPlainObject13(measures) && group.every((uid) => Object.prototype.hasOwnProperty.call(measures, elements[uid]?.props?.measure));
25828
+ const counts = stateCounts2(handlers[source]);
25829
+ if (known ? !group.every((uid) => counts[elements[uid].props.measure] !== void 0) : group.length !== stats.length) continue;
25830
+ const where2 = section === null ? page.path : `band "${section.id}" on ${page.path}`;
25831
+ const shape = section?.layout === "kpi_strip" ? "is a kpi_strip of" : "holds a row of";
25832
+ const what = known ? `${group.length} counts of rows by ${[...new Set(group.map((uid) => counts[elements[uid].props.measure]))].join(", ")}` : `${group.length} counts`;
25833
+ out.push({
25834
+ rule: "L9",
25835
+ code: "kpi-strip-on-workflow",
25836
+ page: page.path,
25837
+ section: section?.id ?? null,
25838
+ where: where2,
25839
+ message: `${where2} ${shape} ${what} from the summary "${source}", and ${workflow}: folder or state counts belong on the rail, not in a KPI strip (workflow.md \xA78)`,
25840
+ fix: VALUES_FIX2
25841
+ });
25842
+ break;
25843
+ }
25844
+ }
25845
+ }
25846
+ return out;
25847
+ }
25454
25848
 
25455
25849
  // ../flows/appspace-builder/kit/contract/platformPalette.mjs
25456
25850
  var PLATFORM_CHART2 = Object.freeze([
@@ -25672,7 +26066,7 @@ function rgbText2(parts) {
25672
26066
 
25673
26067
  // ../flows/appspace-builder/kit/checks/rules/look.mjs
25674
26068
  var THEME_FILE8 = "ui/theme.css";
25675
- var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7"];
26069
+ var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"];
25676
26070
  function checkLook(app, contract, contractError) {
25677
26071
  const view = {
25678
26072
  ...app,
@@ -25701,6 +26095,9 @@ function checkLook(app, contract, contractError) {
25701
26095
  for (const found of railReachProblems2(contract)) {
25702
26096
  violations.push({ rule: "L7", where: found.where, message: found.message });
25703
26097
  }
26098
+ for (const found of [...shellExpectedProblems2(contract), ...kpiStripWorkflowProblems2(contract)]) {
26099
+ violations.push({ rule: found.rule, where: found.where, message: `${found.code}: ${found.message}; ${found.fix}` });
26100
+ }
25704
26101
  return { violations, skipped: [] };
25705
26102
  }
25706
26103
 
@@ -25885,7 +26282,7 @@ function checkUx(app, contract, contractError) {
25885
26282
  return out;
25886
26283
  };
25887
26284
  const nameOf5 = (uid) => said(elements[uid]?.props?.id) ?? said(elements[uid]?.id) ?? uid;
25888
- const tableOf2 = (el) => said(el?.props?.entity) ?? said(handlers[el?.props?.source]?.table);
26285
+ const tableOf4 = (el) => said(el?.props?.entity) ?? said(handlers[el?.props?.source]?.table);
25889
26286
  const violations = [];
25890
26287
  for (const uid of uids) {
25891
26288
  const el = elements[uid];
@@ -25895,7 +26292,7 @@ function checkUx(app, contract, contractError) {
25895
26292
  if (!table || handlers[el.props?.action]?.role === "save") continue;
25896
26293
  const listing = uids.find((other) => {
25897
26294
  const o = elements[other];
25898
- return o?.page === el.page && ROWS.has(o.type) && tableOf2(o) === table && !ancestors(other).some((up) => up.type === "detail");
26295
+ return o?.page === el.page && ROWS.has(o.type) && tableOf4(o) === table && !ancestors(other).some((up) => up.type === "detail");
25899
26296
  });
25900
26297
  if (!listing) continue;
25901
26298
  const label = said(el.props?.label) ?? "New";
@@ -25912,7 +26309,7 @@ function checkUx(app, contract, contractError) {
25912
26309
  const el = elements[uid];
25913
26310
  if (!ROWS.has(el?.type)) continue;
25914
26311
  if (ancestors(uid).some((up) => up.type === "detail")) continue;
25915
- const table = tableOf2(el);
26312
+ const table = tableOf4(el);
25916
26313
  if (!table || table.startsWith("_") || !isPlainObject14(tables[table]?.columns) || tables[table].singleton === true) continue;
25917
26314
  const columns = tables[table].columns;
25918
26315
  const pk = Object.keys(columns).find((c) => isPlainObject14(columns[c]) && columns[c].pk === true) ?? "id";
@@ -25953,7 +26350,7 @@ function checkUx(app, contract, contractError) {
25953
26350
  for (const uid of uids) {
25954
26351
  const el = elements[uid];
25955
26352
  if (!ROWS.has(el?.type) || el.type === "data_grid") continue;
25956
- const table = tableOf2(el);
26353
+ const table = tableOf4(el);
25957
26354
  const columns = isPlainObject14(tables[table]?.columns) ? tables[table].columns : {};
25958
26355
  const state = STATE_COLUMNS.find((c) => isPlainObject14(columns[c]) && Array.isArray(columns[c].enum));
25959
26356
  const seen = /* @__PURE__ */ new Map();
@@ -26269,6 +26666,11 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
26269
26666
  const asks = action.confirm === true || named10(action.confirm);
26270
26667
  add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
26271
26668
  }
26669
+ for (const action of isPlainObject15(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
26670
+ if (!isPlainObject15(action) || !named10(action.call)) continue;
26671
+ const asks = action.confirm === true || named10(action.confirm);
26672
+ add(action.call, { page, uid, id, what: `bulk action "${action.label ?? action.call}"`, asks });
26673
+ }
26272
26674
  break;
26273
26675
  case "action":
26274
26676
  case "detail": {
@@ -26562,7 +26964,7 @@ function kitFromDisk2() {
26562
26964
  }
26563
26965
  return kitOnDisk2;
26564
26966
  }
26565
- 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"]);
26967
+ var ADVISORY = /* @__PURE__ */ new Set(["L6", "L7", "L8", "L9", "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"]);
26566
26968
  function verify2({ app: appRef }, { extractContract: extractContract2 = extract2, kit } = {}) {
26567
26969
  const KIT = kit ?? kitFromDisk2();
26568
26970
  const app = loadApp2(resolve12(appRef.dir));
@@ -30891,7 +31293,7 @@ function auditedOf(model) {
30891
31293
  if (typeof tables !== "object" || tables === null) return [];
30892
31294
  return Object.keys(tables).filter((name) => tables[name]?.audit === true);
30893
31295
  }
30894
- function tablesOf2(model) {
31296
+ function tablesOf4(model) {
30895
31297
  const tables = model?.tables;
30896
31298
  if (typeof tables !== "object" || tables === null) return {};
30897
31299
  const out = {};
@@ -30929,7 +31331,7 @@ function toRemove(rows2, { stage: stage2, cleanUp, keep, wrote = true }) {
30929
31331
  return rows2.filter((row) => row.recent);
30930
31332
  }
30931
31333
  async function sweepTestRows(client, appId, opts) {
30932
- const tables = tablesOf2(opts.model);
31334
+ const tables = tablesOf4(opts.model);
30933
31335
  if (Object.keys(tables).length === 0) {
30934
31336
  return { checked: false, why: "the app has no tables", left: [], removed: [], errors: [], truncated: [] };
30935
31337
  }
@@ -31187,7 +31589,7 @@ function pushedContract(bundle, pages) {
31187
31589
  }
31188
31590
  return null;
31189
31591
  }
31190
- function isAppContract(contract) {
31592
+ function isAppContract3(contract) {
31191
31593
  return contract.app?.kind !== "dashboard" && typeof contract.data?.app === "object";
31192
31594
  }
31193
31595
  var COMPONENTS2 = /* @__PURE__ */ new Set(["form", "list", "detail", "queue", "action", "filter", "stat", "chart", "custom", "widget"]);
@@ -31216,7 +31618,7 @@ function sectionOf3(element) {
31216
31618
  return typeof raw === "string" && raw !== "" ? raw : void 0;
31217
31619
  }
31218
31620
  function contractFindings(contract) {
31219
- if (!isAppContract(contract)) return [];
31621
+ if (!isAppContract3(contract)) return [];
31220
31622
  const pages = contract.pages ?? [];
31221
31623
  const elements = contract.elements ?? {};
31222
31624
  const out = [];
@@ -31281,7 +31683,7 @@ function contractFindings(contract) {
31281
31683
  return out;
31282
31684
  }
31283
31685
  function auditFindings(contract) {
31284
- if (!isAppContract(contract)) return [];
31686
+ if (!isAppContract3(contract)) return [];
31285
31687
  const tables = contract.app?.data_model?.tables ?? {};
31286
31688
  const handlers = contract.data?.app?.handlers ?? {};
31287
31689
  const elements = contract.elements ?? {};
@@ -31410,7 +31812,7 @@ function renderFindings(runs, contract, seed) {
31410
31812
  return out;
31411
31813
  }
31412
31814
  function chartFindings(runs, contract, seed) {
31413
- if (contract === null || !isAppContract(contract)) return [];
31815
+ if (contract === null || !isAppContract3(contract)) return [];
31414
31816
  const handlers = contract.data?.app?.handlers ?? {};
31415
31817
  const out = [];
31416
31818
  const said2 = /* @__PURE__ */ new Set();
@@ -31506,7 +31908,7 @@ function mapDecisions(handler) {
31506
31908
  return Object.keys(decision).sort();
31507
31909
  }
31508
31910
  function refusalFindings(runs, contract) {
31509
- if (contract === null || !isAppContract(contract)) return [];
31911
+ if (contract === null || !isAppContract3(contract)) return [];
31510
31912
  const handlers = contract.data?.app?.handlers ?? {};
31511
31913
  const byButton = /* @__PURE__ */ new Map();
31512
31914
  for (const run of runs) {
@@ -31902,7 +32304,7 @@ function partsSummary(contract, bundle, handlerCode) {
31902
32304
  custom_handler_lines: customHandlerLines,
31903
32305
  custom_share: components === 0 ? 0 : Math.round(customUi / components * 100)
31904
32306
  };
31905
- if (isAppContract(contract)) {
32307
+ if (isAppContract3(contract)) {
31906
32308
  const files = transformFiles2(contract, handlerCode);
31907
32309
  let lines = 0;
31908
32310
  for (const text3 of files.values()) if (text3 !== void 0) lines += nonBlankLinesOf(text3);
@@ -31928,7 +32330,7 @@ function pushedFiles(bundle, handlerCode) {
31928
32330
  };
31929
32331
  }
31930
32332
  function payloadFindings(contract, bundle, handlerCode) {
31931
- if (contract === null || !isAppContract(contract)) return [];
32333
+ if (contract === null || !isAppContract3(contract)) return [];
31932
32334
  return DECLARES.unproducedPayloadKeys(contract, pushedFiles(bundle, handlerCode)).map((found) => ({
31933
32335
  code: "payload-key-unproduced",
31934
32336
  where: `${found.pages.join(", ")} \u2192 ${found.handler}`,
@@ -31938,7 +32340,7 @@ function payloadFindings(contract, bundle, handlerCode) {
31938
32340
  }));
31939
32341
  }
31940
32342
  function leverFindings(contract, bundle, handlerCode) {
31941
- if (contract === null || !isAppContract(contract)) return [];
32343
+ if (contract === null || !isAppContract3(contract)) return [];
31942
32344
  const files = pushedFiles(bundle, handlerCode);
31943
32345
  const finding = (code) => (found) => ({
31944
32346
  code,
@@ -31953,7 +32355,7 @@ function leverFindings(contract, bundle, handlerCode) {
31953
32355
  ];
31954
32356
  }
31955
32357
  function workflowFindings(contract, bundle, handlerCode) {
31956
- if (contract === null || !isAppContract(contract)) return [];
32358
+ if (contract === null || !isAppContract3(contract)) return [];
31957
32359
  const files = pushedFiles(bundle, handlerCode);
31958
32360
  return [
31959
32361
  // A role that opens the app on a 404: counted, like the unreachable page
@@ -32012,7 +32414,7 @@ function shown(value) {
32012
32414
  return typeof value === "number" || typeof value === "boolean" || typeof value === "string" ? String(value) : JSON.stringify(value);
32013
32415
  }
32014
32416
  function leverStaticFindings(runs, contract, unread = /* @__PURE__ */ new Set()) {
32015
- if (contract === null || !isAppContract(contract)) return [];
32417
+ if (contract === null || !isAppContract3(contract)) return [];
32016
32418
  const sources = contract.data?.sources ?? {};
32017
32419
  const out = [];
32018
32420
  const seen = /* @__PURE__ */ new Set();
@@ -32110,7 +32512,7 @@ function declaresOf(element) {
32110
32512
  return { calls, shows };
32111
32513
  }
32112
32514
  function customDeclaresFindings(contract, bundle, runs) {
32113
- if (contract === null || !isAppContract(contract)) return [];
32515
+ if (contract === null || !isAppContract3(contract)) return [];
32114
32516
  const out = [];
32115
32517
  const said2 = /* @__PURE__ */ new Set();
32116
32518
  const undeclared = (uid, page, module, handler, problem5, fix) => {
@@ -32163,7 +32565,7 @@ function customDeclaresFindings(contract, bundle, runs) {
32163
32565
  return out;
32164
32566
  }
32165
32567
  function roleFindings(contract, held) {
32166
- if (held.own === null || !isAppContract(contract)) return [];
32568
+ if (held.own === null || !isAppContract3(contract)) return [];
32167
32569
  const elements = contract.elements ?? {};
32168
32570
  const parentOf2 = /* @__PURE__ */ new Map();
32169
32571
  for (const [uid, element] of Object.entries(elements)) {
@@ -32208,7 +32610,7 @@ function roleFindings(contract, held) {
32208
32610
  ];
32209
32611
  }
32210
32612
  function cardTitleFindings(contract) {
32211
- if (!isAppContract(contract)) return [];
32613
+ if (!isAppContract3(contract)) return [];
32212
32614
  return LOOK.untitledCards(contract).map((found) => ({
32213
32615
  code: "card-untitled",
32214
32616
  where: `${found.page} \u2192 ${found.uid}`,
@@ -32217,8 +32619,19 @@ function cardTitleFindings(contract) {
32217
32619
  see: SEE.labels
32218
32620
  }));
32219
32621
  }
32622
+ function shellFindings(contract) {
32623
+ if (contract === null) return [];
32624
+ return [...LOOK.shellExpectedProblems(contract), ...LOOK.kpiStripWorkflowProblems(contract)].map((found) => ({
32625
+ code: found.code,
32626
+ where: found.where,
32627
+ message: found.message,
32628
+ fix: `${found.fix.charAt(0).toUpperCase()}${found.fix.slice(1)}.`,
32629
+ see: `App kit ${found.rule} (contract/look.mjs)`,
32630
+ severity: "warning"
32631
+ }));
32632
+ }
32220
32633
  function uxFindings(contract) {
32221
- if (contract === null || !isAppContract(contract)) return [];
32634
+ if (contract === null || !isAppContract3(contract)) return [];
32222
32635
  const codes = {
32223
32636
  "A3.23": "create-form-inline",
32224
32637
  "A3.24": "row-no-detail",
@@ -32263,7 +32676,8 @@ function collectFindings(input) {
32263
32676
  ...input.declared === null ? [] : unreachedFindings(input.declared, input.cases, input.contract, input.runs),
32264
32677
  // Last: said and never counted, so they never push a counted finding down the list.
32265
32678
  ...workflow.filter((one) => one.severity === "warning"),
32266
- ...uxFindings(input.contract)
32679
+ ...uxFindings(input.contract),
32680
+ ...shellFindings(input.contract)
32267
32681
  ];
32268
32682
  }
32269
32683
  function formatFindings(findings) {
@@ -32683,7 +33097,7 @@ function shownValue(value) {
32683
33097
  return typeof value === "number" || typeof value === "boolean" || typeof value === "string" ? String(value) : JSON.stringify(value);
32684
33098
  }
32685
33099
  function paramsOf3(contract, page) {
32686
- if (contract === null || !isAppContract(contract)) return [];
33100
+ if (contract === null || !isAppContract3(contract)) return [];
32687
33101
  const found = (contract?.pages ?? []).find((one) => one.path === page && one.owned !== true);
32688
33102
  return Array.isArray(found?.params) ? found.params : [];
32689
33103
  }
@@ -32735,7 +33149,7 @@ function leversNotChecked(skipUi, runs, contract) {
32735
33149
  }
32736
33150
  }
32737
33151
  const loaded = new Set(runs.map((run) => `ui/${run.entry}`));
32738
- if (contract === null || !isAppContract(contract)) return out;
33152
+ if (contract === null || !isAppContract3(contract)) return out;
32739
33153
  for (const page of contract.pages ?? []) {
32740
33154
  if (page.owned === true || !Array.isArray(page.params) || page.params.length === 0) continue;
32741
33155
  if (judged.has(page.path) || !loaded.has(page.path)) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-appspace-cli",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Datarails App Space CLI — build, try, push and publish App Space apps",
5
5
  "type": "module",
6
6
  "engines": {