@datarails-ai/dr-appspace-cli 0.12.29 → 0.12.30

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 +389 -58
  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.29",
16
+ version: "0.12.30",
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-25T11:42:55.355Z";
60
+ var BUILD_TIME = "2026-10-25T11:56:43.845Z";
61
61
  function formatBuildTime(iso) {
62
62
  if (iso === "") return "";
63
63
  const d = new Date(iso);
@@ -6327,15 +6327,15 @@ function inputTypeOf(node) {
6327
6327
  }
6328
6328
  function optionsOf(node, text3, tree) {
6329
6329
  const options = [];
6330
- const walk4 = (index) => {
6330
+ const walk5 = (index) => {
6331
6331
  for (const child of tree.nodes[index].children) {
6332
6332
  const candidate = tree.nodes[child];
6333
6333
  if (candidate.tag === "option") {
6334
6334
  if (!("disabled" in candidate.attrs)) options.push(candidate.attrs.value ?? textOf(text3, candidate));
6335
- } else walk4(child);
6335
+ } else walk5(child);
6336
6336
  }
6337
6337
  };
6338
- walk4(node.index);
6338
+ walk5(node.index);
6339
6339
  return options;
6340
6340
  }
6341
6341
  function splitList(value) {
@@ -6354,14 +6354,14 @@ function textOf(text3, node) {
6354
6354
  function slotsOf(node, tree) {
6355
6355
  if (!(WIDGET_ATTR in node.attrs)) return {};
6356
6356
  const slots = {};
6357
- const walk4 = (index) => {
6357
+ const walk5 = (index) => {
6358
6358
  for (const child of tree.nodes[index].children) {
6359
6359
  const kind = classify(tree.nodes[child]);
6360
6360
  if (kind?.type === "slot") slots[kind.role] = kind.role === "body" ? classesOf(tree.nodes[child])[0] ?? true : true;
6361
- else walk4(child);
6361
+ else walk5(child);
6362
6362
  }
6363
6363
  };
6364
- walk4(node.index);
6364
+ walk5(node.index);
6365
6365
  return slots;
6366
6366
  }
6367
6367
  function bindingOf(entry, node, spec) {
@@ -7082,15 +7082,15 @@ function hasCycle(tables) {
7082
7082
  }
7083
7083
  }
7084
7084
  const state = /* @__PURE__ */ new Map();
7085
- const walk4 = (name) => {
7085
+ const walk5 = (name) => {
7086
7086
  if (state.get(name) === "done") return false;
7087
7087
  if (state.get(name) === "open") return true;
7088
7088
  state.set(name, "open");
7089
- for (const next of graph.get(name) ?? []) if (walk4(next)) return true;
7089
+ for (const next of graph.get(name) ?? []) if (walk5(next)) return true;
7090
7090
  state.set(name, "done");
7091
7091
  return false;
7092
7092
  };
7093
- return [...graph.keys()].some((name) => walk4(name));
7093
+ return [...graph.keys()].some((name) => walk5(name));
7094
7094
  }
7095
7095
  function byProblem(a, b) {
7096
7096
  return natural3(a.code, b.code) || natural3(a.where ?? "", b.where ?? "") || natural3(a.message, b.message);
@@ -7769,15 +7769,16 @@ var contract_schema_default = {
7769
7769
  sources: {
7770
7770
  $comment: "Present only for kind: app, and only when the contract declares any. A source some component names compiles to handlers/<name>.py; one only depended on is served in-process by the module that depends on it.",
7771
7771
  type: "object",
7772
- description: "one row per source: where its rows come from (from), the shape it answers in, the transforms/<name>.py that reshapes them, and the keys its read is narrowed by.",
7772
+ description: "one row per source: where its rows come from (from), the shape it answers in, the transforms/<name>.py that reshapes them or the expr the platform evaluates over them, and the keys its read is narrowed by.",
7773
7773
  additionalProperties: {
7774
7774
  type: "object",
7775
7775
  required: ["from"],
7776
7776
  properties: {
7777
7777
  from: {
7778
- description: 'one of four spellings: {"financeos": {...}} (the dashboard SPEC source vocabulary), {"table": "<app table>"}, {"call": "<dotted method>", "args": {...}}, or ["<source>", ...] to compose others',
7778
+ description: 'one of four spellings: {"financeos": {...}} (the dashboard SPEC source vocabulary), {"table": "<app table>"}, {"call": "<dotted method>", "args": {...}}, or ["<source>", ...] to compose others (with a transform, or with an expr the platform evaluates)',
7779
7779
  oneOf: [{ type: "object" }, { type: "array", items: { type: "string" } }]
7780
7780
  },
7781
+ expr: { type: "string", description: "OPTIONAL, beside from as a list and no transform (lineage tier 3, docs/appspace/design-computed-card-lineage.md): the source's figure as a formula over its from sources' figures, which the PLATFORM evaluates, so the figure is made of Datarails reads and never computed by the app. It may use numbers, the operands' names, + - * / and parentheses (/ is a safe divide: a zero or missing divisor is no figure, and any missing figure makes the result none), and abs(x), min(a, b, ...), max(a, b, ...), round(x) or round(x, decimals 0-6, half away from zero), and if(comparison, then, else), the one place a comparison (< <= > >= == !=) may stand. Shape kpi (the value, and the compare where every operand carries one), series (point by point: each series operand draws one line, and a kpi operand is one figure at every point) or breakdown (member by member: every breakdown operand states top: null, and a kpi operand is one figure for every member). Its figure comes from an operand on every path: each branch of if() and each argument of min()/max() reads an operand, so a number alone (870000, or a fallback 0) is never the figure; write actual / budget, not if(budget > 0, actual / budget, 0). Every operand is a FinanceOS table read or metric (from.financeos) or another expr source over them - never a transform's output, a composed source, a call or an app table (a KPI belongs in the Datarails semantic layer, not in app data) - and every operand is used; compare is not stated here (it is the formula over the operands' compares). The compiler refuses the rest (source_expr)." },
7781
7782
  shape: { enum: ["rows", "series", "kpi", "breakdown", "table", "ohlc", "scatter", "narrative"] },
7782
7783
  transform: { type: "string", pattern: "^transforms/[a-z][a-z0-9_]*\\.py$" },
7783
7784
  period: { type: ["string", "object", "null"], description: `OPTIONAL. The window the source reads. "all": every period, whatever the page sends, honoured or not (not a metric). "ytd", "last12" or "quarter" (a FinanceOS source, table or metric): a rolling window resolved at each request as the page's period presets are, so it ends today; a source that honours period reads the page's window when the page sends one, and this one when it sends none. An object {start, end, grain}: a fixed window, read where the source does not honour period. Absent: year to date, or the page's window where the source honours period.` },
@@ -8500,16 +8501,16 @@ function payloadProducers(contract, { moduleSource = () => void 0, pageSource =
8500
8501
  };
8501
8502
  const fieldsUnder = (uid) => {
8502
8503
  const fields = [];
8503
- const walk4 = (at) => {
8504
+ const walk5 = (at) => {
8504
8505
  for (const child of children.get(at) ?? []) {
8505
8506
  const element = elements[child];
8506
8507
  const field = fieldOf(element);
8507
8508
  if (field !== null && !fields.includes(field)) fields.push(field);
8508
8509
  if (element?.type === "form" || element?.type === "dialog") continue;
8509
- walk4(child);
8510
+ walk5(child);
8510
8511
  }
8511
8512
  };
8512
- walk4(uid);
8513
+ walk5(uid);
8513
8514
  return fields;
8514
8515
  };
8515
8516
  for (const uid of Object.keys(elements).sort()) {
@@ -9736,20 +9737,20 @@ function untitledCards(contract) {
9736
9737
  if (page?.owned === true) continue;
9737
9738
  const seen = /* @__PURE__ */ new Set();
9738
9739
  const drawn2 = [];
9739
- const walk4 = (uids) => {
9740
+ const walk5 = (uids) => {
9740
9741
  for (const uid of uids) {
9741
9742
  const element = elements[uid];
9742
9743
  if (element === void 0 || seen.has(uid)) continue;
9743
9744
  seen.add(uid);
9744
9745
  if (LAYOUT.has(element.type ?? "")) {
9745
9746
  const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
9746
- walk4(children);
9747
+ walk5(children);
9747
9748
  } else {
9748
9749
  drawn2.push(uid);
9749
9750
  }
9750
9751
  }
9751
9752
  };
9752
- walk4(rootsOf(contract, page));
9753
+ walk5(rootsOf(contract, page));
9753
9754
  for (const uid of drawn2) {
9754
9755
  const element = elements[uid];
9755
9756
  const type = element.type ?? "";
@@ -11627,6 +11628,225 @@ function badgesCss() {
11627
11628
  `;
11628
11629
  }
11629
11630
 
11631
+ // ../flows/insights-builder/kit/contract/formula.mjs
11632
+ var FORMULA_FUNCTIONS = Object.freeze({
11633
+ abs: [1, 1],
11634
+ if: [3, 3],
11635
+ max: [2, null],
11636
+ min: [2, null],
11637
+ round: [1, 2]
11638
+ });
11639
+ var ROUND_DIGITS_MAX = 6;
11640
+ var FORMULA_NUMBERS_ALONE = "its figure can be a number it states rather than an operand's, and a number the app typed is no Datarails figure; build it from its operands (from) on every path: each branch of if() and each argument of min() and max() must come from an operand (if()'s condition only picks a branch), and / already gives no figure for a zero divisor, so no fallback number is needed";
11641
+ var COMPARISONS = ["<=", ">=", "==", "!=", "<", ">"];
11642
+ var FormulaSyntax = class extends Error {
11643
+ };
11644
+ function tokenize2(text3) {
11645
+ const tokens = [];
11646
+ let at = 0;
11647
+ const isDigit = (c) => c >= "0" && c <= "9";
11648
+ const isStart = (c) => /[\p{L}_]/u.test(c);
11649
+ const isPart5 = (c) => /[\p{L}\p{N}_]/u.test(c);
11650
+ while (at < text3.length) {
11651
+ const c = text3[at];
11652
+ if (/\s/.test(c)) {
11653
+ at += 1;
11654
+ } else if (COMPARISONS.slice(0, 4).includes(text3.slice(at, at + 2))) {
11655
+ tokens.push(["op", text3.slice(at, at + 2)]);
11656
+ at += 2;
11657
+ } else if ("+-*/(),<>".includes(c)) {
11658
+ tokens.push(["op", c]);
11659
+ at += 1;
11660
+ } else if (isDigit(c) || c === ".") {
11661
+ const start = at;
11662
+ while (at < text3.length && (isDigit(text3[at]) || text3[at] === ".")) at += 1;
11663
+ const spelled = text3.slice(start, at);
11664
+ if ((spelled.match(/\./g) ?? []).length > 1 || spelled === ".") throw new FormulaSyntax();
11665
+ tokens.push(["num", Number(spelled)]);
11666
+ } else if (isStart(c)) {
11667
+ const start = at;
11668
+ while (at < text3.length && isPart5(text3[at])) at += 1;
11669
+ tokens.push(["name", text3.slice(start, at)]);
11670
+ } else {
11671
+ throw new FormulaSyntax();
11672
+ }
11673
+ }
11674
+ return tokens;
11675
+ }
11676
+ function parseFormula(text3) {
11677
+ const tokens = tokenize2(String(text3));
11678
+ if (tokens.length === 0) throw new FormulaSyntax();
11679
+ const is = (at, op) => at < tokens.length && tokens[at][0] === "op" && tokens[at][1] === op;
11680
+ const isComparison = (at) => at < tokens.length && tokens[at][0] === "op" && COMPARISONS.includes(tokens[at][1]);
11681
+ function expr(at) {
11682
+ let [node2, next2] = term(at);
11683
+ while (is(next2, "+") || is(next2, "-")) {
11684
+ const op = tokens[next2][1];
11685
+ const [right, after] = term(next2 + 1);
11686
+ node2 = ["bin", op, node2, right];
11687
+ next2 = after;
11688
+ }
11689
+ return [node2, next2];
11690
+ }
11691
+ function term(at) {
11692
+ let [node2, next2] = factor(at);
11693
+ while (is(next2, "*") || is(next2, "/")) {
11694
+ const op = tokens[next2][1];
11695
+ const [right, after] = factor(next2 + 1);
11696
+ node2 = ["bin", op, node2, right];
11697
+ next2 = after;
11698
+ }
11699
+ return [node2, next2];
11700
+ }
11701
+ function factor(at) {
11702
+ if (is(at, "-")) {
11703
+ const [node2, next2] = factor(at + 1);
11704
+ return [["neg", node2], next2];
11705
+ }
11706
+ return atom(at);
11707
+ }
11708
+ function atom(at) {
11709
+ if (at >= tokens.length) throw new FormulaSyntax();
11710
+ const [kind, value] = tokens[at];
11711
+ if (kind === "num") return [["num", value], at + 1];
11712
+ if (kind === "name") {
11713
+ if (is(at + 1, "(")) return call(at + 2, value);
11714
+ return [["name", value], at + 1];
11715
+ }
11716
+ if (value === "(") {
11717
+ const [node2, next2] = expr(at + 1);
11718
+ if (!is(next2, ")")) throw new FormulaSyntax();
11719
+ return [node2, next2 + 1];
11720
+ }
11721
+ throw new FormulaSyntax();
11722
+ }
11723
+ function call(at, name) {
11724
+ const args = [];
11725
+ let next2 = at;
11726
+ for (; ; ) {
11727
+ let [node2, after] = expr(next2);
11728
+ if (isComparison(after)) {
11729
+ const op = tokens[after][1];
11730
+ const [right, past] = expr(after + 1);
11731
+ node2 = ["cmp", op, node2, right];
11732
+ after = past;
11733
+ }
11734
+ args.push(node2);
11735
+ if (is(after, ",")) {
11736
+ next2 = after + 1;
11737
+ continue;
11738
+ }
11739
+ if (is(after, ")")) return [["fn", name, args], after + 1];
11740
+ throw new FormulaSyntax();
11741
+ }
11742
+ }
11743
+ let [node, next] = expr(0);
11744
+ if (isComparison(next)) {
11745
+ const op = tokens[next][1];
11746
+ const [right, after] = expr(next + 1);
11747
+ node = ["cmp", op, node, right];
11748
+ next = after;
11749
+ }
11750
+ if (next !== tokens.length) throw new FormulaSyntax();
11751
+ return node;
11752
+ }
11753
+ function formulaNames(node) {
11754
+ const out = /* @__PURE__ */ new Set();
11755
+ const walk5 = (one) => {
11756
+ if (one[0] === "name") out.add(one[1]);
11757
+ else if (one[0] === "neg") walk5(one[1]);
11758
+ else if (one[0] === "bin" || one[0] === "cmp") {
11759
+ walk5(one[2]);
11760
+ walk5(one[3]);
11761
+ } else if (one[0] === "fn") one[2].forEach(walk5);
11762
+ };
11763
+ walk5(node);
11764
+ return out;
11765
+ }
11766
+ function takesAnOperand(node) {
11767
+ const kind = node[0];
11768
+ if (kind === "name") return true;
11769
+ if (kind === "neg") return takesAnOperand(node[1]);
11770
+ if (kind === "bin") return takesAnOperand(node[2]) || takesAnOperand(node[3]);
11771
+ if (kind === "fn") {
11772
+ const [, name, args] = node;
11773
+ let figures = args;
11774
+ if (name === "if") figures = args.slice(1);
11775
+ else if (name === "abs" || name === "round") figures = args.slice(0, 1);
11776
+ return figures.length > 0 && figures.every(takesAnOperand);
11777
+ }
11778
+ return false;
11779
+ }
11780
+ var ALLOWED = Object.keys(FORMULA_FUNCTIONS).sort().map((name) => `${name}()`).join(", ");
11781
+ function walk2(node, problems, condition) {
11782
+ const note = (sentence2) => {
11783
+ if (!problems.includes(sentence2)) problems.push(sentence2);
11784
+ };
11785
+ const kind = node[0];
11786
+ if (kind === "cmp") {
11787
+ if (!condition) note("a comparison (< <= > >= == !=) stands only as if()'s first argument, such as if(actual > budget, actual, budget)");
11788
+ walk2(node[2], problems, false);
11789
+ walk2(node[3], problems, false);
11790
+ return;
11791
+ }
11792
+ if (condition) note("if() takes a comparison first, such as if(actual > budget, actual, budget)");
11793
+ if (kind === "neg") walk2(node[1], problems, false);
11794
+ else if (kind === "bin") {
11795
+ walk2(node[2], problems, false);
11796
+ walk2(node[3], problems, false);
11797
+ } else if (kind === "fn") {
11798
+ const [, name, args] = node;
11799
+ callProblems(name, args, note);
11800
+ args.forEach((arg, index) => walk2(arg, problems, name === "if" && index === 0));
11801
+ }
11802
+ }
11803
+ function callProblems(name, args, note) {
11804
+ if (!Object.prototype.hasOwnProperty.call(FORMULA_FUNCTIONS, name)) {
11805
+ note(`${name}() is not a function a formula may call; it may call ${ALLOWED}`);
11806
+ return;
11807
+ }
11808
+ const [fewest, most] = FORMULA_FUNCTIONS[name];
11809
+ if (args.length < fewest || most !== null && args.length > most) {
11810
+ let said2;
11811
+ if (name === "if") said2 = "3 arguments: a comparison, the figure when it holds, and the figure when it does not";
11812
+ else if (most === null) said2 = `at least ${fewest} arguments`;
11813
+ else if (fewest === most) said2 = `${fewest} argument${fewest === 1 ? "" : "s"}`;
11814
+ else said2 = `${fewest} or ${most} arguments`;
11815
+ note(`${name}() takes ${said2}, and it is given ${args.length}`);
11816
+ return;
11817
+ }
11818
+ if (name === "round" && args.length === 2) {
11819
+ const digits = args[1];
11820
+ if (!(digits[0] === "num" && Number.isInteger(digits[1]) && digits[1] >= 0 && digits[1] <= ROUND_DIGITS_MAX)) {
11821
+ note(`round()'s second argument is how many decimals to keep: a whole number from 0 to ${ROUND_DIGITS_MAX}, written as a number`);
11822
+ }
11823
+ }
11824
+ }
11825
+ function formulaProblems(text3, operands) {
11826
+ if (typeof text3 !== "string") return { tree: null, problems: ["the formula is not text"] };
11827
+ let tree;
11828
+ try {
11829
+ tree = parseFormula(text3);
11830
+ } catch (error) {
11831
+ if (!(error instanceof FormulaSyntax)) throw error;
11832
+ return {
11833
+ tree: null,
11834
+ problems: [`it is not a formula the platform can read: it may use numbers, its operands' names, + - * /, parentheses, and the functions ${ALLOWED}`]
11835
+ };
11836
+ }
11837
+ const problems = [];
11838
+ walk2(tree, problems, false);
11839
+ const known = (Array.isArray(operands) ? operands : []).filter((one) => typeof one === "string");
11840
+ for (const name of [...formulaNames(tree)].sort()) {
11841
+ if (!known.includes(name)) {
11842
+ const sentence2 = `it reads ${name}, which is not one of its operands (from); its operands are: ${known.join(", ") || "none"}`;
11843
+ if (!problems.includes(sentence2)) problems.push(sentence2);
11844
+ }
11845
+ }
11846
+ if (!takesAnOperand(tree) && !problems.includes(FORMULA_NUMBERS_ALONE)) problems.push(FORMULA_NUMBERS_ALONE);
11847
+ return problems.length > 0 ? { tree: null, problems } : { tree, problems: [] };
11848
+ }
11849
+
11630
11850
  // ../flows/insights-builder/kit/contract/compileApp.mjs
11631
11851
  var RUNTIME = "dr-app-runtime@1.11.0";
11632
11852
  var UI_KIT2 = "ui-kit/v4";
@@ -12084,7 +12304,7 @@ function sourceProblems(sources, app, handlers, extra) {
12084
12304
  });
12085
12305
  }
12086
12306
  }
12087
- if (row.transform === void 0 || row.transform === null) {
12307
+ if ((row.transform === void 0 || row.transform === null) && row.expr === void 0) {
12088
12308
  problems.push({
12089
12309
  code: "transform_missing",
12090
12310
  pointer: at(name, "transform"),
@@ -12119,6 +12339,109 @@ function sourceProblems(sources, app, handlers, extra) {
12119
12339
  }
12120
12340
  return problems;
12121
12341
  }
12342
+ var FORMULA_SHAPES = ["kpi", "series", "breakdown"];
12343
+ function exprProblems(sources) {
12344
+ const problems = [];
12345
+ const at = (name, key) => `/data/sources/${escapePointer3(name)}${key ? `/${key}` : ""}`;
12346
+ const isFormula = (row) => isPlainObject8(row) && row.expr !== void 0;
12347
+ const say = (pointer2, message) => problems.push({ code: "source_expr", pointer: pointer2, message });
12348
+ const cutOperands = /* @__PURE__ */ new Set();
12349
+ for (const name of Object.keys(sources).sort(natural3)) {
12350
+ const row = sources[name];
12351
+ if (!isFormula(row)) continue;
12352
+ const pointer2 = at(name, "expr");
12353
+ if (!Array.isArray(row.from)) {
12354
+ say(pointer2, `source "${name}" states expr, and a formula is evaluated over other sources' figures: write from as the list of sources it reads (["budget", "actual"])`);
12355
+ continue;
12356
+ }
12357
+ if (row.transform !== void 0 && row.transform !== null) {
12358
+ say(pointer2, `source "${name}" states expr and a transform; its figure is one or the other \u2014 drop the transform, and the platform evaluates expr`);
12359
+ continue;
12360
+ }
12361
+ const shape = row.shape ?? "rows";
12362
+ if (!FORMULA_SHAPES.includes(shape)) {
12363
+ say(at(name, "shape"), `source "${name}" states a formula and answers ${JSON.stringify(shape)}; a formula answers ${either3(FORMULA_SHAPES.map((one) => JSON.stringify(one)))} \u2014 state its shape`);
12364
+ }
12365
+ if (row.compare !== void 0) {
12366
+ say(at(name, "compare"), `source "${name}" is a formula and states compare; its compare is its formula over its operands' compares \u2014 state compare on the operands (compare_label still names it here)`);
12367
+ }
12368
+ const operands = row.from.filter(named5);
12369
+ const { tree, problems: wrong } = formulaProblems(row.expr, operands);
12370
+ for (const one of wrong) say(pointer2, `source "${name}"'s formula cannot be evaluated: ${one}`);
12371
+ if (tree === null) continue;
12372
+ const read = formulaNames(tree);
12373
+ row.from.forEach((dep, index) => {
12374
+ if (named5(dep) && !read.has(dep)) {
12375
+ say(
12376
+ at(name, `from/${index}`),
12377
+ `source "${name}"'s formula never reads "${dep}"; an operand it does not use would be listed as an input its figure was not built from \u2014 drop it from from, or use it in expr`
12378
+ );
12379
+ }
12380
+ });
12381
+ const allowed = shape === "kpi" ? ["kpi"] : [shape, "kpi"];
12382
+ let shaped = shape === "kpi";
12383
+ for (const dep of [...new Set(operands)]) {
12384
+ const operand = sources[dep];
12385
+ if (!isPlainObject8(operand)) continue;
12386
+ const origin = originOf(operand);
12387
+ if (operand.transform !== void 0 && operand.transform !== null || origin === "depends" && !isFormula(operand)) {
12388
+ say(
12389
+ pointer2,
12390
+ `source "${name}"'s formula reads "${dep}", which the app computes in its own code${named5(operand.transform) ? ` (${operand.transform})` : ""}; a formula's operands are FinanceOS table reads or metrics (from.financeos), or other formulas over them, so its figure never comes from app arithmetic`
12391
+ );
12392
+ continue;
12393
+ }
12394
+ if (origin === "call") {
12395
+ say(pointer2, `source "${name}"'s formula reads "${dep}", a call whose answer has no rows behind it to open; read the table or metric with from.financeos instead`);
12396
+ continue;
12397
+ }
12398
+ if (origin === "table") {
12399
+ say(
12400
+ pointer2,
12401
+ `source "${name}"'s formula reads "${dep}", one of the app's own tables; a formula's operands are FinanceOS table reads or metrics (from.financeos), or other formulas over them \u2014 a figure the page needs from app data is no formula`
12402
+ );
12403
+ continue;
12404
+ }
12405
+ const theirs = operand.shape ?? "rows";
12406
+ if (!allowed.includes(theirs)) {
12407
+ say(
12408
+ pointer2,
12409
+ `source "${name}" is a ${shape} formula, and its operand "${dep}" answers ${JSON.stringify(theirs)}; a ${shape} formula's operands answer ${allowed.map((one) => JSON.stringify(one)).join(" or ")} \u2014 give "${dep}" that shape`
12410
+ );
12411
+ continue;
12412
+ }
12413
+ if (theirs === shape) shaped = true;
12414
+ if (theirs === "breakdown" && operand.top !== null && !cutOperands.has(dep)) {
12415
+ cutOperands.add(dep);
12416
+ say(
12417
+ at(dep, "top"),
12418
+ `source "${dep}" is an operand of the breakdown formula "${name}", which needs every member of it; state "top": null on "${dep}" (a breakdown draws its top 5 by default, and the members past it would drop out of the formula)`
12419
+ );
12420
+ }
12421
+ }
12422
+ if (!shaped && FORMULA_SHAPES.includes(shape)) {
12423
+ say(pointer2, `source "${name}" is a ${shape} formula with no ${shape} operand; it takes its ${shape === "series" ? "periods" : "members"} from one \u2014 add a ${shape} source to from`);
12424
+ }
12425
+ }
12426
+ for (const name of Object.keys(sources).sort(natural3)) {
12427
+ if (!isFormula(sources[name]) || !Array.isArray(sources[name].from)) continue;
12428
+ for (const dep of sources[name].from.filter(named5)) {
12429
+ if (reaches2(sources, dep, name, /* @__PURE__ */ new Set(), isFormula)) {
12430
+ say(at(name, "expr"), `source "${name}"'s formula reads itself, through "${dep}"; a formula's operands are read before it, so none may read it back`);
12431
+ break;
12432
+ }
12433
+ }
12434
+ }
12435
+ return problems;
12436
+ }
12437
+ function reaches2(sources, start, target, seen, isFormula) {
12438
+ if (start === target) return true;
12439
+ if (seen.has(start)) return false;
12440
+ seen.add(start);
12441
+ const row = sources[start];
12442
+ if (!isFormula(row) || !Array.isArray(row.from)) return false;
12443
+ return row.from.filter(named5).some((one) => reaches2(sources, one, target, seen, isFormula));
12444
+ }
12122
12445
  var SOURCE_ORDERS = ["source", "declared"];
12123
12446
  function orderProblems(name, row, at) {
12124
12447
  const problems = [];
@@ -12698,6 +13021,7 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
12698
13021
  const sourceFindings = sourceProblems(stated2, app, handlers, extra);
12699
13022
  problems.push(...sourceFindings.filter((one) => !one.advisory));
12700
13023
  problems.push(...compareProblems(stated2, model));
13024
+ problems.push(...exprProblems(stated2));
12701
13025
  problems.push(...paramProblems(contract, stated2));
12702
13026
  problems.push(...themeProblems(app));
12703
13027
  problems.push(...shellProblems(contract, typed, handlers, stated2));
@@ -13723,8 +14047,8 @@ function numberPinProblems(elements, pins) {
13723
14047
  const pin = pins[module];
13724
14048
  if (typeof pin !== "string" || drawsPlatformNumbers(pin)) continue;
13725
14049
  const need = typeof PLATFORM_NUMBERS2[module] === "string" ? `${module}@${PLATFORM_NUMBERS2[module]}` : `a ${module} release that draws it`;
13726
- const walk4 = (node, at) => {
13727
- if (Array.isArray(node)) return node.forEach((child, i) => walk4(child, `${at}/${i}`));
14050
+ const walk5 = (node, at) => {
14051
+ if (Array.isArray(node)) return node.forEach((child, i) => walk5(child, `${at}/${i}`));
13728
14052
  if (!isPlainObject8(node)) return;
13729
14053
  for (const [key, value] of Object.entries(node)) {
13730
14054
  const pointer2 = `${at}/${escapePointer3(key)}`;
@@ -13735,10 +14059,10 @@ function numberPinProblems(elements, pins) {
13735
14059
  });
13736
14060
  if (key === "format" && isPlatformFormat(value)) say(`format ${JSON.stringify(value)}`);
13737
14061
  else if (key === "negative" && value !== void 0 && value !== null) say(`negative ${JSON.stringify(value)}`);
13738
- else walk4(value, pointer2);
14062
+ else walk5(value, pointer2);
13739
14063
  }
13740
14064
  };
13741
- walk4(element.props ?? {}, `/elements/${escapePointer3(uid)}/props`);
14065
+ walk5(element.props ?? {}, `/elements/${escapePointer3(uid)}/props`);
13742
14066
  }
13743
14067
  return problems;
13744
14068
  }
@@ -16835,7 +17159,7 @@ function checkAppSources(doc, problems) {
16835
17159
  fix: "Name a declared source, or declare it."
16836
17160
  });
16837
17161
  }
16838
- if (row["transform"] === void 0 || row["transform"] === null) {
17162
+ if ((row["transform"] === void 0 || row["transform"] === null) && row["expr"] === void 0) {
16839
17163
  problems.push({
16840
17164
  code: "transform-missing",
16841
17165
  pointer: pointer("data", "sources", name, "transform"),
@@ -17878,7 +18202,7 @@ function checkKitRules(doc, kind, problems, renderer = "widgets") {
17878
18202
  fix: "Give it a source with the brief\u2019s metric_id or table_alias, or a compute."
17879
18203
  });
17880
18204
  }
17881
- walk2(row, ["data", "widgets", id], (value, path3, key) => {
18205
+ walk3(row, ["data", "widgets", id], (value, path3, key) => {
17882
18206
  if (isObject4(value) && typeof value["$callable"] === "string") {
17883
18207
  if (known.size > 0 && !known.has(value["$callable"])) {
17884
18208
  problems.push({
@@ -18293,17 +18617,17 @@ function inheritedEntity2(doc, uid, entityOf2) {
18293
18617
  }
18294
18618
  return null;
18295
18619
  }
18296
- function walk2(node, path3, visit) {
18620
+ function walk3(node, path3, visit) {
18297
18621
  const key = typeof path3[path3.length - 1] === "string" ? path3[path3.length - 1] : null;
18298
18622
  if (!visit(node, path3, key)) return;
18299
18623
  if (Array.isArray(node)) {
18300
18624
  node.forEach((item, index) => {
18301
- walk2(item, [...path3, index], visit);
18625
+ walk3(item, [...path3, index], visit);
18302
18626
  });
18303
18627
  return;
18304
18628
  }
18305
18629
  if (isObject4(node)) {
18306
- for (const [name, value] of Object.entries(node)) walk2(value, [...path3, name], visit);
18630
+ for (const [name, value] of Object.entries(node)) walk3(value, [...path3, name], visit);
18307
18631
  }
18308
18632
  }
18309
18633
  function dedupe(problems) {
@@ -19600,7 +19924,7 @@ async function readDirIfPresent(path3) {
19600
19924
  }
19601
19925
  async function readUiTree(uiDir) {
19602
19926
  const out = [];
19603
- const walk4 = async (dir, prefix) => {
19927
+ const walk5 = async (dir, prefix) => {
19604
19928
  for (const name of (await readDirIfPresent(dir)).sort()) {
19605
19929
  const full = join5(dir, name);
19606
19930
  let entry;
@@ -19610,14 +19934,14 @@ async function readUiTree(uiDir) {
19610
19934
  continue;
19611
19935
  }
19612
19936
  if (entry.isDirectory()) {
19613
- await walk4(full, `${prefix}${name}/`);
19937
+ await walk5(full, `${prefix}${name}/`);
19614
19938
  continue;
19615
19939
  }
19616
19940
  if (!entry.isFile()) continue;
19617
19941
  out.push({ path: `${prefix}${name}`, bytes: await readFile3(full) });
19618
19942
  }
19619
19943
  };
19620
- await walk4(uiDir, "");
19944
+ await walk5(uiDir, "");
19621
19945
  return out.sort((a, b) => a.path < b.path ? -1 : 1);
19622
19946
  }
19623
19947
  async function readManifest(dir) {
@@ -23114,7 +23438,7 @@ function loadApp2(dir) {
23114
23438
  source: readHandlerSource2(dir, file)
23115
23439
  };
23116
23440
  });
23117
- const ui = walk3(join13(dir, UI_PREFIX3)).map((path3) => ({ path: UI_PREFIX3 + relative2(join13(dir, UI_PREFIX3), path3).split("\\").join("/"), text: readText2(path3) })).sort((a, b) => a.path < b.path ? -1 : 1);
23441
+ const ui = walk4(join13(dir, UI_PREFIX3)).map((path3) => ({ path: UI_PREFIX3 + relative2(join13(dir, UI_PREFIX3), path3).split("\\").join("/"), text: readText2(path3) })).sort((a, b) => a.path < b.path ? -1 : 1);
23118
23442
  const pages = ui.filter((file) => PAGE_PATH4.test(file.path));
23119
23443
  const index = pages.find((file) => file.path === "ui/index.html") ?? null;
23120
23444
  return { manifest, dataModel, handlers, handlersSource, ui, pages, index };
@@ -23143,12 +23467,12 @@ function readText2(path3) {
23143
23467
  const bytes = readFileSync4(path3);
23144
23468
  return bytes.includes(0) ? "" : bytes.toString("utf8");
23145
23469
  }
23146
- function walk3(dir) {
23470
+ function walk4(dir) {
23147
23471
  if (!existsSync2(dir)) return [];
23148
23472
  const out = [];
23149
23473
  for (const entry of readdirSync2(dir).sort()) {
23150
23474
  const full = join13(dir, entry);
23151
- if (statSync2(full).isDirectory()) out.push(...walk3(full));
23475
+ if (statSync2(full).isDirectory()) out.push(...walk4(full));
23152
23476
  else out.push(full);
23153
23477
  }
23154
23478
  return out;
@@ -23379,9 +23703,9 @@ var semver2 = (version) => String(version ?? "").split(".").map((part) => Number
23379
23703
  var APP_WIDGET_KINDS5 = APP_WIDGET_KINDS4;
23380
23704
  var PLATFORM_FINANCE4 = PLATFORM_FINANCE3;
23381
23705
  function drawsFinanceKinds2(pin) {
23382
- return reaches2(pin, PLATFORM_FINANCE4);
23706
+ return reaches3(pin, PLATFORM_FINANCE4);
23383
23707
  }
23384
- function reaches2(pin, table) {
23708
+ function reaches3(pin, table) {
23385
23709
  const at = String(pin ?? "").lastIndexOf("@");
23386
23710
  const module = at > 0 ? pin.slice(0, at) : String(pin ?? "");
23387
23711
  const first = table[module];
@@ -23562,7 +23886,7 @@ var TERMINATORS2 = /* @__PURE__ */ new Set([",", "]", "}", ")", ":", ";"]);
23562
23886
  var OPENERS3 = "([{";
23563
23887
  var CLOSERS3 = ")]}";
23564
23888
  var STRING_PREFIX2 = /^[rRbBuUfF]{0,3}$/;
23565
- function tokenize2(src, d) {
23889
+ function tokenize3(src, d) {
23566
23890
  const tokens = [];
23567
23891
  let i = 0;
23568
23892
  let line2 = 1;
@@ -23720,7 +24044,7 @@ var Literals2 = class {
23720
24044
  constructor(src, dialect = "py") {
23721
24045
  this.src = src;
23722
24046
  this.d = DIALECTS2[dialect] ?? PY2;
23723
- this.tokens = tokenize2(src, this.d);
24047
+ this.tokens = tokenize3(src, this.d);
23724
24048
  this.consts = /* @__PURE__ */ new Map();
23725
24049
  this.defs = /* @__PURE__ */ new Set();
23726
24050
  this.cache = /* @__PURE__ */ new Map();
@@ -24794,15 +25118,15 @@ function inputTypeOf2(node) {
24794
25118
  }
24795
25119
  function optionsOf2(node, text3, tree) {
24796
25120
  const options = [];
24797
- const walk4 = (index) => {
25121
+ const walk5 = (index) => {
24798
25122
  for (const child of tree.nodes[index].children) {
24799
25123
  const candidate = tree.nodes[child];
24800
25124
  if (candidate.tag === "option") {
24801
25125
  if (!("disabled" in candidate.attrs)) options.push(candidate.attrs.value ?? textOf2(text3, candidate));
24802
- } else walk4(child);
25126
+ } else walk5(child);
24803
25127
  }
24804
25128
  };
24805
- walk4(node.index);
25129
+ walk5(node.index);
24806
25130
  return options;
24807
25131
  }
24808
25132
  function splitList2(value) {
@@ -24821,14 +25145,14 @@ function textOf2(text3, node) {
24821
25145
  function slotsOf2(node, tree) {
24822
25146
  if (!(WIDGET_ATTR2 in node.attrs)) return {};
24823
25147
  const slots = {};
24824
- const walk4 = (index) => {
25148
+ const walk5 = (index) => {
24825
25149
  for (const child of tree.nodes[index].children) {
24826
25150
  const kind = classify2(tree.nodes[child]);
24827
25151
  if (kind?.type === "slot") slots[kind.role] = kind.role === "body" ? classesOf3(tree.nodes[child])[0] ?? true : true;
24828
- else walk4(child);
25152
+ else walk5(child);
24829
25153
  }
24830
25154
  };
24831
- walk4(node.index);
25155
+ walk5(node.index);
24832
25156
  return slots;
24833
25157
  }
24834
25158
  function bindingOf2(entry, node, spec) {
@@ -25549,15 +25873,15 @@ function hasCycle2(tables) {
25549
25873
  }
25550
25874
  }
25551
25875
  const state = /* @__PURE__ */ new Map();
25552
- const walk4 = (name) => {
25876
+ const walk5 = (name) => {
25553
25877
  if (state.get(name) === "done") return false;
25554
25878
  if (state.get(name) === "open") return true;
25555
25879
  state.set(name, "open");
25556
- for (const next of graph.get(name) ?? []) if (walk4(next)) return true;
25880
+ for (const next of graph.get(name) ?? []) if (walk5(next)) return true;
25557
25881
  state.set(name, "done");
25558
25882
  return false;
25559
25883
  };
25560
- return [...graph.keys()].some((name) => walk4(name));
25884
+ return [...graph.keys()].some((name) => walk5(name));
25561
25885
  }
25562
25886
  function byProblem3(a, b) {
25563
25887
  return natural5(a.code, b.code) || natural5(a.where ?? "", b.where ?? "") || natural5(a.message, b.message);
@@ -26117,16 +26441,16 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
26117
26441
  };
26118
26442
  const fieldsUnder = (uid) => {
26119
26443
  const fields = [];
26120
- const walk4 = (at) => {
26444
+ const walk5 = (at) => {
26121
26445
  for (const child of children.get(at) ?? []) {
26122
26446
  const element = elements[child];
26123
26447
  const field = fieldOf2(element);
26124
26448
  if (field !== null && !fields.includes(field)) fields.push(field);
26125
26449
  if (element?.type === "form" || element?.type === "dialog") continue;
26126
- walk4(child);
26450
+ walk5(child);
26127
26451
  }
26128
26452
  };
26129
- walk4(uid);
26453
+ walk5(uid);
26130
26454
  return fields;
26131
26455
  };
26132
26456
  for (const uid of Object.keys(elements).sort()) {
@@ -27001,20 +27325,20 @@ function untitledCards2(contract) {
27001
27325
  if (page?.owned === true) continue;
27002
27326
  const seen = /* @__PURE__ */ new Set();
27003
27327
  const drawn2 = [];
27004
- const walk4 = (uids) => {
27328
+ const walk5 = (uids) => {
27005
27329
  for (const uid of uids) {
27006
27330
  const element = elements[uid];
27007
27331
  if (element === void 0 || seen.has(uid)) continue;
27008
27332
  seen.add(uid);
27009
27333
  if (LAYOUT2.has(element.type ?? "")) {
27010
27334
  const children = element.children?.length ? element.children : Object.keys(elements).filter((other) => elements[other]?.parent === uid);
27011
- walk4(children);
27335
+ walk5(children);
27012
27336
  } else {
27013
27337
  drawn2.push(uid);
27014
27338
  }
27015
27339
  }
27016
27340
  };
27017
- walk4(rootsOf3(contract, page));
27341
+ walk5(rootsOf3(contract, page));
27018
27342
  for (const uid of drawn2) {
27019
27343
  const element = elements[uid];
27020
27344
  const type = element.type ?? "";
@@ -28719,6 +29043,8 @@ var REFUSALS = /* @__PURE__ */ new Set([
28719
29043
  "transform_forbidden",
28720
29044
  // a KPI source's compare (E6)
28721
29045
  "source_compare_field",
29046
+ // a source's formula, which the platform evaluates (lineage tier 3)
29047
+ "source_expr",
28722
29048
  // dr-components parts (DC-4)
28723
29049
  "part_props",
28724
29050
  "part_source_shape",
@@ -28802,6 +29128,9 @@ var SOURCE_REFUSALS = {
28802
29128
  var KPI_REFUSALS = {
28803
29129
  source_compare_field: "a source's compare names a field its rows do not carry (where that is knowable: a FinanceOS aggregate's dimensions, measures and period column; an app table's columns), or is set on a source not shaped kpi \u2014 declare the measure beside the figure, or name one it carries"
28804
29130
  };
29131
+ var EXPR_REFUSALS = {
29132
+ source_expr: "a source's formula (its expr) has a from that is no list, or sits beside a transform; answers a shape other than kpi, series or breakdown; states compare (its compare is its formula over its operands' compares); cannot be evaluated (it is not text, a syntax error, a function other than abs, min, max, round and if, a wrong number of arguments, round decimals other than a whole number from 0 to 6, an if whose first argument is no comparison, a comparison anywhere but if's condition, a name not in from); can be, on some path, a number it states rather than an operand's (numbers alone such as 870000, a typed if() branch such as if(actual > 0, 870000, actual), or a typed min() or max() bound such as max(actual, 0): each branch of if() and each argument of min() and max() must come from an operand, and / already gives no figure for a zero divisor, so no fallback number is needed); names an operand it never reads; reads an operand that is not a FinanceOS read or metric or a formula over them (a transform, a composed source, a call, one of the app's own tables); reads an operand of a shape it cannot take (a kpi formula takes kpis; a series or breakdown formula its own shape and kpis), or, as a series or breakdown formula, has no operand of its own shape; reads a breakdown operand that does not state top: null; or reads itself \u2014 the message names which"
29133
+ };
28805
29134
  var TRANSFORM_REFUSALS = {
28806
29135
  transform_row: 'a row names a transform and is not a dataset series row ("kind": "series", "data": "<dataset>", and no source, compute, depends, drill, window or by), or its dataset reads with the period pushed down (a period other than "all")',
28807
29136
  transform_missing: "a row or source names a transform not spelled transforms/<name>.py, or that file is not beside contract.json",
@@ -29908,6 +30237,8 @@ function registerCompileCommand(parent) {
29908
30237
  ...Object.entries(SOURCE_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
29909
30238
  "A KPI source (shape kpi: its period comes from the rows; compare sets the figure against another measure of the same row) is refused with:",
29910
30239
  ...Object.entries(KPI_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
30240
+ "A source that states its figure as a formula (expr over from, evaluated by the platform) is refused with:",
30241
+ ...Object.entries(EXPR_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`),
29911
30242
  "",
29912
30243
  "A transform (a dashboard dataset series row's transforms/<name>.py, or an app source's) is refused with:",
29913
30244
  ...Object.entries(TRANSFORM_REFUSALS).map(([code, what]) => ` ${code.padEnd(22)}${what}`)
@@ -31612,19 +31943,19 @@ function figuresIn(text3) {
31612
31943
  }
31613
31944
  function seriesNumbers(data) {
31614
31945
  const out = [];
31615
- const walk4 = (one) => {
31946
+ const walk5 = (one) => {
31616
31947
  if (Array.isArray(one)) {
31617
- for (const item of one) walk4(item);
31948
+ for (const item of one) walk5(item);
31618
31949
  return;
31619
31950
  }
31620
31951
  if (typeof one === "object" && one !== null) {
31621
- walk4(one.value);
31952
+ walk5(one.value);
31622
31953
  return;
31623
31954
  }
31624
31955
  const n = numberOf(one);
31625
31956
  if (n !== null) out.push(n);
31626
31957
  };
31627
- walk4(data);
31958
+ walk5(data);
31628
31959
  return out;
31629
31960
  }
31630
31961
  var INPUT_TAGS = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-appspace-cli",
3
- "version": "0.12.29",
3
+ "version": "0.12.30",
4
4
  "description": "Datarails App Space CLI — build, try, push and publish App Space apps",
5
5
  "type": "module",
6
6
  "engines": {