@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.
- package/dist/cli.js +453 -39
- 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.
|
|
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-
|
|
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:
|
|
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
|
-
|
|
13890
|
-
|
|
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
|
|
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` :
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
|
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) &&
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 (!
|
|
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 (!
|
|
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 || !
|
|
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 || !
|
|
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 (
|
|
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 || !
|
|
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 || !
|
|
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 || !
|
|
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 || !
|
|
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 || !
|
|
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 || !
|
|
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 (!
|
|
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 || !
|
|
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 || !
|
|
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 || !
|
|
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;
|