@lotics/cli 0.280.0 → 0.281.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -1197,13 +1197,13 @@ var require_ast = __commonJS({
1197
1197
  helperExpression: function helperExpression(node) {
1198
1198
  return node.type === "SubExpression" || (node.type === "MustacheStatement" || node.type === "BlockStatement") && !!(node.params && node.params.length || node.hash);
1199
1199
  },
1200
- scopedId: function scopedId(path15) {
1201
- return /^\.|this\b/.test(path15.original);
1200
+ scopedId: function scopedId(path16) {
1201
+ return /^\.|this\b/.test(path16.original);
1202
1202
  },
1203
1203
  // an ID is simple if it only has one part, and that part is not
1204
1204
  // `..` or `this`.
1205
- simpleId: function simpleId(path15) {
1206
- return path15.parts.length === 1 && !AST.helpers.scopedId(path15) && !path15.depth;
1205
+ simpleId: function simpleId(path16) {
1206
+ return path16.parts.length === 1 && !AST.helpers.scopedId(path16) && !path16.depth;
1207
1207
  }
1208
1208
  }
1209
1209
  };
@@ -2273,12 +2273,12 @@ var require_helpers2 = __commonJS({
2273
2273
  loc
2274
2274
  };
2275
2275
  }
2276
- function prepareMustache(path15, params, hash2, open, strip, locInfo) {
2276
+ function prepareMustache(path16, params, hash2, open, strip, locInfo) {
2277
2277
  var escapeFlag = open.charAt(3) || open.charAt(2), escaped = escapeFlag !== "{" && escapeFlag !== "&";
2278
2278
  var decorator = /\*/.test(open);
2279
2279
  return {
2280
2280
  type: decorator ? "Decorator" : "MustacheStatement",
2281
- path: path15,
2281
+ path: path16,
2282
2282
  params,
2283
2283
  hash: hash2,
2284
2284
  escaped,
@@ -2596,9 +2596,9 @@ var require_compiler = __commonJS({
2596
2596
  },
2597
2597
  DecoratorBlock: function DecoratorBlock(decorator) {
2598
2598
  var program = decorator.program && this.compileProgram(decorator.program);
2599
- var params = this.setupFullMustacheParams(decorator, program, void 0), path15 = decorator.path;
2599
+ var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
2600
2600
  this.useDecorators = true;
2601
- this.opcode("registerDecorator", params.length, path15.original);
2601
+ this.opcode("registerDecorator", params.length, path16.original);
2602
2602
  },
2603
2603
  PartialStatement: function PartialStatement(partial2) {
2604
2604
  this.usePartial = true;
@@ -2662,46 +2662,46 @@ var require_compiler = __commonJS({
2662
2662
  }
2663
2663
  },
2664
2664
  ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) {
2665
- var path15 = sexpr.path, name = path15.parts[0], isBlock = program != null || inverse != null;
2666
- this.opcode("getContext", path15.depth);
2665
+ var path16 = sexpr.path, name = path16.parts[0], isBlock = program != null || inverse != null;
2666
+ this.opcode("getContext", path16.depth);
2667
2667
  this.opcode("pushProgram", program);
2668
2668
  this.opcode("pushProgram", inverse);
2669
- path15.strict = true;
2670
- this.accept(path15);
2669
+ path16.strict = true;
2670
+ this.accept(path16);
2671
2671
  this.opcode("invokeAmbiguous", name, isBlock);
2672
2672
  },
2673
2673
  simpleSexpr: function simpleSexpr(sexpr) {
2674
- var path15 = sexpr.path;
2675
- path15.strict = true;
2676
- this.accept(path15);
2674
+ var path16 = sexpr.path;
2675
+ path16.strict = true;
2676
+ this.accept(path16);
2677
2677
  this.opcode("resolvePossibleLambda");
2678
2678
  },
2679
2679
  helperSexpr: function helperSexpr(sexpr, program, inverse) {
2680
- var params = this.setupFullMustacheParams(sexpr, program, inverse), path15 = sexpr.path, name = path15.parts[0];
2680
+ var params = this.setupFullMustacheParams(sexpr, program, inverse), path16 = sexpr.path, name = path16.parts[0];
2681
2681
  if (this.options.knownHelpers[name]) {
2682
2682
  this.opcode("invokeKnownHelper", params.length, name);
2683
2683
  } else if (this.options.knownHelpersOnly) {
2684
2684
  throw new _exception2["default"]("You specified knownHelpersOnly, but used the unknown helper " + name, sexpr);
2685
2685
  } else {
2686
- path15.strict = true;
2687
- path15.falsy = true;
2688
- this.accept(path15);
2689
- this.opcode("invokeHelper", params.length, path15.original, _ast2["default"].helpers.simpleId(path15));
2686
+ path16.strict = true;
2687
+ path16.falsy = true;
2688
+ this.accept(path16);
2689
+ this.opcode("invokeHelper", params.length, path16.original, _ast2["default"].helpers.simpleId(path16));
2690
2690
  }
2691
2691
  },
2692
- PathExpression: function PathExpression(path15) {
2693
- this.addDepth(path15.depth);
2694
- this.opcode("getContext", path15.depth);
2695
- var name = path15.parts[0], scoped = _ast2["default"].helpers.scopedId(path15), blockParamId = !path15.depth && !scoped && this.blockParamIndex(name);
2692
+ PathExpression: function PathExpression(path16) {
2693
+ this.addDepth(path16.depth);
2694
+ this.opcode("getContext", path16.depth);
2695
+ var name = path16.parts[0], scoped = _ast2["default"].helpers.scopedId(path16), blockParamId = !path16.depth && !scoped && this.blockParamIndex(name);
2696
2696
  if (blockParamId) {
2697
- this.opcode("lookupBlockParam", blockParamId, path15.parts);
2697
+ this.opcode("lookupBlockParam", blockParamId, path16.parts);
2698
2698
  } else if (!name) {
2699
2699
  this.opcode("pushContext");
2700
- } else if (path15.data) {
2700
+ } else if (path16.data) {
2701
2701
  this.options.data = true;
2702
- this.opcode("lookupData", path15.depth, path15.parts, path15.strict);
2702
+ this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
2703
2703
  } else {
2704
- this.opcode("lookupOnContext", path15.parts, path15.falsy, path15.strict, scoped);
2704
+ this.opcode("lookupOnContext", path16.parts, path16.falsy, path16.strict, scoped);
2705
2705
  }
2706
2706
  },
2707
2707
  StringLiteral: function StringLiteral(string4) {
@@ -3051,16 +3051,16 @@ var require_util = __commonJS({
3051
3051
  }
3052
3052
  exports.urlGenerate = urlGenerate;
3053
3053
  function normalize(aPath) {
3054
- var path15 = aPath;
3054
+ var path16 = aPath;
3055
3055
  var url2 = urlParse(aPath);
3056
3056
  if (url2) {
3057
3057
  if (!url2.path) {
3058
3058
  return aPath;
3059
3059
  }
3060
- path15 = url2.path;
3060
+ path16 = url2.path;
3061
3061
  }
3062
- var isAbsolute = exports.isAbsolute(path15);
3063
- var parts = path15.split(/\/+/);
3062
+ var isAbsolute = exports.isAbsolute(path16);
3063
+ var parts = path16.split(/\/+/);
3064
3064
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
3065
3065
  part = parts[i];
3066
3066
  if (part === ".") {
@@ -3077,15 +3077,15 @@ var require_util = __commonJS({
3077
3077
  }
3078
3078
  }
3079
3079
  }
3080
- path15 = parts.join("/");
3081
- if (path15 === "") {
3082
- path15 = isAbsolute ? "/" : ".";
3080
+ path16 = parts.join("/");
3081
+ if (path16 === "") {
3082
+ path16 = isAbsolute ? "/" : ".";
3083
3083
  }
3084
3084
  if (url2) {
3085
- url2.path = path15;
3085
+ url2.path = path16;
3086
3086
  return urlGenerate(url2);
3087
3087
  }
3088
- return path15;
3088
+ return path16;
3089
3089
  }
3090
3090
  exports.normalize = normalize;
3091
3091
  function join2(aRoot, aPath) {
@@ -5868,8 +5868,8 @@ var require_printer = __commonJS({
5868
5868
  return this.accept(sexpr.path) + " " + params + hash2;
5869
5869
  };
5870
5870
  PrintVisitor.prototype.PathExpression = function(id) {
5871
- var path15 = id.parts.join("/");
5872
- return (id.data ? "@" : "") + "PATH:" + path15;
5871
+ var path16 = id.parts.join("/");
5872
+ return (id.data ? "@" : "") + "PATH:" + path16;
5873
5873
  };
5874
5874
  PrintVisitor.prototype.StringLiteral = function(string4) {
5875
5875
  return '"' + string4.value + '"';
@@ -5908,8 +5908,8 @@ var require_lib = __commonJS({
5908
5908
  handlebars.print = printer.print;
5909
5909
  module.exports = handlebars;
5910
5910
  function extension(module2, filename) {
5911
- var fs16 = __require("fs");
5912
- var templateString = fs16.readFileSync(filename, "utf8");
5911
+ var fs17 = __require("fs");
5912
+ var templateString = fs17.readFileSync(filename, "utf8");
5913
5913
  module2.exports = handlebars.compile(templateString);
5914
5914
  }
5915
5915
  if (typeof __require !== "undefined" && __require.extensions) {
@@ -5924,8 +5924,8 @@ import dns from "node:dns";
5924
5924
  import net from "node:net";
5925
5925
 
5926
5926
  // src/cli_dispatch.ts
5927
- import fs15 from "node:fs";
5928
- import path14 from "node:path";
5927
+ import fs16 from "node:fs";
5928
+ import path15 from "node:path";
5929
5929
  import readline from "node:readline";
5930
5930
 
5931
5931
  // ../shared/src/multipart_parts.ts
@@ -6237,8 +6237,8 @@ var LoticsClient = class {
6237
6237
  headers["x-request-id"] = newRequestId();
6238
6238
  return headers;
6239
6239
  }
6240
- async request(method, path15, body) {
6241
- const url2 = `${this.baseUrl}${path15}`;
6240
+ async request(method, path16, body) {
6241
+ const url2 = `${this.baseUrl}${path16}`;
6242
6242
  const headers = this.buildHeaders();
6243
6243
  const init = { method, headers };
6244
6244
  if (body !== void 0) {
@@ -6385,6 +6385,19 @@ var LoticsClient = class {
6385
6385
  async createApp(body) {
6386
6386
  return this.request("POST", "/v1/apps", body);
6387
6387
  }
6388
+ async getApp(appId) {
6389
+ return this.request("GET", `/v1/apps/${encodeURIComponent(appId)}`);
6390
+ }
6391
+ /** A built app version's project source, as the `source.tar.gz` its deploy uploaded. */
6392
+ async downloadAppVersionSource(appId, versionId) {
6393
+ const { url: url2 } = await this.request(
6394
+ "GET",
6395
+ `/v1/apps/${encodeURIComponent(appId)}/versions/${encodeURIComponent(versionId)}/source`
6396
+ );
6397
+ const response = await fetch(url2);
6398
+ if (!response.ok) await this.throwResponseError(response);
6399
+ return Buffer.from(await response.arrayBuffer());
6400
+ }
6388
6401
  /**
6389
6402
  * Workspace-wide dangling-reference sweep — active app/workflow artifacts
6390
6403
  * whose prefixed schema ids no longer resolve. Backs
@@ -8177,10 +8190,10 @@ function mergeDefs(...defs) {
8177
8190
  function cloneDef(schema) {
8178
8191
  return mergeDefs(schema._zod.def);
8179
8192
  }
8180
- function getElementAtPath(obj, path15) {
8181
- if (!path15)
8193
+ function getElementAtPath(obj, path16) {
8194
+ if (!path16)
8182
8195
  return obj;
8183
- return path15.reduce((acc, key) => acc?.[key], obj);
8196
+ return path16.reduce((acc, key) => acc?.[key], obj);
8184
8197
  }
8185
8198
  function promiseAllObject(promisesObj) {
8186
8199
  const keys2 = Object.keys(promisesObj);
@@ -8589,11 +8602,11 @@ function explicitlyAborted(x, startIndex = 0) {
8589
8602
  }
8590
8603
  return false;
8591
8604
  }
8592
- function prefixIssues(path15, issues) {
8605
+ function prefixIssues(path16, issues) {
8593
8606
  return issues.map((iss) => {
8594
8607
  var _a3;
8595
8608
  (_a3 = iss).path ?? (_a3.path = []);
8596
- iss.path.unshift(path15);
8609
+ iss.path.unshift(path16);
8597
8610
  return iss;
8598
8611
  });
8599
8612
  }
@@ -8740,16 +8753,16 @@ function flattenError(error52, mapper = (issue2) => issue2.message) {
8740
8753
  }
8741
8754
  function formatError(error52, mapper = (issue2) => issue2.message) {
8742
8755
  const fieldErrors = { _errors: [] };
8743
- const processError = (error53, path15 = []) => {
8756
+ const processError = (error53, path16 = []) => {
8744
8757
  for (const issue2 of error53.issues) {
8745
8758
  if (issue2.code === "invalid_union" && issue2.errors.length) {
8746
- issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8759
+ issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
8747
8760
  } else if (issue2.code === "invalid_key") {
8748
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8761
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
8749
8762
  } else if (issue2.code === "invalid_element") {
8750
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8763
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
8751
8764
  } else {
8752
- const fullpath = [...path15, ...issue2.path];
8765
+ const fullpath = [...path16, ...issue2.path];
8753
8766
  if (fullpath.length === 0) {
8754
8767
  fieldErrors._errors.push(mapper(issue2));
8755
8768
  } else {
@@ -8776,17 +8789,17 @@ function formatError(error52, mapper = (issue2) => issue2.message) {
8776
8789
  }
8777
8790
  function treeifyError(error52, mapper = (issue2) => issue2.message) {
8778
8791
  const result = { errors: [] };
8779
- const processError = (error53, path15 = []) => {
8792
+ const processError = (error53, path16 = []) => {
8780
8793
  var _a3, _b;
8781
8794
  for (const issue2 of error53.issues) {
8782
8795
  if (issue2.code === "invalid_union" && issue2.errors.length) {
8783
- issue2.errors.map((issues) => processError({ issues }, [...path15, ...issue2.path]));
8796
+ issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
8784
8797
  } else if (issue2.code === "invalid_key") {
8785
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8798
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
8786
8799
  } else if (issue2.code === "invalid_element") {
8787
- processError({ issues: issue2.issues }, [...path15, ...issue2.path]);
8800
+ processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
8788
8801
  } else {
8789
- const fullpath = [...path15, ...issue2.path];
8802
+ const fullpath = [...path16, ...issue2.path];
8790
8803
  if (fullpath.length === 0) {
8791
8804
  result.errors.push(mapper(issue2));
8792
8805
  continue;
@@ -8818,8 +8831,8 @@ function treeifyError(error52, mapper = (issue2) => issue2.message) {
8818
8831
  }
8819
8832
  function toDotPath(_path) {
8820
8833
  const segs = [];
8821
- const path15 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8822
- for (const seg of path15) {
8834
+ const path16 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8835
+ for (const seg of path16) {
8823
8836
  if (typeof seg === "number")
8824
8837
  segs.push(`[${seg}]`);
8825
8838
  else if (typeof seg === "symbol")
@@ -21511,13 +21524,13 @@ function resolveRef(ref, ctx) {
21511
21524
  if (!ref.startsWith("#")) {
21512
21525
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
21513
21526
  }
21514
- const path15 = ref.slice(1).split("/").filter(Boolean);
21515
- if (path15.length === 0) {
21527
+ const path16 = ref.slice(1).split("/").filter(Boolean);
21528
+ if (path16.length === 0) {
21516
21529
  return ctx.rootSchema;
21517
21530
  }
21518
21531
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
21519
- if (path15[0] === defsKey) {
21520
- const key = path15[1];
21532
+ if (path16[0] === defsKey) {
21533
+ const key = path16[1];
21521
21534
  if (!key || !ctx.defs[key]) {
21522
21535
  throw new Error(`Reference not found: ${ref}`);
21523
21536
  }
@@ -25281,16 +25294,16 @@ function conditionValueIssue(type, operator, value, fieldKey) {
25281
25294
  return null;
25282
25295
  }
25283
25296
  }
25284
- function conditionIssues(node, path15, fieldMap) {
25297
+ function conditionIssues(node, path16, fieldMap) {
25285
25298
  if (!node || typeof node !== "object") {
25286
- return [`${path15}: expected condition object, got ${typeof node}`];
25299
+ return [`${path16}: expected condition object, got ${typeof node}`];
25287
25300
  }
25288
25301
  const obj = node;
25289
25302
  if (obj.type === "locked") {
25290
25303
  const validOps2 = VALID_OPERATORS.locked;
25291
25304
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25292
25305
  return [
25293
- `${path15}: invalid operator '${String(obj.operator)}' for locked filter. Valid: ${validOps2.join(", ")}`
25306
+ `${path16}: invalid operator '${String(obj.operator)}' for locked filter. Valid: ${validOps2.join(", ")}`
25294
25307
  ];
25295
25308
  }
25296
25309
  return [];
@@ -25299,12 +25312,12 @@ function conditionIssues(node, path15, fieldMap) {
25299
25312
  const validOps2 = VALID_OPERATORS.current_member;
25300
25313
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25301
25314
  return [
25302
- `${path15}: invalid operator '${String(obj.operator)}' for current_member filter. Valid: ${validOps2.join(", ")}`
25315
+ `${path16}: invalid operator '${String(obj.operator)}' for current_member filter. Valid: ${validOps2.join(", ")}`
25303
25316
  ];
25304
25317
  }
25305
25318
  if (!Array.isArray(obj.value) || obj.value.length === 0 || !obj.value.every((v) => typeof v === "string" && v.length > 0)) {
25306
25319
  return [
25307
- `${path15}: current_member '${obj.operator}' expects a non-empty string[] of group IDs.`
25320
+ `${path16}: current_member '${obj.operator}' expects a non-empty string[] of group IDs.`
25308
25321
  ];
25309
25322
  }
25310
25323
  return [];
@@ -25313,38 +25326,38 @@ function conditionIssues(node, path15, fieldMap) {
25313
25326
  const validOps2 = VALID_OPERATORS.record_id;
25314
25327
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25315
25328
  return [
25316
- `${path15}: invalid operator '${String(obj.operator)}' for record_id filter. Valid: ${validOps2.join(", ")}`
25329
+ `${path16}: invalid operator '${String(obj.operator)}' for record_id filter. Valid: ${validOps2.join(", ")}`
25317
25330
  ];
25318
25331
  }
25319
25332
  if (!Array.isArray(obj.value) || obj.value.length === 0 || !obj.value.every((v) => typeof v === "string" && v.length > 0)) {
25320
- return [`${path15}: record_id '${obj.operator}' expects a non-empty string[] of record ids.`];
25333
+ return [`${path16}: record_id '${obj.operator}' expects a non-empty string[] of record ids.`];
25321
25334
  }
25322
25335
  return [];
25323
25336
  }
25324
25337
  if (typeof obj.field_key !== "string" || !obj.field_key) {
25325
- return [`${path15}: field_key must be a non-empty string`];
25338
+ return [`${path16}: field_key must be a non-empty string`];
25326
25339
  }
25327
25340
  let filterType;
25328
25341
  if (fieldMap) {
25329
25342
  filterType = fieldMap.get(obj.field_key);
25330
25343
  if (filterType === void 0) {
25331
25344
  const available = Array.from(fieldMap.keys()).join(", ");
25332
- return [`${path15}: unknown field_key '${obj.field_key}'. Available: ${available}`];
25345
+ return [`${path16}: unknown field_key '${obj.field_key}'. Available: ${available}`];
25333
25346
  }
25334
25347
  } else if (typeof obj.type === "string") {
25335
25348
  filterType = obj.type;
25336
25349
  }
25337
25350
  if (filterType === "button") {
25338
- return [`${path15}: a button field holds no data and cannot be filtered.`];
25351
+ return [`${path16}: a button field holds no data and cannot be filtered.`];
25339
25352
  }
25340
25353
  if (typeof filterType !== "string" || !VALID_FILTER_TYPES.includes(filterType)) {
25341
25354
  const named = fieldMap && filterType !== obj.type ? `'${String(filterType)}' (resolved from field '${obj.field_key}')` : `'${String(obj.type)}'`;
25342
- return [`${path15}: invalid filter type ${named}. Valid: ${VALID_FILTER_TYPES.join(", ")}`];
25355
+ return [`${path16}: invalid filter type ${named}. Valid: ${VALID_FILTER_TYPES.join(", ")}`];
25343
25356
  }
25344
25357
  const validOps = VALID_OPERATORS[filterType];
25345
25358
  if (typeof obj.operator !== "string" || !validOps.includes(obj.operator)) {
25346
25359
  return [
25347
- `${path15}: invalid operator '${String(obj.operator)}' for ${filterType} filter. Valid: ${validOps.join(", ")}`
25360
+ `${path16}: invalid operator '${String(obj.operator)}' for ${filterType} filter. Valid: ${validOps.join(", ")}`
25348
25361
  ];
25349
25362
  }
25350
25363
  if (obj.unit_option !== void 0 && (filterType !== "number" || typeof obj.unit_option !== "string")) {
@@ -25355,8 +25368,8 @@ function conditionIssues(node, path15, fieldMap) {
25355
25368
  const valueIssue = conditionValueIssue(filterType, obj.operator, obj.value, obj.field_key);
25356
25369
  return valueIssue ? [valueIssue] : [];
25357
25370
  }
25358
- function validateFilterCondition(condition, path15 = "filter") {
25359
- return conditionIssues(condition, path15, void 0);
25371
+ function validateFilterCondition(condition, path16 = "filter") {
25372
+ return conditionIssues(condition, path16, void 0);
25360
25373
  }
25361
25374
  function inheritedFilterType(type, format2, fallback) {
25362
25375
  switch (type) {
@@ -25546,11 +25559,11 @@ function perRowRefusals(value) {
25546
25559
  ...currencyField.map((message2) => ({ key: "currency_field", message: message2 }))
25547
25560
  ];
25548
25561
  }
25549
- function figureIssue(path15) {
25562
+ function figureIssue(path16) {
25550
25563
  return (value, ctx) => {
25551
25564
  const refused = numberUnitRefusal(value.format, value.unit);
25552
- if (refused !== void 0) ctx.addIssue({ code: "custom", message: refused, path: [...path15, "unit"] });
25553
- for (const { key, message: message2 } of perRowRefusals(value)) ctx.addIssue({ code: "custom", message: message2, path: [...path15, key] });
25565
+ if (refused !== void 0) ctx.addIssue({ code: "custom", message: refused, path: [...path16, "unit"] });
25566
+ for (const { key, message: message2 } of perRowRefusals(value)) ctx.addIssue({ code: "custom", message: message2, path: [...path16, key] });
25554
25567
  };
25555
25568
  }
25556
25569
  var tableNumberFieldSchema = tableFieldBaseSchema.extend({
@@ -28030,9 +28043,9 @@ __export(expression_object_exports, {
28030
28043
  pick: () => pick2,
28031
28044
  values: () => values
28032
28045
  });
28033
- function getNestedValue(obj, path15) {
28046
+ function getNestedValue(obj, path16) {
28034
28047
  if (obj == null) return void 0;
28035
- const parts = path15.split(".");
28048
+ const parts = path16.split(".");
28036
28049
  let value = obj;
28037
28050
  for (const part of parts) {
28038
28051
  if (value == null) return void 0;
@@ -28071,11 +28084,11 @@ function entries(obj) {
28071
28084
  }
28072
28085
  return Object.entries(obj);
28073
28086
  }
28074
- function get(obj, path15, defaultValue) {
28075
- if (typeof path15 !== "string") {
28087
+ function get(obj, path16, defaultValue) {
28088
+ if (typeof path16 !== "string") {
28076
28089
  throw new Error("get: path must be a string");
28077
28090
  }
28078
- const value = getNestedValue(obj, path15);
28091
+ const value = getNestedValue(obj, path16);
28079
28092
  return value !== void 0 ? value : defaultValue;
28080
28093
  }
28081
28094
  function pick2(obj, fields) {
@@ -31453,12 +31466,12 @@ function parseFixtureDateExpression(value) {
31453
31466
  function isFixtureScalarValue(value) {
31454
31467
  return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
31455
31468
  }
31456
- function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors) {
31469
+ function checkTraversalFilter(node, path16, contextEntity, fieldByEntity, errors) {
31457
31470
  if (contextEntity === null) {
31458
31471
  errors.push({
31459
31472
  severity: "error",
31460
31473
  rule: "model.filter",
31461
- path: path15,
31474
+ path: path16,
31462
31475
  message: `traversal filter "${node.path[0]}" resolves only directly over a from_table`
31463
31476
  });
31464
31477
  return;
@@ -31470,7 +31483,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31470
31483
  errors.push({
31471
31484
  severity: "error",
31472
31485
  rule: "model.filter",
31473
- path: path15,
31486
+ path: path16,
31474
31487
  message: `traversal path[${index}] "${hop}" is not an entity.field alias`
31475
31488
  });
31476
31489
  return;
@@ -31479,7 +31492,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31479
31492
  errors.push({
31480
31493
  severity: "error",
31481
31494
  rule: "model.filter",
31482
- path: path15,
31495
+ path: path16,
31483
31496
  message: index === 0 ? `traversal path[0] "${hop}" is addressed on entity "${parsed2.entity}", but the filter holding it reads "${current}"` : `traversal path[${index}] "${hop}" is addressed on entity "${parsed2.entity}", but the hop before it lands on "${current}"`
31484
31497
  });
31485
31498
  return;
@@ -31489,7 +31502,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31489
31502
  errors.push({
31490
31503
  severity: "error",
31491
31504
  rule: "model.filter",
31492
- path: path15,
31505
+ path: path16,
31493
31506
  message: `traversal path[${index}] "${hop}" is not a field on entity "${parsed2.entity}"`
31494
31507
  });
31495
31508
  return;
@@ -31498,7 +31511,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31498
31511
  errors.push({
31499
31512
  severity: "error",
31500
31513
  rule: "model.filter",
31501
- path: path15,
31514
+ path: path16,
31502
31515
  message: `traversal path[${index}] "${hop}" is a ${field.type} field, not select_record_link`
31503
31516
  });
31504
31517
  return;
@@ -31512,7 +31525,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31512
31525
  errors.push({
31513
31526
  severity: "error",
31514
31527
  rule: "model.filter",
31515
- path: path15,
31528
+ path: path16,
31516
31529
  message: `traversal condition "${innerField}" must be an entity.field alias on "${current}", the entity its path lands on`
31517
31530
  });
31518
31531
  return;
@@ -31522,12 +31535,12 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31522
31535
  errors.push({
31523
31536
  severity: "error",
31524
31537
  rule: "model.filter",
31525
- path: path15,
31538
+ path: path16,
31526
31539
  message: `traversal condition "${innerField}" is not a field on entity "${parsed.entity}"`
31527
31540
  });
31528
31541
  return;
31529
31542
  }
31530
- checkConditionShape(node.condition, parsed.entity, target, path15, fieldByEntity, errors);
31543
+ checkConditionShape(node.condition, parsed.entity, target, path16, fieldByEntity, errors);
31531
31544
  }
31532
31545
  function contractFilterType(entityAlias, field, fieldByEntity, hops = 0) {
31533
31546
  if (field.type === "formula") return formulaResultType(field.formula);
@@ -31550,7 +31563,7 @@ function formulaSelectOptions(entityAlias, field, fieldByEntity, hops = 0) {
31550
31563
  const shown = fieldByEntity.get(source.target_entity)?.get(field.lookup_field_alias);
31551
31564
  return shown === void 0 ? void 0 : formulaSelectOptions(source.target_entity, shown, fieldByEntity, hops + 1);
31552
31565
  }
31553
- function selectWordRefusals(entityAlias, expression, fieldByEntity, path15) {
31566
+ function selectWordRefusals(entityAlias, expression, fieldByEntity, path16) {
31554
31567
  const words = new Set([...expression.matchAll(/(["'])((?:(?!\1).)*)\1/g)].map((match2) => match2[2]));
31555
31568
  if (words.size === 0) return [];
31556
31569
  return parseFieldRefTokens(expression).flatMap((token2) => {
@@ -31564,19 +31577,19 @@ function selectWordRefusals(entityAlias, expression, fieldByEntity, path15) {
31564
31577
  {
31565
31578
  severity: "error",
31566
31579
  rule: "field.formula",
31567
- path: path15,
31580
+ path: path16,
31568
31581
  message: held.own ? `compares {${token2}} to "${word}", and a select reaches a formula as the KEYS of its options, never their words \u2014 name the option {${token2}:${spelled.alias}}: includes({${token2}}, {${token2}:${spelled.alias}})` : `compares {${token2}} to "${word}", and a looked-up select reaches a formula as the KEYS of its options, never their words \u2014 test it on "${held.entity}", where the select lives ({field:option} names one there), and look that result up`
31569
31582
  }
31570
31583
  ];
31571
31584
  });
31572
31585
  }
31573
- function formulaOptionRefusals(field, path15) {
31586
+ function formulaOptionRefusals(field, path16) {
31574
31587
  const { options, output_type: stated } = field.formula;
31575
31588
  if (options === void 0) {
31576
- return stated === "select" ? [{ severity: "error", rule: "field.formula", path: path15, message: 'states output_type "select" and no options \u2014 a formula yields one of the options it declares' }] : [];
31589
+ return stated === "select" ? [{ severity: "error", rule: "field.formula", path: path16, message: 'states output_type "select" and no options \u2014 a formula yields one of the options it declares' }] : [];
31577
31590
  }
31578
31591
  const refusals = [];
31579
- const refuse = (message2) => refusals.push({ severity: "error", rule: "field.formula", path: path15, message: message2 });
31592
+ const refuse = (message2) => refusals.push({ severity: "error", rule: "field.formula", path: path16, message: message2 });
31580
31593
  if (stated !== void 0 && stated !== "select") refuse(`states output_type "${stated}" beside options \u2014 a formula with options yields one of them`);
31581
31594
  const figure = ["format", "currency", "unit", "unit_field", "currency_field"].filter((key) => field.formula[key] !== void 0);
31582
31595
  if (figure.length > 0) refuse(`states ${figure.join(", ")} beside options \u2014 a formula yielding an option is no figure`);
@@ -31596,12 +31609,12 @@ function formulaOptionRefusals(field, path15) {
31596
31609
  }
31597
31610
  return refusals;
31598
31611
  }
31599
- function checkConditionShape(condition, entityAlias, field, path15, fieldByEntity, errors) {
31612
+ function checkConditionShape(condition, entityAlias, field, path16, fieldByEntity, errors) {
31600
31613
  const type = contractFilterType(entityAlias, field, fieldByEntity);
31601
31614
  if (type === void 0 && condition.type === void 0) return;
31602
31615
  const typed = type === void 0 ? condition : { ...condition, type };
31603
- for (const message2 of validateFilterCondition(typed, path15)) {
31604
- errors.push({ severity: "error", rule: "model.filter", path: path15, message: message2.startsWith(`${path15}: `) ? message2.slice(path15.length + 2) : message2 });
31616
+ for (const message2 of validateFilterCondition(typed, path16)) {
31617
+ errors.push({ severity: "error", rule: "model.filter", path: path16, message: message2.startsWith(`${path16}: `) ? message2.slice(path16.length + 2) : message2 });
31605
31618
  }
31606
31619
  if (field.type !== "select") return;
31607
31620
  const declared = new Set(field.options.map((option) => option.alias));
@@ -31611,20 +31624,20 @@ function checkConditionShape(condition, entityAlias, field, path15, fieldByEntit
31611
31624
  errors.push({
31612
31625
  severity: "error",
31613
31626
  rule: "model.filter",
31614
- path: path15,
31627
+ path: path16,
31615
31628
  message: `filters field "${field.alias}" on option "${raw}", which that field does not declare (${declared.size > 0 ? [...declared].join(", ") : "it declares none"})`
31616
31629
  });
31617
31630
  }
31618
31631
  }
31619
- function checkFilterFields(filters, path15, contextEntity, fieldByEntity, errors, checkField) {
31632
+ function checkFilterFields(filters, path16, contextEntity, fieldByEntity, errors, checkField) {
31620
31633
  if (filters.node_type === "group") {
31621
31634
  for (const child of filters.children) {
31622
- checkFilterFields(child, path15, contextEntity, fieldByEntity, errors, checkField);
31635
+ checkFilterFields(child, path16, contextEntity, fieldByEntity, errors, checkField);
31623
31636
  }
31624
31637
  return;
31625
31638
  }
31626
31639
  if (filters.node_type === "traversal") {
31627
- checkTraversalFilter(filters, path15, contextEntity, fieldByEntity, errors);
31640
+ checkTraversalFilter(filters, path16, contextEntity, fieldByEntity, errors);
31628
31641
  return;
31629
31642
  }
31630
31643
  if (filters.type === "current_member") return;
@@ -31632,7 +31645,7 @@ function checkFilterFields(filters, path15, contextEntity, fieldByEntity, errors
31632
31645
  checkField(filters.field_key);
31633
31646
  if (contextEntity === null) return;
31634
31647
  const field = fieldByEntity.get(contextEntity)?.get(filters.field_key);
31635
- if (field !== void 0) checkConditionShape(filters, contextEntity, field, path15, fieldByEntity, errors);
31648
+ if (field !== void 0) checkConditionShape(filters, contextEntity, field, path16, fieldByEntity, errors);
31636
31649
  }
31637
31650
  function findDuplicates(aliases) {
31638
31651
  const seen = /* @__PURE__ */ new Set();
@@ -31647,19 +31660,19 @@ function checkUniqueTuples(entity, fields) {
31647
31660
  const errors = [];
31648
31661
  const seen = /* @__PURE__ */ new Set();
31649
31662
  for (const [index, tuple2] of (entity.unique ?? []).entries()) {
31650
- const path15 = `entities.${entity.alias}.unique.${index}`;
31663
+ const path16 = `entities.${entity.alias}.unique.${index}`;
31651
31664
  const key = [...new Set(tuple2)].sort().join("+");
31652
31665
  if (new Set(tuple2).size !== tuple2.length) {
31653
- errors.push({ severity: "error", rule: "entity.unique", path: path15, message: `names a field twice \u2014 a set of fields names each once` });
31666
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `names a field twice \u2014 a set of fields names each once` });
31654
31667
  }
31655
31668
  if (seen.has(key)) {
31656
- errors.push({ severity: "error", rule: "entity.unique", path: path15, message: `states the set ${tuple2.join(" + ")} again \u2014 once is the rule` });
31669
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `states the set ${tuple2.join(" + ")} again \u2014 once is the rule` });
31657
31670
  }
31658
31671
  seen.add(key);
31659
31672
  for (const alias2 of tuple2) {
31660
31673
  const field = fields.get(alias2);
31661
31674
  if (field === void 0) {
31662
- errors.push({ severity: "error", rule: "entity.unique", path: path15, message: `names "${alias2}", which is not a field of "${entity.alias}"` });
31675
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `names "${alias2}", which is not a field of "${entity.alias}"` });
31663
31676
  continue;
31664
31677
  }
31665
31678
  const admitted = field.type === "select" && field.multi !== true || field.type === "select_record_link" && field.cardinality === "one" || field.type === "text" || field.type === "number" || field.type === "date";
@@ -31667,14 +31680,14 @@ function checkUniqueTuples(entity, fields) {
31667
31680
  errors.push({
31668
31681
  severity: "error",
31669
31682
  rule: "entity.unique",
31670
- path: path15,
31683
+ path: path16,
31671
31684
  message: `names "${alias2}", which holds no single value to compare (a ${field.type}) \u2014 a unique set takes text, number, date, a single select, or a link of cardinality "one"`
31672
31685
  });
31673
31686
  }
31674
31687
  }
31675
31688
  const [only] = tuple2;
31676
31689
  if (tuple2.length === 1 && fields.get(only)?.type === "text") {
31677
- errors.push({ severity: "error", rule: "entity.unique", path: path15, message: `is "${only}" alone \u2014 declare \`unique: true\` on the text field itself` });
31690
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `is "${only}" alone \u2014 declare \`unique: true\` on the text field itself` });
31678
31691
  }
31679
31692
  }
31680
31693
  return errors;
@@ -32567,8 +32580,8 @@ var MODEL_RULES = {
32567
32580
  "check.resolve": { sentence: "`resolve` names an act of this app on the rows the check stands on, pressed on one row, not refused by the check, and drawn as a button \u2014 never a correction." },
32568
32581
  "check.restates-meter": { sentence: "A warning reading a gated meter's figure against its bound or pass mark is refused \u2014 the meter draws it." }
32569
32582
  };
32570
- var error51 = (rule, path15, message2) => ({ severity: "error", path: path15, message: message2, rule });
32571
- var note = (rule, path15, message2) => ({ severity: "note", path: path15, message: message2, rule });
32583
+ var error51 = (rule, path16, message2) => ({ severity: "error", path: path16, message: message2, rule });
32584
+ var note = (rule, path16, message2) => ({ severity: "note", path: path16, message: message2, rule });
32572
32585
 
32573
32586
  // ../shared/src/schemas/app_spec_version.ts
32574
32587
  var APP_SPEC_VERSION = 13;
@@ -35073,9 +35086,9 @@ function templateFields(content) {
35073
35086
  }
35074
35087
 
35075
35088
  // ../shared/src/schemas/app_model_validate_parts.ts
35076
- function checkOwnRows(model, entity, field, path15) {
35089
+ function checkOwnRows(model, entity, field, path16) {
35077
35090
  if (field.type !== "select_record_link" || !childList(model, entity, field)) return [];
35078
- return [error51("app.own-rows", path15, `"${field.alias}" lists "${field.target_entity}" rows that each belong to one "${entity.alias}" \u2014 they are this record's own rows: read them as a block, \`{ "rows": "${field.target_entity}" }\``)];
35091
+ return [error51("app.own-rows", path16, `"${field.alias}" lists "${field.target_entity}" rows that each belong to one "${entity.alias}" \u2014 they are this record's own rows: read them as a block, \`{ "rows": "${field.target_entity}" }\``)];
35079
35092
  }
35080
35093
  function checkWhen(entities, entity, when, at2, place) {
35081
35094
  const findings = [];
@@ -35118,21 +35131,21 @@ function checkNarrowing(entities, entity, narrowing, at2) {
35118
35131
  return [...mine, ...checkWhen(entities, entity, conditions, at2, "offer")];
35119
35132
  }
35120
35133
  var MADE_FROM = ["html", "excel"];
35121
- function checkTemplate(model, template, path15) {
35134
+ function checkTemplate(model, template, path16) {
35122
35135
  if (model.templates === void 0) return [];
35123
35136
  const declared = model.templates.find((one) => one.alias === template);
35124
- if (declared === void 0) return [error51("model.names-declared", path15, `names template "${template}", which this model does not declare`)];
35137
+ if (declared === void 0) return [error51("model.names-declared", path16, `names template "${template}", which this model does not declare`)];
35125
35138
  if (!MADE_FROM.includes(declared.type)) {
35126
- return [error51("model.template-kind", path15, `names the ${declared.type} template "${template}" \u2014 a document is made from an html or an excel one`)];
35139
+ return [error51("model.template-kind", path16, `names the ${declared.type} template "${template}" \u2014 a document is made from an html or an excel one`)];
35127
35140
  }
35128
35141
  return [];
35129
35142
  }
35130
- function checkExportTemplate(model, entity, tabs, template, path15) {
35131
- const kind = checkTemplate(model, template, path15);
35143
+ function checkExportTemplate(model, entity, tabs, template, path16) {
35144
+ const kind = checkTemplate(model, template, path16);
35132
35145
  if (kind.length > 0 || model.templates === void 0) return kind;
35133
35146
  const reports = APP_EXPORT_REPORT_KEYS;
35134
35147
  if (reports.includes(entity)) {
35135
- return [error51("register.export-template", path15, `lists its rows under "${entity}", a key the report fills itself (${reports.join(", ")}) \u2014 rename the entity`)];
35148
+ return [error51("register.export-template", path16, `lists its rows under "${entity}", a key the report fills itself (${reports.join(", ")}) \u2014 rename the entity`)];
35136
35149
  }
35137
35150
  const fed = [...reports, entity, ...tabs];
35138
35151
  const content = templateContent(model, template);
@@ -35141,8 +35154,8 @@ function checkExportTemplate(model, entity, tabs, template, path15) {
35141
35154
  (app) => isDashboardApp(app) ? [] : (app.acts ?? []).flatMap((act) => [act.template, ...(act.templates ?? []).map((one) => one.template)].includes(template) ? [`${app.alias}.${act.alias}`] : [])
35142
35155
  );
35143
35156
  return [
35144
- ...unfed.length === 0 ? [] : [error51("register.export-template", path15, `reads ${unfed.map((root) => `"${root}"`).join(", ")} at its root, which an export does not fill \u2014 it fills ${fed.map((key) => `"${key}"`).join(", ")}`)],
35145
- ...acts.length === 0 ? [] : [error51("register.export-template", path15, `is also the document of ${acts.map((act) => `"${act}"`).join(", ")} \u2014 an act fills its template with a record's fields, an export with the report; give each its own`)]
35157
+ ...unfed.length === 0 ? [] : [error51("register.export-template", path16, `reads ${unfed.map((root) => `"${root}"`).join(", ")} at its root, which an export does not fill \u2014 it fills ${fed.map((key) => `"${key}"`).join(", ")}`)],
35158
+ ...acts.length === 0 ? [] : [error51("register.export-template", path16, `is also the document of ${acts.map((act) => `"${act}"`).join(", ")} \u2014 an act fills its template with a record's fields, an export with the report; give each its own`)]
35146
35159
  ];
35147
35160
  }
35148
35161
 
@@ -35192,9 +35205,9 @@ function checkReading(model, reading, at2) {
35192
35205
  const entity = entityOf(model, alias2);
35193
35206
  if (entity === void 0) return [error51("model.names-declared", `${at2}.${key}`, `names entity "${alias2}", which this model does not declare`)];
35194
35207
  const findings = [];
35195
- const own = (name, path15) => {
35208
+ const own = (name, path16) => {
35196
35209
  const found = fieldOf(entity, name);
35197
- if (found === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`));
35210
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
35198
35211
  return found;
35199
35212
  };
35200
35213
  if ("value" in reading && reading.value !== void 0) {
@@ -35231,31 +35244,31 @@ function checkReading(model, reading, at2) {
35231
35244
  if (refused !== void 0) findings.push(error51("reading.over", `${at2}.over`, `"${reading.over}" ${refused}`));
35232
35245
  }
35233
35246
  for (const [name, held] of Object.entries(reading.where ?? {})) {
35234
- const path15 = `${at2}.where.${name}`;
35235
- const named = own(name, path15);
35247
+ const path16 = `${at2}.where.${name}`;
35248
+ const named = own(name, path16);
35236
35249
  if (named === void 0) continue;
35237
35250
  if (held === "me") {
35238
- if (named.type !== "select_member") findings.push(error51("reading.where", path15, `"${name}" is ${kindOf(named, entity, model.entities)} \u2014 "me" narrows a member field to the reader`));
35251
+ if (named.type !== "select_member") findings.push(error51("reading.where", path16, `"${name}" is ${kindOf(named, entity, model.entities)} \u2014 "me" narrows a member field to the reader`));
35239
35252
  continue;
35240
35253
  }
35241
35254
  const read = readField(model, entity, named);
35242
35255
  if (read === void 0 || read.several) {
35243
- findings.push(error51("reading.where", path15, `"${name}" is a lookup through a link holding several rows \u2014 a reading keeps a row by one value: look it up through links to one row`));
35256
+ findings.push(error51("reading.where", path16, `"${name}" is a lookup through a link holding several rows \u2014 a reading keeps a row by one value: look it up through links to one row`));
35244
35257
  continue;
35245
35258
  }
35246
35259
  const { field, entity: holder } = read;
35247
35260
  if (typeof held === "boolean") {
35248
- if (!isYesNo(field, holder, model.entities)) findings.push(error51("reading.where", path15, `"${name}" is ${kindOf(field, holder, model.entities)} \u2014 true or false keeps rows by a yes/no, stored or a formula of one`));
35261
+ if (!isYesNo(field, holder, model.entities)) findings.push(error51("reading.where", path16, `"${name}" is ${kindOf(field, holder, model.entities)} \u2014 true or false keeps rows by a yes/no, stored or a formula of one`));
35249
35262
  continue;
35250
35263
  }
35251
35264
  const options = contractSelectOptions(field);
35252
35265
  if (options === void 0) {
35253
- findings.push(error51("reading.where", path15, `"${name}" is a ${field.type} field \u2014 options keep rows by a select`));
35266
+ findings.push(error51("reading.where", path16, `"${name}" is a ${field.type} field \u2014 options keep rows by a select`));
35254
35267
  continue;
35255
35268
  }
35256
35269
  const declared = new Set(options.map((option) => option.alias));
35257
35270
  for (const [index, option] of held.entries()) {
35258
- if (!declared.has(option)) findings.push(error51("model.names-declared", `${path15}.${index}`, `names "${option}", which is not an option of "${name}"`));
35271
+ if (!declared.has(option)) findings.push(error51("model.names-declared", `${path16}.${index}`, `names "${option}", which is not an option of "${name}"`));
35259
35272
  }
35260
35273
  }
35261
35274
  if ("list" in reading) {
@@ -35279,18 +35292,18 @@ function checkMilestones(entities, entity, display, at2) {
35279
35292
  const findings = [];
35280
35293
  const seen = /* @__PURE__ */ new Set();
35281
35294
  for (const [index, stage] of stages.entries()) {
35282
- const path15 = `${at2}.status.milestones.${index}`;
35295
+ const path16 = `${at2}.status.milestones.${index}`;
35283
35296
  const date5 = fieldOf(entity, stage.field);
35284
35297
  if (date5 === void 0) {
35285
- findings.push(error51("model.names-declared", path15, `names "${stage.field}", which is not a field of "${entity.alias}"`));
35298
+ findings.push(error51("model.names-declared", path16, `names "${stage.field}", which is not a field of "${entity.alias}"`));
35286
35299
  continue;
35287
35300
  }
35288
35301
  if (date5.type !== "date" || !isAsked(date5)) {
35289
- findings.push(error51("records.milestones", path15, `"${stage.field}" is a ${date5.type === "date" ? "date the workspace stamps" : `${date5.type} field`} \u2014 a milestone is a date a person ticks`));
35302
+ findings.push(error51("records.milestones", path16, `"${stage.field}" is a ${date5.type === "date" ? "date the workspace stamps" : `${date5.type} field`} \u2014 a milestone is a date a person ticks`));
35290
35303
  }
35291
- if (seen.has(stage.field)) findings.push(error51("records.milestones", path15, `"${stage.field}" is already a milestone \u2014 each date marks one stage`));
35304
+ if (seen.has(stage.field)) findings.push(error51("records.milestones", path16, `"${stage.field}" is already a milestone \u2014 each date marks one stage`));
35292
35305
  seen.add(stage.field);
35293
- findings.push(...checkWhen(entities, entity, stage.when, `${path15}.when`, "stage"));
35306
+ findings.push(...checkWhen(entities, entity, stage.when, `${path16}.when`, "stage"));
35294
35307
  }
35295
35308
  for (const [index, closed] of (display.status?.closed ?? []).entries()) {
35296
35309
  if (!seen.has(closed)) findings.push(error51("records.milestones", `${at2}.status.closed.${index}`, `names "${closed}", which is not one of the milestones`));
@@ -35301,11 +35314,11 @@ function checkMilestones(entities, entity, display, at2) {
35301
35314
  }
35302
35315
  return findings;
35303
35316
  }
35304
- function checkCreateMilestones(model, entity, asked, path15) {
35317
+ function checkCreateMilestones(model, entity, asked, path16) {
35305
35318
  const stages = milestonesOf(recordDisplay(model, entity)) ?? [];
35306
35319
  const later = new Set(stages.slice(1).map((stage) => stage.field));
35307
35320
  return asked.flatMap(
35308
- (name, index) => later.has(name) ? [error51("records.milestone-add", `${path15}.${index}`, `asks "${name}", a later milestone of "${entity}" \u2014 a row starts at the first, and each after it is ticked in order`)] : []
35321
+ (name, index) => later.has(name) ? [error51("records.milestone-add", `${path16}.${index}`, `asks "${name}", a later milestone of "${entity}" \u2014 a row starts at the first, and each after it is ticked in order`)] : []
35309
35322
  );
35310
35323
  }
35311
35324
 
@@ -35316,9 +35329,9 @@ function checkAppRow(model, app, at2) {
35316
35329
  const display = model.records?.[app.entity];
35317
35330
  if (row === void 0 || entity === void 0 || display === void 0) return [];
35318
35331
  const findings = [];
35319
- const field = (name, path15) => {
35332
+ const field = (name, path16) => {
35320
35333
  const found = fieldOf(entity, name);
35321
- if (found === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`));
35334
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
35322
35335
  return found;
35323
35336
  };
35324
35337
  const same = (part) => error51("app.row", `${at2}.row.${part}`, `restates \`records.${entity.alias}.${part}\` \u2014 a row reads it there already; drop it`);
@@ -35349,13 +35362,13 @@ function checkAppRow(model, app, at2) {
35349
35362
  }
35350
35363
  function checkApplies(model, entity, display, at2) {
35351
35364
  return Object.entries(display.applies ?? {}).flatMap(([name, conditions]) => {
35352
- const path15 = `${at2}.applies.${name}`;
35365
+ const path16 = `${at2}.applies.${name}`;
35353
35366
  const applying = fieldOf(entity, name);
35354
- if (applying === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
35355
- if (name === display.title) return [error51("records.applies", path15, `"${name}" is the row's title, which names it wherever it is drawn \u2014 it always applies`)];
35356
- const own = applying.required === true && !hasDefault(applying) ? [error51("records.applies", path15, `"${name}" is required and starts at no \`default\` \u2014 where it does not apply no add asks it, so a row could not be made; give it a default, or make it optional`)] : [];
35367
+ if (applying === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
35368
+ if (name === display.title) return [error51("records.applies", path16, `"${name}" is the row's title, which names it wherever it is drawn \u2014 it always applies`)];
35369
+ const own = applying.required === true && !hasDefault(applying) ? [error51("records.applies", path16, `"${name}" is required and starts at no \`default\` \u2014 where it does not apply no add asks it, so a row could not be made; give it a default, or make it optional`)] : [];
35357
35370
  const held = Object.entries(conditions).flatMap(([condition, value]) => {
35358
- const at3 = `${path15}.${condition}`;
35371
+ const at3 = `${path16}.${condition}`;
35359
35372
  if (condition === name) return [error51("records.applies", at3, `"${name}" applies by itself \u2014 a field applies by another of the row's`)];
35360
35373
  const shape = appliesCondition(model, entity, condition);
35361
35374
  if ("refused" in shape) return [error51(shape.refused.startsWith("names") ? "model.names-declared" : "records.applies", at3, shape.refused)];
@@ -35378,9 +35391,9 @@ function checkRecords(model) {
35378
35391
  findings.push(error51("model.names-declared", at2, `names entity "${alias2}", which this model does not declare`));
35379
35392
  continue;
35380
35393
  }
35381
- const field = (name, path15) => {
35394
+ const field = (name, path16) => {
35382
35395
  const found = fieldOf(entity, name);
35383
- if (found === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${alias2}"`));
35396
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${alias2}"`));
35384
35397
  return found;
35385
35398
  };
35386
35399
  const valueType = (found) => resolvedFieldType(found, entity, model.entities);
@@ -35450,14 +35463,14 @@ function checkRecords(model) {
35450
35463
  }
35451
35464
  }
35452
35465
  for (const [name, start] of Object.entries(display.starts ?? {})) {
35453
- const path15 = `${at2}.starts.${name}`;
35454
- const started = field(name, path15);
35466
+ const path16 = `${at2}.starts.${name}`;
35467
+ const started = field(name, path16);
35455
35468
  if (started === void 0) continue;
35456
- if (!isAsked(started)) findings.push(error51("records.starts", path15, `"${name}" is a ${started.type} field nobody writes, so an add never starts it`));
35469
+ if (!isAsked(started)) findings.push(error51("records.starts", path16, `"${name}" is a ${started.type} field nobody writes, so an add never starts it`));
35457
35470
  else if (start === "today" && (started.type !== "date" || started.format === "date_range" || started.format === "datetime_range")) {
35458
- findings.push(error51("records.starts", path15, `"${name}" is ${started.type === "date" ? "a span of days" : `a ${started.type} field`} \u2014 \`today\` starts a date or a moment`));
35471
+ findings.push(error51("records.starts", path16, `"${name}" is ${started.type === "date" ? "a span of days" : `a ${started.type} field`} \u2014 \`today\` starts a date or a moment`));
35459
35472
  } else if (start === "me" && started.type !== "select_member") {
35460
- findings.push(error51("records.starts", path15, `"${name}" is a ${started.type} field \u2014 \`me\` starts a member field at the reader adding the row`));
35473
+ findings.push(error51("records.starts", path16, `"${name}" is a ${started.type} field \u2014 \`me\` starts a member field at the reader adding the row`));
35461
35474
  }
35462
35475
  }
35463
35476
  for (const [index, name] of (display.due ?? []).entries()) {
@@ -35656,30 +35669,30 @@ function optionsWhereConditions(filter2, target) {
35656
35669
  }
35657
35670
  return out.length === 0 ? void 0 : out;
35658
35671
  }
35659
- function checkBoundRef(entity, field, ref, byAlias, path15) {
35672
+ function checkBoundRef(entity, field, ref, byAlias, path16) {
35660
35673
  const entities = [...byAlias.values()];
35661
35674
  let on = entity;
35662
35675
  if ("link" in ref) {
35663
35676
  const link = entity.fields.find((one) => one.alias === ref.link);
35664
35677
  if (link === void 0 || link.type !== "select_record_link" || link.cardinality !== "one") {
35665
- return [error51("write.bounds", path15, `reads "${ref.link}", which is not a one-row link of ${entity.label} \u2014 a bound is read off this row or the one row a link names`)];
35678
+ return [error51("write.bounds", path16, `reads "${ref.link}", which is not a one-row link of ${entity.label} \u2014 a bound is read off this row or the one row a link names`)];
35666
35679
  }
35667
35680
  const target = byAlias.get(link.target_entity);
35668
- if (target === void 0) return [error51("model.names-declared", path15, `reads "${ref.link}", whose entity "${link.target_entity}" this model does not declare`)];
35681
+ if (target === void 0) return [error51("model.names-declared", path16, `reads "${ref.link}", whose entity "${link.target_entity}" this model does not declare`)];
35669
35682
  on = target;
35670
35683
  }
35671
35684
  const source = on.fields.find((one) => one.alias === ref.field);
35672
- if (source === void 0) return [error51("model.names-declared", path15, `names "${ref.field}", which is not a field of ${on.label}`)];
35673
- if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds", path15, `bounds ${field.label} by itself`)];
35685
+ if (source === void 0) return [error51("model.names-declared", path16, `names "${ref.field}", which is not a field of ${on.label}`)];
35686
+ if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds", path16, `bounds ${field.label} by itself`)];
35674
35687
  const from = resolvedFieldType(source, on, entities);
35675
35688
  const into = resolvedFieldType(field, entity, entities);
35676
- if (from !== into) return [error51("write.bounds", path15, `bounds ${field.label} (${String(into)}) by "${source.label}" (${String(from)}) \u2014 a bound is of the value's own type`)];
35689
+ if (from !== into) return [error51("write.bounds", path16, `bounds ${field.label} (${String(into)}) by "${source.label}" (${String(from)}) \u2014 a bound is of the value's own type`)];
35677
35690
  const apart = boundUnitRefusal(field, source, entity, entities, "link" in ref ? { link: ref.link, on } : void 0);
35678
- return apart === void 0 ? [] : [error51("write.bounds", path15, apart)];
35691
+ return apart === void 0 ? [] : [error51("write.bounds", path16, apart)];
35679
35692
  }
35680
- function checkSame(entity, field, same, byAlias, path15) {
35693
+ function checkSame(entity, field, same, byAlias, path16) {
35681
35694
  if (field.type !== "select_record_link") {
35682
- return [error51("write.same", path15, `${field.label} is a ${field.type} field \u2014 \`same\` narrows the rows a LINK points at`)];
35695
+ return [error51("write.same", path16, `${field.label} is a ${field.type} field \u2014 \`same\` narrows the rows a LINK points at`)];
35683
35696
  }
35684
35697
  const target = byAlias.get(field.target_entity);
35685
35698
  if (target === void 0) return [];
@@ -35687,7 +35700,7 @@ function checkSame(entity, field, same, byAlias, path15) {
35687
35700
  return same.flatMap((alias2, index) => {
35688
35701
  const own = oneLink(entity, alias2);
35689
35702
  const theirs = oneLink(target, alias2);
35690
- const at2 = `${path15}.${index}`;
35703
+ const at2 = `${path16}.${index}`;
35691
35704
  if (own === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${entity.label}`)];
35692
35705
  if (theirs === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${target.label} \u2014 the row ${field.label} points at names nothing to compare`)];
35693
35706
  if (own.type !== "select_record_link" || theirs.type !== "select_record_link" || own.target_entity !== theirs.target_entity) {
@@ -35696,14 +35709,14 @@ function checkSame(entity, field, same, byAlias, path15) {
35696
35709
  return [];
35697
35710
  });
35698
35711
  }
35699
- function checkSuggest(entity, field, stated, byAlias, path15) {
35700
- if (field.type !== "text") return [error51("write.suggest", path15, `${field.label} is a ${field.type} field \u2014 \`suggest\` offers values to a text field a person types`)];
35712
+ function checkSuggest(entity, field, stated, byAlias, path16) {
35713
+ if (field.type !== "text") return [error51("write.suggest", path16, `${field.label} is a ${field.type} field \u2014 \`suggest\` offers values to a text field a person types`)];
35701
35714
  const ref = suggestRef(stated);
35702
35715
  const catalog = byAlias.get(ref.entity);
35703
- if (catalog === void 0) return [error51("model.names-declared", path15, `names entity "${ref.entity}", which this model does not declare`)];
35716
+ if (catalog === void 0) return [error51("model.names-declared", path16, `names entity "${ref.entity}", which this model does not declare`)];
35704
35717
  const source = catalog.fields.find((one) => one.alias === ref.field);
35705
- if (source === void 0) return [error51("model.names-declared", path15, `names "${ref.field}", which is not a field of ${catalog.label}`)];
35706
- if (source.type !== "text") return [error51("write.suggest", path15, `offers the values of "${source.label}", a ${source.type} field of ${catalog.label} \u2014 the values offered are a stored text field's`)];
35718
+ if (source === void 0) return [error51("model.names-declared", path16, `names "${ref.field}", which is not a field of ${catalog.label}`)];
35719
+ if (source.type !== "text") return [error51("write.suggest", path16, `offers the values of "${source.label}", a ${source.type} field of ${catalog.label} \u2014 the values offered are a stored text field's`)];
35707
35720
  return [];
35708
35721
  }
35709
35722
  function checkNoOverlap(entity, rule, at2) {
@@ -35764,50 +35777,50 @@ function checkWriteRules(entities, rules) {
35764
35777
  }
35765
35778
  }
35766
35779
  for (const [fieldAlias2, rule] of Object.entries(entry.fields ?? {})) {
35767
- const path15 = `${at2}.fields.${fieldAlias2}`;
35780
+ const path16 = `${at2}.fields.${fieldAlias2}`;
35768
35781
  const field = fieldByAlias.get(fieldAlias2);
35769
35782
  if (field === void 0) {
35770
- findings.push(error51("model.names-declared", path15, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
35783
+ findings.push(error51("model.names-declared", path16, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
35771
35784
  continue;
35772
35785
  }
35773
35786
  if (rule.default_from !== void 0) {
35774
- findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path15}.default_from`));
35787
+ findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path16}.default_from`));
35775
35788
  }
35776
35789
  const bounded = resolvedFieldType(field, entity, entities);
35777
35790
  if ((rule.min !== void 0 || rule.max !== void 0) && !BOUNDED_TYPES.includes(bounded ?? field.type)) {
35778
- findings.push(error51("write.bounds", path15, `${field.label} is a ${field.type} field \u2014 \`min\`/\`max\` bound a number or a date`));
35791
+ findings.push(error51("write.bounds", path16, `${field.label} is a ${field.type} field \u2014 \`min\`/\`max\` bound a number or a date`));
35779
35792
  }
35780
35793
  for (const side of ["min", "max"]) {
35781
35794
  const bound = rule[side];
35782
35795
  if (typeof bound === "number" && bounded === "date") {
35783
- findings.push(error51("write.bounds", `${path15}.${side}`, `bounds the date ${field.label} by a number \u2014 a date is bounded by a date field`));
35796
+ findings.push(error51("write.bounds", `${path16}.${side}`, `bounds the date ${field.label} by a number \u2014 a date is bounded by a date field`));
35784
35797
  }
35785
35798
  const perRow = typeof bound === "number" && bound !== 0 ? contractUnitSource(field, entity, entities) : void 0;
35786
35799
  if (perRow !== void 0) {
35787
- findings.push(error51("write.bound-per-row-unit", `${path15}.${side}`, `bounds ${field.label} by ${String(bound)}, which is in no ${perRow.vocabulary}, and each row reads it in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds \u2014 only 0 is the same in every ${perRow.vocabulary}`));
35800
+ findings.push(error51("write.bound-per-row-unit", `${path16}.${side}`, `bounds ${field.label} by ${String(bound)}, which is in no ${perRow.vocabulary}, and each row reads it in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds \u2014 only 0 is the same in every ${perRow.vocabulary}`));
35788
35801
  }
35789
35802
  const ref = boundRef(bound);
35790
- if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path15}.${side}`));
35803
+ if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path16}.${side}`));
35791
35804
  }
35792
35805
  if (typeof rule.min === "number" && typeof rule.max === "number" && rule.min > rule.max) {
35793
- findings.push(error51("write.bounds", `${path15}.min`, `is above \`max\` (${rule.min} > ${rule.max}) \u2014 the bound refuses every figure`));
35806
+ findings.push(error51("write.bounds", `${path16}.min`, `is above \`max\` (${rule.min} > ${rule.max}) \u2014 the bound refuses every figure`));
35794
35807
  }
35795
- if (rule.same !== void 0) findings.push(...checkSame(entity, field, rule.same, byAlias, `${path15}.same`));
35796
- if (rule.suggest !== void 0) findings.push(...checkSuggest(entity, field, rule.suggest, byAlias, `${path15}.suggest`));
35808
+ if (rule.same !== void 0) findings.push(...checkSame(entity, field, rule.same, byAlias, `${path16}.same`));
35809
+ if (rule.suggest !== void 0) findings.push(...checkSuggest(entity, field, rule.suggest, byAlias, `${path16}.suggest`));
35797
35810
  if (rule.options_where === void 0) continue;
35798
35811
  if (field.type !== "select_record_link") {
35799
- findings.push(error51("write.options-where", `${path15}.options_where`, `${field.label} is a ${field.type} field \u2014 \`options_where\` narrows the rows a LINK may point at`));
35812
+ findings.push(error51("write.options-where", `${path16}.options_where`, `${field.label} is a ${field.type} field \u2014 \`options_where\` narrows the rows a LINK may point at`));
35800
35813
  continue;
35801
35814
  }
35802
35815
  const target = byAlias.get(field.target_entity);
35803
35816
  if (target === void 0) continue;
35804
35817
  const conditions = optionsWhereConditions(rule.options_where, target);
35805
35818
  if (conditions === void 0) {
35806
- findings.push(error51("write.options-where", `${path15}.options_where`, `is not a narrowing this clause can both filter a picker by and refuse a write with: an \`and\` group of plain conditions over ${target.label}'s own fields, each \`field_key\` a field alias and each operator one of ${Object.entries(OPTIONS_WHERE_OPERATORS).map(([type, ops]) => `${type} ${ops.join("/")}`).join("; ")}`));
35819
+ findings.push(error51("write.options-where", `${path16}.options_where`, `is not a narrowing this clause can both filter a picker by and refuse a write with: an \`and\` group of plain conditions over ${target.label}'s own fields, each \`field_key\` a field alias and each operator one of ${Object.entries(OPTIONS_WHERE_OPERATORS).map(([type, ops]) => `${type} ${ops.join("/")}`).join("; ")}`));
35807
35820
  continue;
35808
35821
  }
35809
35822
  for (const condition of conditions) {
35810
- const at3 = `${path15}.options_where`;
35823
+ const at3 = `${path16}.options_where`;
35811
35824
  const perRow = condition.type === "number" ? contractUnitSource(condition.field, target, entities) : void 0;
35812
35825
  if (perRow !== void 0) {
35813
35826
  findings.push(error51("write.options-where", at3, `compares "${condition.field.label}", which each row reads in the ${perRow.vocabulary} its ${unitSourceName(perRow)} holds, with a bare number \u2014 a narrowing compares a figure read in one ${perRow.vocabulary} for every row`));
@@ -35831,42 +35844,42 @@ function checkWriteRules(entities, rules) {
35831
35844
  }
35832
35845
  return findings;
35833
35846
  }
35834
- function checkDefaultFrom(entity, field, stated, byAlias, path15) {
35847
+ function checkDefaultFrom(entity, field, stated, byAlias, path16) {
35835
35848
  const entities = [...byAlias.values()];
35836
35849
  const [linkAlias, sourceAlias] = stated.split(".");
35837
35850
  const link = entity.fields.find((one) => one.alias === linkAlias);
35838
35851
  if (link === void 0 || link.type !== "select_record_link") {
35839
35852
  return [
35840
- error51("write.default-from", path15, `names "${linkAlias}", which is not a link field of ${entity.label} \u2014 a default is copied from a row this one points at`)
35853
+ error51("write.default-from", path16, `names "${linkAlias}", which is not a link field of ${entity.label} \u2014 a default is copied from a row this one points at`)
35841
35854
  ];
35842
35855
  }
35843
35856
  if (link.cardinality !== "one") {
35844
35857
  return [
35845
- error51("write.default-from", path15, `reads "${link.label}", which holds MANY rows \u2014 a default copied from a list would take whichever row came back first`)
35858
+ error51("write.default-from", path16, `reads "${link.label}", which holds MANY rows \u2014 a default copied from a list would take whichever row came back first`)
35846
35859
  ];
35847
35860
  }
35848
35861
  const target = byAlias.get(link.target_entity);
35849
35862
  const source = target?.fields.find((one) => one.alias === sourceAlias);
35850
35863
  if (target === void 0 || source === void 0) {
35851
35864
  return [
35852
- error51("model.names-declared", path15, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
35865
+ error51("model.names-declared", path16, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
35853
35866
  ];
35854
35867
  }
35855
35868
  const from = resolvedFieldType(source, target, entities);
35856
35869
  const into = resolvedFieldType(field, entity, entities);
35857
35870
  if (from !== into) {
35858
35871
  return [
35859
- error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
35872
+ error51("write.default-from", path16, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
35860
35873
  ];
35861
35874
  }
35862
35875
  if (source.type !== "select" || field.type !== "select") return [];
35863
35876
  const findings = [];
35864
35877
  if (source.multi === true && field.multi !== true) {
35865
- findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}", which holds several options, into ${field.label}, which holds one \u2014 a copy would keep whichever came first`));
35878
+ findings.push(error51("write.default-from", path16, `copies ${target.label}'s "${source.label}", which holds several options, into ${field.label}, which holds one \u2014 a copy would keep whichever came first`));
35866
35879
  }
35867
35880
  const missing = source.options.filter((option) => !field.options.some((one) => one.alias === option.alias));
35868
35881
  if (missing.length > 0) {
35869
- findings.push(error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" into ${field.label}, which declares no option ${missing.map((option) => `"${option.alias}"`).join(", ")} \u2014 a copied option is written as the one of the same alias`));
35882
+ findings.push(error51("write.default-from", path16, `copies ${target.label}'s "${source.label}" into ${field.label}, which declares no option ${missing.map((option) => `"${option.alias}"`).join(", ")} \u2014 a copied option is written as the one of the same alias`));
35870
35883
  }
35871
35884
  return findings;
35872
35885
  }
@@ -36870,28 +36883,28 @@ var RECORDS_INPUT = "record_ids";
36870
36883
  var TEMPLATES_INPUT = "templates";
36871
36884
 
36872
36885
  // ../shared/src/schemas/app_model_validate_acts.ts
36873
- function checkChildCheck(model, check2, children, acts, path15) {
36886
+ function checkChildCheck(model, check2, children, acts, path16) {
36874
36887
  const child = check2.of === void 0 ? void 0 : entityOf(model, check2.of);
36875
36888
  if (child === void 0 || !children.has(child.alias)) {
36876
- return [error51("check.of", `${path15}.of`, `names "${String(check2.of)}", which this record lists no rows of \u2014 a check of a child is of rows the record lists`)];
36889
+ return [error51("check.of", `${path16}.of`, `names "${String(check2.of)}", which this record lists no rows of \u2014 a check of a child is of rows the record lists`)];
36877
36890
  }
36878
36891
  const field = fieldOf(child, check2.field);
36879
- if (field === void 0) return [error51("model.names-declared", `${path15}.field`, `names "${check2.field}", which is not a field of "${child.alias}"`)];
36892
+ if (field === void 0) return [error51("model.names-declared", `${path16}.field`, `names "${check2.field}", which is not a field of "${child.alias}"`)];
36880
36893
  const result = field.type === "formula" ? resolvedFieldType(field, child, model.entities) : void 0;
36881
36894
  const findings = [];
36882
36895
  if (result === void 0 || !["boolean", "text"].includes(result)) {
36883
- findings.push(error51("check.field", `${path15}.field`, `"${check2.field}" is a ${field.type} field \u2014 a check is a yes/no or text formula that stands while true or non-empty`));
36896
+ findings.push(error51("check.field", `${path16}.field`, `"${check2.field}" is a ${field.type} field \u2014 a check is a yes/no or text formula that stands while true or non-empty`));
36884
36897
  }
36885
- findings.push(...checkRestatesMeter(model, child, check2, path15));
36898
+ findings.push(...checkRestatesMeter(model, child, check2, path16));
36886
36899
  if (check2.blocks !== void 0 && check2.blocks !== "all") {
36887
36900
  for (const [index, target] of check2.blocks.entries()) {
36888
36901
  if (target === "edit" || target === "delete" || acts.get(target) === child.alias) continue;
36889
- findings.push(error51("check.blocks", `${path15}.blocks.${index}`, `names "${target}" \u2014 a check of a child row refuses its saves ("edit"), its Delete ("delete"), an act of "${child.alias}" or "all"`));
36902
+ findings.push(error51("check.blocks", `${path16}.blocks.${index}`, `names "${target}" \u2014 a check of a child row refuses its saves ("edit"), its Delete ("delete"), an act of "${child.alias}" or "all"`));
36890
36903
  }
36891
36904
  }
36892
36905
  return findings;
36893
36906
  }
36894
- function checkRestatesMeter(model, entity, check2, path15) {
36907
+ function checkRestatesMeter(model, entity, check2, path16) {
36895
36908
  const display = model.records?.[entity.alias];
36896
36909
  const figure = display?.figure;
36897
36910
  const bound = figure === void 0 ? void 0 : display?.limits?.[figure];
@@ -36904,7 +36917,7 @@ function checkRestatesMeter(model, entity, check2, path15) {
36904
36917
  return [
36905
36918
  error51(
36906
36919
  "check.restates-meter",
36907
- `${path15}.field`,
36920
+ `${path16}.field`,
36908
36921
  `"${check2.field}" reads "${figure}" against its pass mark ${pass}, which the figure's meter already draws \u2014 named under its track, amber until reached; drop the check`
36909
36922
  )
36910
36923
  ];
@@ -36929,9 +36942,9 @@ function checkAct(model, entity, act, at2) {
36929
36942
  if (act.on === "view" && (act.requires ?? []).length > 0) {
36930
36943
  findings.push(error51("act.view", `${at2}.requires`, "runs over every row in view, and a field one row lacks would refuse them all \u2014 state a `when` the view is narrowed to, or run it on each record"));
36931
36944
  }
36932
- const own = (name, path15) => {
36945
+ const own = (name, path16) => {
36933
36946
  const found = fieldOf(entity, name);
36934
- if (found === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`));
36947
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
36935
36948
  return found;
36936
36949
  };
36937
36950
  findings.push(...checkWhen(model.entities, entity, act.when, `${at2}.when`, "offer"));
@@ -36973,17 +36986,17 @@ function checkAct(model, entity, act, at2) {
36973
36986
  }
36974
36987
  function checkTemplates(model, entity, act, at2) {
36975
36988
  const templates = act.templates ?? [];
36976
- const path15 = `${at2}.templates`;
36989
+ const path16 = `${at2}.templates`;
36977
36990
  const findings = [];
36978
- if (act.template !== void 0) findings.push(error51("act.templates", path15, "states `template` too \u2014 one paper is a `template`, several are `templates`; keep one"));
36979
- if (act.into === void 0) findings.push(error51("act.templates", path15, "keeps its papers in no field \u2014 name the files field of the record they are kept in (`into`)"));
36980
- if (act.workflow !== void 0) findings.push(error51("act.makes-or-runs", path15, "makes papers and runs an authored workflow \u2014 the workflow makes what it needs"));
36981
- if (act.of !== void 0 || (act.on ?? "record") !== "record") findings.push(error51("act.templates", path15, "makes a set of papers over several rows or a child's \u2014 a set is one record's, kept in its `into`"));
36991
+ if (act.template !== void 0) findings.push(error51("act.templates", path16, "states `template` too \u2014 one paper is a `template`, several are `templates`; keep one"));
36992
+ if (act.into === void 0) findings.push(error51("act.templates", path16, "keeps its papers in no field \u2014 name the files field of the record they are kept in (`into`)"));
36993
+ if (act.workflow !== void 0) findings.push(error51("act.makes-or-runs", path16, "makes papers and runs an authored workflow \u2014 the workflow makes what it needs"));
36994
+ if (act.of !== void 0 || (act.on ?? "record") !== "record") findings.push(error51("act.templates", path16, "makes a set of papers over several rows or a child's \u2014 a set is one record's, kept in its `into`"));
36982
36995
  if (act.asks !== void 0) {
36983
36996
  findings.push(error51("act.templates", `${at2}.asks`, `makes a set of papers and asks \u2014 its press is the papers ticked (sent as "${TEMPLATES_INPUT}"); place what it needs in its section and \`requires\` it`));
36984
36997
  }
36985
36998
  for (const [index, one] of templates.entries()) {
36986
- const each = `${path15}.${index}`;
36999
+ const each = `${path16}.${index}`;
36987
37000
  findings.push(...checkTemplate(model, one.template, `${each}.template`), ...checkWhen(model.entities, entity, one.when, `${each}.when`, "offer"));
36988
37001
  if (templates.findIndex((other) => other.template === one.template) !== index) findings.push(error51("act.templates", `${each}.template`, `names "${one.template}" twice \u2014 each template is one paper of the set`));
36989
37002
  }
@@ -37084,53 +37097,53 @@ function checkIntake(model, app, act, at2) {
37084
37097
  if (act.fills === void 0) findings.push(error51("act.intake", at2, "reads papers and fills nothing \u2014 name what they fill in `fills`"));
37085
37098
  findings.push(...checkPapers(model, app, record2, act.intake, `${at2}.intake`));
37086
37099
  for (const [index, name] of (act.fills ?? []).entries()) {
37087
- const path15 = `${at2}.fills.${index}`;
37088
- if ((act.fills ?? []).indexOf(name) !== index) findings.push(error51("act.fills", path15, `names "${name}" twice \u2014 each is filled once`));
37089
- findings.push(...checkFill(model, app, record2, name, path15));
37100
+ const path16 = `${at2}.fills.${index}`;
37101
+ if ((act.fills ?? []).indexOf(name) !== index) findings.push(error51("act.fills", path16, `names "${name}" twice \u2014 each is filled once`));
37102
+ findings.push(...checkFill(model, app, record2, name, path16));
37090
37103
  }
37091
37104
  return findings;
37092
37105
  }
37093
- function checkPapers(model, app, record2, name, path15) {
37106
+ function checkPapers(model, app, record2, name, path16) {
37094
37107
  const child = entityOf(model, name);
37095
- if (child === void 0) return [error51("model.names-declared", path15, `names "${name}", which this model does not declare`)];
37108
+ if (child === void 0) return [error51("model.names-declared", path16, `names "${name}", which this model does not declare`)];
37096
37109
  const title = model.records?.[child.alias]?.title;
37097
37110
  const kind = title === void 0 ? void 0 : fieldOf(child, title);
37098
37111
  const block = recordBlocks(app.record).find((one) => isRowsBlock(one) && one.rows === child.alias && one.expect !== void 0 && one.expect === title);
37099
37112
  if (block === void 0 || kind === void 0 || !isSingleSelect(kind)) {
37100
- return [error51("act.intake", path15, `"${name}" is listed in no rows block that expects its title as a single select \u2014 each paper is a line of its kind under the record: \`"expect": "<its title>"\``)];
37113
+ return [error51("act.intake", path16, `"${name}" is listed in no rows block that expects its title as a single select \u2014 each paper is a line of its kind under the record: \`"expect": "<its title>"\``)];
37101
37114
  }
37102
37115
  const findings = [];
37103
- if (block.where !== void 0) findings.push(error51("act.intake", path15, `"${name}" is listed narrowed by \`where\` \u2014 a paper of any kind is filed; drop the \`where\``));
37116
+ if (block.where !== void 0) findings.push(error51("act.intake", path16, `"${name}" is listed narrowed by \`where\` \u2014 a paper of any kind is filed; drop the \`where\``));
37104
37117
  const files = child.fields.filter((field) => field.type === "files" && isAsked(field));
37105
- if (files.length !== 1) findings.push(error51("act.intake", path15, `"${name}" holds ${files.length} files fields \u2014 a paper's file is kept in the one it holds`));
37118
+ if (files.length !== 1) findings.push(error51("act.intake", path16, `"${name}" holds ${files.length} files fields \u2014 a paper's file is kept in the one it holds`));
37106
37119
  const link = childLink(child, record2.alias, block.via);
37107
37120
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37108
37121
  const filled = ["link" in link ? link.link : "", kind.alias, ...files.map((field) => field.alias), ...starts];
37109
37122
  for (const needed of requiredOnCreate(model, child, filled)) {
37110
- findings.push(error51("act.intake", path15, `a paper's line is made from its kind and its file, and "${name}" requires "${needed}" too \u2014 give it a default`));
37123
+ findings.push(error51("act.intake", path16, `a paper's line is made from its kind and its file, and "${name}" requires "${needed}" too \u2014 give it a default`));
37111
37124
  }
37112
37125
  return findings;
37113
37126
  }
37114
- function checkFill(model, app, record2, name, path15) {
37127
+ function checkFill(model, app, record2, name, path16) {
37115
37128
  const [head, tail] = name.split(".");
37116
37129
  if (tail !== void 0) {
37117
37130
  const link2 = fieldOf(record2, head);
37118
- if (link2 === void 0) return [error51("model.names-declared", path15, `names "${head}", which is not a field of "${record2.alias}"`)];
37131
+ if (link2 === void 0) return [error51("model.names-declared", path16, `names "${head}", which is not a field of "${record2.alias}"`)];
37119
37132
  if (link2.type !== "select_record_link" || link2.cardinality !== "one" || link2.required !== true) {
37120
- return [error51("act.fills", path15, `"${head}" is not a required one-row link \u2014 papers fill the row one names only where the record always names one`)];
37133
+ return [error51("act.fills", path16, `"${head}" is not a required one-row link \u2014 papers fill the row one names only where the record always names one`)];
37121
37134
  }
37122
37135
  const party = entityOf(model, link2.target_entity);
37123
37136
  if (party === void 0) return [];
37124
- if (!peopleWrite(party)) return [error51("act.fills", path15, `"${party.alias}" rows are written by no person, so no paper fills one`)];
37125
- return fillable(model, party, tail, path15);
37137
+ if (!peopleWrite(party)) return [error51("act.fills", path16, `"${party.alias}" rows are written by no person, so no paper fills one`)];
37138
+ return fillable(model, party, tail, path16);
37126
37139
  }
37127
37140
  const listed = recordBlocks(app.record).filter((block2) => isRowsBlock(block2) && block2.rows === name);
37128
- if (listed.length === 0) return fillable(model, record2, name, path15);
37141
+ if (listed.length === 0) return fillable(model, record2, name, path16);
37129
37142
  const child = entityOf(model, name);
37130
37143
  const block = listed.find((one) => one.expect === void 0 && one.under === void 0 && one.where === void 0 && one.create !== false);
37131
37144
  if (child === void 0) return [];
37132
37145
  if (block === void 0 || !peopleWrite(child)) {
37133
- return [error51("act.fills", path15, `"${name}" rows are added in no rows block without \`expect\`, \`under\` or \`where\` \u2014 papers add rows where the record lists them plainly`)];
37146
+ return [error51("act.fills", path16, `"${name}" rows are added in no rows block without \`expect\`, \`under\` or \`where\` \u2014 papers add rows where the record lists them plainly`)];
37134
37147
  }
37135
37148
  const link = childLink(child, record2.alias, block.via);
37136
37149
  if (!("link" in link)) return [];
@@ -37141,7 +37154,7 @@ function checkFill(model, app, record2, name, path15) {
37141
37154
  });
37142
37155
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37143
37156
  return requiredOnCreate(model, child, [link.link, ...filled, ...starts]).map(
37144
- (needed) => error51("act.fills", path15, `"${name}" rows need "${needed}", which their block does not add as a value papers fill \u2014 add it to the block's \`create\`, or give it a default`)
37157
+ (needed) => error51("act.fills", path16, `"${name}" rows need "${needed}", which their block does not add as a value papers fill \u2014 add it to the block's \`create\`, or give it a default`)
37145
37158
  );
37146
37159
  }
37147
37160
  var RECORD_KEYS = /* @__PURE__ */ new Set(["alias", "label", "when", "requires", "record", "fills"]);
@@ -37151,25 +37164,25 @@ function checkRecord(model, app, act, at2) {
37151
37164
  const record2 = entityOf(model, app.entity);
37152
37165
  if (record2 === void 0) return [];
37153
37166
  const findings = [];
37154
- const path15 = `${at2}.record`;
37167
+ const path16 = `${at2}.record`;
37155
37168
  for (const key of Object.keys(act).filter((one) => !RECORD_KEYS.has(one))) {
37156
37169
  findings.push(error51("act.record", `${at2}.${key}`, `states \`${key}\` beside \`record\` \u2014 an act recording a call files it under one record and fills what was said; drop it`));
37157
37170
  }
37158
37171
  const child = entityOf(model, recorded.into);
37159
- if (child === void 0) return [...findings, error51("model.names-declared", `${path15}.into`, `names "${recorded.into}", which this model does not declare`)];
37172
+ if (child === void 0) return [...findings, error51("model.names-declared", `${path16}.into`, `names "${recorded.into}", which this model does not declare`)];
37160
37173
  const listing = recordBlocks(app.record).filter((one) => (isRowsBlock(one) ? one.rows : "timeline" in one ? one.timeline : void 0) === child.alias);
37161
37174
  if (listing.length === 0) {
37162
- return [...findings, error51("act.record", `${path15}.into`, `"${child.alias}" is drawn in no rows or timeline block of this record \u2014 a recording's row is seen where the record lists them`)];
37175
+ return [...findings, error51("act.record", `${path16}.into`, `"${child.alias}" is drawn in no rows or timeline block of this record \u2014 a recording's row is seen where the record lists them`)];
37163
37176
  }
37164
37177
  const block = listing.find((one) => !isRowsBlock(one) || one.expect === void 0 && one.under === void 0 && one.where === void 0);
37165
37178
  if (block === void 0) {
37166
- return [...findings, error51("act.record", `${path15}.into`, `"${child.alias}" is listed only with \`expect\`, \`under\` or \`where\` \u2014 a recording's row is drawn among every one the record lists`)];
37179
+ return [...findings, error51("act.record", `${path16}.into`, `"${child.alias}" is listed only with \`expect\`, \`under\` or \`where\` \u2014 a recording's row is drawn among every one the record lists`)];
37167
37180
  }
37168
37181
  const link = childLink(child, record2.alias, "via" in block ? block.via : void 0);
37169
- if ("refused" in link) return [...findings, error51("act.record", `${path15}.into`, link.refused)];
37182
+ if ("refused" in link) return [...findings, error51("act.record", `${path16}.into`, link.refused)];
37170
37183
  const back = fieldOf(child, link.link);
37171
37184
  if (back?.type !== "select_record_link" || back.cardinality !== "one") {
37172
- findings.push(error51("act.record", `${path15}.into`, `"${child.alias}" links to "${record2.alias}" through "${link.link}", which holds several \u2014 a recording is filed under one record`));
37185
+ findings.push(error51("act.record", `${path16}.into`, `"${child.alias}" links to "${record2.alias}" through "${link.link}", which holds several \u2014 a recording is filed under one record`));
37173
37186
  }
37174
37187
  const kinds = [
37175
37188
  { key: "audio", types: ["files"], what: "a files field" },
@@ -37184,16 +37197,16 @@ function checkRecord(model, app, act, at2) {
37184
37197
  if (name === void 0) continue;
37185
37198
  const field = fieldOf(child, name);
37186
37199
  if (field === void 0) {
37187
- findings.push(error51("model.names-declared", `${path15}.${key}`, `names "${name}", which is not a field of "${child.alias}"`));
37200
+ findings.push(error51("model.names-declared", `${path16}.${key}`, `names "${name}", which is not a field of "${child.alias}"`));
37188
37201
  continue;
37189
37202
  }
37190
- if (!types.includes(field.type)) findings.push(error51("act.record", `${path15}.${key}`, `"${name}" is a ${field.type} field \u2014 the recording's ${key} is kept in ${what}`));
37191
- if (written.includes(name)) findings.push(error51("act.record", `${path15}.${key}`, `"${name}" is written twice by the recording \u2014 each is a field of its own`));
37203
+ if (!types.includes(field.type)) findings.push(error51("act.record", `${path16}.${key}`, `"${name}" is a ${field.type} field \u2014 the recording's ${key} is kept in ${what}`));
37204
+ if (written.includes(name)) findings.push(error51("act.record", `${path16}.${key}`, `"${name}" is written twice by the recording \u2014 each is a field of its own`));
37192
37205
  written.push(name);
37193
37206
  }
37194
37207
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37195
37208
  for (const needed of requiredOnCreate(model, child, [...written, ...starts])) {
37196
- findings.push(error51("act.record", `${path15}.into`, `a recording's row is made from what was recorded, and "${child.alias}" requires "${needed}" too \u2014 give it a default`));
37209
+ findings.push(error51("act.record", `${path16}.into`, `a recording's row is made from what was recorded, and "${child.alias}" requires "${needed}" too \u2014 give it a default`));
37197
37210
  }
37198
37211
  for (const [index, name] of (act.fills ?? []).entries()) {
37199
37212
  const filled = `${at2}.fills.${index}`;
@@ -37206,11 +37219,11 @@ function checkRecord(model, app, act, at2) {
37206
37219
  }
37207
37220
  return findings;
37208
37221
  }
37209
- function fillable(model, entity, name, path15) {
37222
+ function fillable(model, entity, name, path16) {
37210
37223
  const field = fieldOf(entity, name);
37211
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
37224
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
37212
37225
  const why = unfillable(model, entity, field);
37213
- return why === void 0 ? [] : [error51("act.fills", path15, `"${entity.alias}.${name}" is ${why}`)];
37226
+ return why === void 0 ? [] : [error51("act.fills", path16, `"${entity.alias}.${name}" is ${why}`)];
37214
37227
  }
37215
37228
  function noteIntake(model, app, resolved, at2) {
37216
37229
  const record2 = entityOf(model, app.entity);
@@ -37233,12 +37246,12 @@ function noteIntake(model, app, resolved, at2) {
37233
37246
  if (intake === void 0) return [];
37234
37247
  const children = new Set(intake.rows.map((one) => one.entity));
37235
37248
  return (app.acts?.[index]?.fills ?? []).flatMap((name, order) => {
37236
- const path15 = `${at2}.acts.${index}.fills.${order}`;
37249
+ const path16 = `${at2}.acts.${index}.fills.${order}`;
37237
37250
  if (children.has(name)) {
37238
37251
  const keyed = intake.rows.find((one) => one.entity === name)?.key.length ?? 0;
37239
- return keyed > 0 ? [] : [note("act.fills-new", path15, `"${name}" rows are only added: the papers fill no natural key to find one already listed \u2014 state \`natural_key\` on "${name}" of fields its block adds`)];
37252
+ return keyed > 0 ? [] : [note("act.fills-new", path16, `"${name}" rows are only added: the papers fill no natural key to find one already listed \u2014 state \`natural_key\` on "${name}" of fields its block adds`)];
37240
37253
  }
37241
- return drawn(name) ? [] : [note("act.fills-drawn", path15, `"${name}" is drawn nowhere on the record \u2014 the review shows what it becomes, the record does not`)];
37254
+ return drawn(name) ? [] : [note("act.fills-drawn", path16, `"${name}" is drawn nowhere on the record \u2014 the review shows what it becomes, the record does not`)];
37242
37255
  });
37243
37256
  });
37244
37257
  }
@@ -37250,7 +37263,7 @@ function offeredTogether2(one, other) {
37250
37263
  return held.some((option) => theirs.includes(option));
37251
37264
  });
37252
37265
  }
37253
- function checkFootNeeds(model, entity, section, drawn, act, path15) {
37266
+ function checkFootNeeds(model, entity, section, drawn, act, path16) {
37254
37267
  const asked = new Set((act.asks ?? []).filter((ask) => typeof ask === "string"));
37255
37268
  const writers = registerApps(model).flatMap((app) => (app.acts ?? []).filter((one) => actEntity(app, one) === entity.alias));
37256
37269
  const writtenBy = (one) => [...Object.keys(one.set ?? {}), ...(one.asks ?? []).filter((ask) => typeof ask === "string"), ...one.into === void 0 ? [] : [one.into]];
@@ -37258,7 +37271,7 @@ function checkFootNeeds(model, entity, section, drawn, act, path15) {
37258
37271
  return (act.requires ?? []).flatMap((name, index) => {
37259
37272
  const field = fieldOf(entity, name);
37260
37273
  if (asked.has(name) || field === void 0 || !isAsked(field) && !owned.has(name)) return [];
37261
- const at2 = `${path15}.requires.${index}`;
37274
+ const at2 = `${path16}.requires.${index}`;
37262
37275
  if (!drawn.has(name)) {
37263
37276
  return [error51("act.requires-drawn", at2, `"${act.alias}" needs "${name}", which "${section.title}" does not draw \u2014 its line would name a field the reader cannot see there; place it in the section, or have the act ask it`)];
37264
37277
  }
@@ -38414,11 +38427,11 @@ function checkBookEntities(model, app, at2) {
38414
38427
  const seen = /* @__PURE__ */ new Set();
38415
38428
  const bookless = app.register?.book === true && (app.books ?? []).length === 0 ? [error51("app.books", `${at2}.register.book`, "names the book of each row, but the app reads no `books`")] : [];
38416
38429
  return [...bookless, ...(app.books ?? []).flatMap((book, index) => {
38417
- const path15 = `${at2}.books.${index}.entity`;
38430
+ const path16 = `${at2}.books.${index}.entity`;
38418
38431
  const findings = [];
38419
- if (entityOf(model, book.entity) === void 0) findings.push(error51("model.names-declared", path15, `names entity "${book.entity}", which this model does not declare`));
38420
- if (book.entity === app.entity) findings.push(error51("app.books", path15, `names "${book.entity}", the app's own entity \u2014 a book is another entity's rows`));
38421
- if (seen.has(book.entity)) findings.push(error51("app.books", path15, `names "${book.entity}" twice \u2014 each book is read once`));
38432
+ if (entityOf(model, book.entity) === void 0) findings.push(error51("model.names-declared", path16, `names entity "${book.entity}", which this model does not declare`));
38433
+ if (book.entity === app.entity) findings.push(error51("app.books", path16, `names "${book.entity}", the app's own entity \u2014 a book is another entity's rows`));
38434
+ if (seen.has(book.entity)) findings.push(error51("app.books", path16, `names "${book.entity}" twice \u2014 each book is read once`));
38422
38435
  seen.add(book.entity);
38423
38436
  return findings;
38424
38437
  })];
@@ -38428,26 +38441,26 @@ function checkBookFields(model, app, book, reads, at2) {
38428
38441
  const entity = entityOf(model, book.entity);
38429
38442
  const stated = app.books?.[book.index];
38430
38443
  if (main2 === void 0 || entity === void 0 || stated === void 0) return [];
38431
- const path15 = `${at2}.books.${book.index}`;
38444
+ const path16 = `${at2}.books.${book.index}`;
38432
38445
  const read = /* @__PURE__ */ new Set([...reads, ...book.named]);
38433
38446
  const mappable = /* @__PURE__ */ new Set([...read, ...exportsBooks(app) ? exportPrinted(model, main2.alias) : []]);
38434
38447
  const findings = [];
38435
38448
  for (const [alias2, held] of Object.entries(stated.fields ?? {})) {
38436
- if (fieldOf(main2, alias2) === void 0) findings.push(error51("model.names-declared", `${path15}.fields.${alias2}`, `maps "${alias2}", which is not a field of "${main2.alias}"`));
38437
- else if (!mappable.has(alias2)) findings.push(error51("app.books", `${path15}.fields.${alias2}`, `maps "${alias2}", which the app does not read`));
38438
- if (fieldOf(entity, held) === void 0) findings.push(error51("model.names-declared", `${path15}.fields.${alias2}`, `maps "${alias2}" to "${held}", which is not a field of "${entity.alias}"`));
38449
+ if (fieldOf(main2, alias2) === void 0) findings.push(error51("model.names-declared", `${path16}.fields.${alias2}`, `maps "${alias2}", which is not a field of "${main2.alias}"`));
38450
+ else if (!mappable.has(alias2)) findings.push(error51("app.books", `${path16}.fields.${alias2}`, `maps "${alias2}", which the app does not read`));
38451
+ if (fieldOf(entity, held) === void 0) findings.push(error51("model.names-declared", `${path16}.fields.${alias2}`, `maps "${alias2}" to "${held}", which is not a field of "${entity.alias}"`));
38439
38452
  }
38440
38453
  for (const alias2 of read) {
38441
38454
  const own = fieldOf(main2, alias2);
38442
38455
  const lined = book.lineUp(alias2);
38443
38456
  const field = lined === void 0 ? void 0 : fieldOf(entity, lined);
38444
38457
  const apart = own === void 0 || field === void 0 ? void 0 : fieldMisfit(model, { entity: main2, field: own }, { entity, field });
38445
- if (apart !== void 0) findings.push(error51("app.books", path15, `reads "${alias2}" as "${entity.alias}.${field?.alias ?? alias2}", which ${apart} \u2014 a row of either stands in one column`));
38458
+ if (apart !== void 0) findings.push(error51("app.books", path16, `reads "${alias2}" as "${entity.alias}.${field?.alias ?? alias2}", which ${apart} \u2014 a row of either stands in one column`));
38446
38459
  const options = own === void 0 ? void 0 : contractSelectOptions(own);
38447
38460
  for (const option of field === void 0 ? [] : contractSelectOptions(field) ?? []) {
38448
38461
  const kin = options?.find((one) => one.alias !== option.alias && sameLabel(one.label, option.label));
38449
38462
  if (kin !== void 0 && !options?.some((one) => one.alias === option.alias)) {
38450
- findings.push(note("app.book-option-kin", path15, `holds "${option.label}" of "${alias2}" as "${option.alias}", which "${main2.alias}" holds as "${kin.alias}"`));
38463
+ findings.push(note("app.book-option-kin", path16, `holds "${option.label}" of "${alias2}" as "${option.alias}", which "${main2.alias}" holds as "${kin.alias}"`));
38451
38464
  }
38452
38465
  }
38453
38466
  }
@@ -38456,14 +38469,14 @@ function checkBookFields(model, app, book, reads, at2) {
38456
38469
  function sameLabel(a, b) {
38457
38470
  return a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase();
38458
38471
  }
38459
- function checkReportBooks(model, app, report, path15) {
38472
+ function checkReportBooks(model, app, report, path16) {
38460
38473
  const named = report.books;
38461
38474
  const main2 = entityOf(model, app.entity);
38462
38475
  if (named === void 0 || main2 === void 0) return [];
38463
38476
  const findings = [];
38464
38477
  const seen = /* @__PURE__ */ new Set();
38465
38478
  for (const [index, name] of named.entries()) {
38466
- const at2 = `${path15}.books.${index}`;
38479
+ const at2 = `${path16}.books.${index}`;
38467
38480
  if (name !== app.entity && !(app.books ?? []).some((book) => book.entity === name)) findings.push(error51("app.books", at2, `names "${name}", which is neither "${app.entity}" nor one of the app's books`));
38468
38481
  if (seen.has(name)) findings.push(error51("app.books", at2, `names "${name}" twice \u2014 each book is read once`));
38469
38482
  seen.add(name);
@@ -38475,7 +38488,7 @@ function checkReportBooks(model, app, report, path15) {
38475
38488
  return stated !== void 0 && entity !== void 0 && bookField(main2, entity, stated, alias2) !== void 0;
38476
38489
  };
38477
38490
  for (const alias2 of [...Object.keys(report.where ?? {}), ...report.per === void 0 ? [] : [report.per]]) {
38478
- if (!named.some((name) => holds(name, alias2))) findings.push(error51("app.books", path15, `narrows by "${alias2}", which none of ${named.map((one) => `"${one}"`).join(", ")} holds \u2014 no row of the report would stand in its chip`));
38491
+ if (!named.some((name) => holds(name, alias2))) findings.push(error51("app.books", path16, `narrows by "${alias2}", which none of ${named.map((one) => `"${one}"`).join(", ")} holds \u2014 no row of the report would stand in its chip`));
38479
38492
  }
38480
38493
  return findings;
38481
38494
  }
@@ -38570,18 +38583,18 @@ function checkRegisterApp(model, app, at2) {
38570
38583
  function noteTasks(model, resolved, at2) {
38571
38584
  return Object.entries(resolved.tasks).flatMap(([entity, task]) => taskNote(model, entity, task, at2));
38572
38585
  }
38573
- function taskNote(model, entity, task, path15) {
38586
+ function taskNote(model, entity, task, path16) {
38574
38587
  if (task.close === void 0) {
38575
38588
  const status = model.records?.[entity]?.status?.field ?? "";
38576
38589
  return [
38577
38590
  note(
38578
38591
  "app.tasks",
38579
- path15,
38592
+ path16,
38580
38593
  `"${entity}" rows are drawn as tasks and no write of this app moves one to a closed option \u2014 their ring stands disabled: add an act of "${entity}" setting "${status}" to one, or let people write it`
38581
38594
  )
38582
38595
  ];
38583
38596
  }
38584
- return task.close.act !== void 0 && task.reopen === void 0 ? [note("app.tasks", path15, `"${entity}" rows ticked done stay done: no act of this app moves one back from a closed option`)] : [];
38597
+ return task.close.act !== void 0 && task.reopen === void 0 ? [note("app.tasks", path16, `"${entity}" rows ticked done stay done: no act of this app moves one back from a closed option`)] : [];
38585
38598
  }
38586
38599
  function checkReadsRows(model, reads, entities, at2) {
38587
38600
  const findings = [];
@@ -38646,7 +38659,7 @@ function checkCreates(model, resolved, at2) {
38646
38659
  ])
38647
38660
  )
38648
38661
  ];
38649
- return lists.flatMap(({ entity, asked, fills: filed, path: path15 }) => {
38662
+ return lists.flatMap(({ entity, asked, fills: filed, path: path16 }) => {
38650
38663
  const declared = entityOf(model, entity);
38651
38664
  if (declared === void 0) return [];
38652
38665
  const fills = [...filed, ...Object.keys(model.records?.[entity]?.starts ?? {})];
@@ -38656,16 +38669,16 @@ function checkCreates(model, resolved, at2) {
38656
38669
  return by === void 0 ? void 0 : `apps.${by.app}.acts.${by.act}.set.${name}`;
38657
38670
  };
38658
38671
  return [
38659
- ...checkCreateMilestones(model, entity, asked, path15),
38672
+ ...checkCreateMilestones(model, entity, asked, path16),
38660
38673
  // An act's outcome is the act's to write: an add stating it would decide what only the act decides. What the
38661
38674
  // add fills itself (the record, a document's lines) is no reader's statement.
38662
38675
  ...asked.filter((name) => !fills.includes(name)).flatMap((name) => {
38663
38676
  const act = setter(name);
38664
- return act === void 0 ? [] : [error51("act.set-not-asked", path15, `asks "${name}", which ${act} sets \u2014 the act writes it, so an add never states it; drop it here`)];
38677
+ return act === void 0 ? [] : [error51("act.set-not-asked", path16, `asks "${name}", which ${act} sets \u2014 the act writes it, so an add never states it; drop it here`)];
38665
38678
  }),
38666
38679
  ...requiredOnCreate(model, declared, fills).filter((name) => !asked.includes(name)).map((name) => {
38667
38680
  const act = setter(name);
38668
- return act === void 0 ? error51("app.create-required", path15, `leaves out "${name}", which "${entity}" requires and nothing fills \u2014 ask it, or give it a default`) : error51("act.set-not-asked", path15, `leaves out "${name}", which "${entity}" requires and ${act} sets \u2014 give it a default`);
38681
+ return act === void 0 ? error51("app.create-required", path16, `leaves out "${name}", which "${entity}" requires and nothing fills \u2014 ask it, or give it a default`) : error51("act.set-not-asked", path16, `leaves out "${name}", which "${entity}" requires and ${act} sets \u2014 give it a default`);
38669
38682
  })
38670
38683
  ];
38671
38684
  });
@@ -38678,9 +38691,9 @@ function checkApp(model, app, at2) {
38678
38691
  if (display === void 0) {
38679
38692
  return [error51("app.records-entry", `${at2}.entity`, `lists "${entity.alias}", which has no \`records\` entry \u2014 say how its row is recognised first`)];
38680
38693
  }
38681
- const own = (name, path15) => {
38694
+ const own = (name, path16) => {
38682
38695
  const found = fieldOf(entity, name);
38683
- if (found === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`));
38696
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
38684
38697
  return found;
38685
38698
  };
38686
38699
  const stages = new Set((milestonesOf(display) ?? []).map((stage) => stage.field));
@@ -38693,41 +38706,41 @@ function checkApp(model, app, at2) {
38693
38706
  findings.push(error51("app.scope", `${at2}.scope`, `"${scope}" is not required, so a row holding none belongs to no folder and is never listed \u2014 make it required`));
38694
38707
  }
38695
38708
  }
38696
- const scoped = (name, path15, where) => {
38697
- if (name === scope) findings.push(error51("app.scope", path15, `"${name}" is the folder the app is inside, the same on every row ${where} \u2014 drop it here`));
38709
+ const scoped = (name, path16, where) => {
38710
+ if (name === scope) findings.push(error51("app.scope", path16, `"${name}" is the folder the app is inside, the same on every row ${where} \u2014 drop it here`));
38698
38711
  };
38699
38712
  const rowParts = partsOf(display);
38700
38713
  const registerBounds = meterBounds(display, app.register?.columns ?? []);
38701
38714
  const columns = /* @__PURE__ */ new Map();
38702
38715
  for (const [index, name] of (app.register?.columns ?? []).entries()) {
38703
- const path15 = `${at2}.register.columns.${index}`;
38704
- const column = own(name, path15);
38716
+ const path16 = `${at2}.register.columns.${index}`;
38717
+ const column = own(name, path16);
38705
38718
  const part = rowParts.get(name);
38706
- if (column !== void 0 && part !== void 0) findings.push(error51("register.column-drawn", path15, `"${name}" is already drawn by the row itself, as ${part} \u2014 drop the column`));
38719
+ if (column !== void 0 && part !== void 0) findings.push(error51("register.column-drawn", path16, `"${name}" is already drawn by the row itself, as ${part} \u2014 drop the column`));
38707
38720
  const meter = registerBounds.get(name);
38708
- if (meter !== void 0) findings.push(error51("register.column-drawn", path15, `"${name}" is the bound of the meter "${meter}" on the same row, which states it \u2014 drop the column`));
38709
- scoped(name, path15, "it lists");
38721
+ if (meter !== void 0) findings.push(error51("register.column-drawn", path16, `"${name}" is the bound of the meter "${meter}" on the same row, which states it \u2014 drop the column`));
38722
+ scoped(name, path16, "it lists");
38710
38723
  const first2 = columns.get(name);
38711
- if (first2 !== void 0) findings.push(error51("register.column-drawn", path15, `"${name}" is already column ${first2} \u2014 a field is one column`));
38724
+ if (first2 !== void 0) findings.push(error51("register.column-drawn", path16, `"${name}" is already column ${first2} \u2014 a field is one column`));
38712
38725
  else columns.set(name, index);
38713
38726
  }
38714
38727
  const splits = new Set((app.register?.readings ?? []).flatMap((reading) => "metric" in reading ? [] : pressFields(modelPressed(reading))));
38715
38728
  for (const [index, stated] of (app.register?.filters ?? []).entries()) {
38716
- const path15 = `${at2}.register.filters.${index}`;
38717
- findings.push(...checkFilter(model, entity, stated, path15, { owner: "a register", chips: display.status?.field, splits }));
38718
- scoped(filterField(stated), path15, "it lists");
38729
+ const path16 = `${at2}.register.filters.${index}`;
38730
+ findings.push(...checkFilter(model, entity, stated, path16, { owner: "a register", chips: display.status?.field, splits }));
38731
+ scoped(filterField(stated), path16, "it lists");
38719
38732
  }
38720
38733
  for (const [index, name] of (app.register?.search ?? []).entries()) {
38721
- const path15 = `${at2}.register.search.${index}`;
38722
- findings.push(...checkSearch(model, entity, name, path15));
38723
- scoped(name, path15, "it lists");
38734
+ const path16 = `${at2}.register.search.${index}`;
38735
+ findings.push(...checkSearch(model, entity, name, path16));
38736
+ scoped(name, path16, "it lists");
38724
38737
  }
38725
38738
  findings.push(...checkOrder(entity, app.register?.sort, `${at2}.register.sort`));
38726
38739
  const tabs = app.register?.tabs ?? [];
38727
- const dated = (name, path15) => {
38728
- const field = own(name, path15);
38740
+ const dated = (name, path16) => {
38741
+ const field = own(name, path16);
38729
38742
  const type = field === void 0 ? void 0 : resolvedFieldType(field, entity, model.entities) ?? field.type;
38730
- if (field !== void 0 && type !== "date") findings.push(error51("register.tabs", path15, `"${name}" is a ${field.type} field \u2014 a tab places its rows in the period by a date`));
38743
+ if (field !== void 0 && type !== "date") findings.push(error51("register.tabs", path16, `"${name}" is a ${field.type} field \u2014 a tab places its rows in the period by a date`));
38731
38744
  };
38732
38745
  if (tabs.length > 0 && !["table", "cards"].includes(app.register?.layout ?? "table")) {
38733
38746
  findings.push(error51("register.tabs", `${at2}.register.tabs`, `a ${app.register?.layout} reads its own window \u2014 tabs stand over a table or cards`));
@@ -38738,24 +38751,24 @@ function checkApp(model, app, at2) {
38738
38751
  const named = /* @__PURE__ */ new Set();
38739
38752
  const reportKeys = APP_EXPORT_REPORT_KEYS;
38740
38753
  for (const [index, tab] of tabs.entries()) {
38741
- const path15 = `${at2}.register.tabs.${index}`;
38742
- if (named.has(tab.tab)) findings.push(error51("register.tabs", `${path15}.tab`, `"${tab.tab}" names another tab already`));
38754
+ const path16 = `${at2}.register.tabs.${index}`;
38755
+ if (named.has(tab.tab)) findings.push(error51("register.tabs", `${path16}.tab`, `"${tab.tab}" names another tab already`));
38743
38756
  if (reportKeys.includes(tab.tab) || tab.tab === entity.alias) {
38744
- findings.push(error51("register.tabs", `${path15}.tab`, `"${tab.tab}" is a key a report already holds \u2014 name the tab otherwise`));
38757
+ findings.push(error51("register.tabs", `${path16}.tab`, `"${tab.tab}" is a key a report already holds \u2014 name the tab otherwise`));
38745
38758
  }
38746
38759
  named.add(tab.tab);
38747
- if (tab.in !== void 0) dated(tab.in, `${path15}.in`);
38760
+ if (tab.in !== void 0) dated(tab.in, `${path16}.in`);
38748
38761
  if (tab.open !== void 0) {
38749
- dated(tab.open.from, `${path15}.open.from`);
38750
- dated(tab.open.to, `${path15}.open.to`);
38751
- if (tab.open.from === tab.open.to) findings.push(error51("register.tabs", `${path15}.open`, `opens and closes on "${tab.open.from}" \u2014 a row is open between two dates`));
38762
+ dated(tab.open.from, `${path16}.open.from`);
38763
+ dated(tab.open.to, `${path16}.open.to`);
38764
+ if (tab.open.from === tab.open.to) findings.push(error51("register.tabs", `${path16}.open`, `opens and closes on "${tab.open.from}" \u2014 a row is open between two dates`));
38752
38765
  }
38753
- findings.push(...checkNarrowing(model.entities, entity, tab.where, `${path15}.where`));
38766
+ findings.push(...checkNarrowing(model.entities, entity, tab.where, `${path16}.where`));
38754
38767
  const status = display?.status?.field;
38755
38768
  if (status !== void 0 && tab.where?.[status] !== void 0) {
38756
- findings.push(error51("register.tabs", `${path15}.where.${status}`, `narrows by "${status}", the status \u2014 its chips split the rows by it, each with its count; a tab splits them by the period or another field`));
38769
+ findings.push(error51("register.tabs", `${path16}.where.${status}`, `narrows by "${status}", the status \u2014 its chips split the rows by it, each with its count; a tab splits them by the period or another field`));
38757
38770
  }
38758
- findings.push(...checkOrder(entity, tab.sort, `${path15}.sort`));
38771
+ findings.push(...checkOrder(entity, tab.sort, `${path16}.sort`));
38759
38772
  }
38760
38773
  const group = app.register?.group;
38761
38774
  if (group !== void 0 && group === app.scope) {
@@ -38775,37 +38788,37 @@ function checkApp(model, app, at2) {
38775
38788
  }
38776
38789
  if (!peopleWrite(entity)) findings.push(error51("app.create-asks", `${at2}.register.create`, `adds rows to "${entity.alias}", which no person writes`));
38777
38790
  for (const [index, name] of create.entries()) {
38778
- const path15 = `${at2}.register.create.${index}`;
38779
- const field = own(name, path15);
38780
- if (field !== void 0 && !isAsked(field)) findings.push(error51("app.asked-written", path15, `asks "${name}", a ${field.type} field nobody writes`));
38781
- if (field !== void 0) findings.push(...checkOwnRows(model, entity, field, path15));
38782
- if (name === scope) findings.push(error51("app.scope", path15, `asks "${name}", the folder the app is inside, which an add files the row under itself \u2014 drop it`));
38791
+ const path16 = `${at2}.register.create.${index}`;
38792
+ const field = own(name, path16);
38793
+ if (field !== void 0 && !isAsked(field)) findings.push(error51("app.asked-written", path16, `asks "${name}", a ${field.type} field nobody writes`));
38794
+ if (field !== void 0) findings.push(...checkOwnRows(model, entity, field, path16));
38795
+ if (name === scope) findings.push(error51("app.scope", path16, `asks "${name}", the folder the app is inside, which an add files the row under itself \u2014 drop it`));
38783
38796
  }
38784
38797
  }
38785
38798
  findings.push(...checkReports(model, entity, app, display.status?.field, at2));
38786
38799
  for (const [index, reading] of (app.register?.readings ?? []).entries()) {
38787
- const path15 = `${at2}.register.readings.${index}`;
38800
+ const path16 = `${at2}.register.readings.${index}`;
38788
38801
  if (readingEntity(reading) !== entity.alias) {
38789
- findings.push(error51("register.readings-own", path15, `reads "${readingEntity(reading)}" \u2014 a register's readings are over its own rows, "${entity.alias}"; put another entity's on a dashboard`));
38802
+ findings.push(error51("register.readings-own", path16, `reads "${readingEntity(reading)}" \u2014 a register's readings are over its own rows, "${entity.alias}"; put another entity's on a dashboard`));
38790
38803
  continue;
38791
38804
  }
38792
- findings.push(...checkReading(model, reading, path15));
38805
+ findings.push(...checkReading(model, reading, path16));
38793
38806
  const tab = "tab" in reading ? reading.tab : void 0;
38794
38807
  if (tab !== void 0 && !named.has(tab)) {
38795
- findings.push(error51("register.tabs", `${path15}.tab`, named.size === 0 ? `reads the tab "${tab}", and the register has no tabs` : `reads the tab "${tab}", which is none of the register's: ${[...named].join(", ")}`));
38808
+ findings.push(error51("register.tabs", `${path16}.tab`, named.size === 0 ? `reads the tab "${tab}", and the register has no tabs` : `reads the tab "${tab}", which is none of the register's: ${[...named].join(", ")}`));
38796
38809
  }
38797
38810
  if ("metric" in reading && reading.value !== void 0 && reading.value === display.figure && reading.where === void 0) {
38798
- findings.push(error51("register.readings-restate", path15, `sums "${reading.value}", the register's figure, over the rows in view \u2014 the foot states that total; give it a \`where\` the foot does not carry, or drop it`));
38811
+ findings.push(error51("register.readings-restate", path16, `sums "${reading.value}", the register's figure, over the rows in view \u2014 the foot states that total; give it a \`where\` the foot does not carry, or drop it`));
38799
38812
  }
38800
38813
  if ("metric" in reading && reading.value === void 0 && reading.where === void 0 && display.status !== void 0) {
38801
- findings.push(error51("register.readings-restate", path15, "counts the rows in view \u2014 the status chip states that count; give it a `where` or drop it"));
38814
+ findings.push(error51("register.readings-restate", path16, "counts the rows in view \u2014 the status chip states that count; give it a `where` or drop it"));
38802
38815
  }
38803
38816
  }
38804
38817
  const placed = /* @__PURE__ */ new Map();
38805
- const place = (name, where, path15) => {
38818
+ const place = (name, where, path16) => {
38806
38819
  const first2 = placed.get(name);
38807
38820
  if (first2 === HEADER_IMAGE && where === FILES_BLOCK) placed.set(name, where);
38808
- else if (first2 !== void 0) findings.push(error51("record.placed-once", path15, `places "${name}" a second time on the record (it is already in ${first2})`));
38821
+ else if (first2 !== void 0) findings.push(error51("record.placed-once", path16, `places "${name}" a second time on the record (it is already in ${first2})`));
38809
38822
  else placed.set(name, where);
38810
38823
  };
38811
38824
  place(display.title, "the header's title", `${at2}.record`);
@@ -38824,21 +38837,21 @@ function checkApp(model, app, at2) {
38824
38837
  }
38825
38838
  const inFacts = /* @__PURE__ */ new Map();
38826
38839
  for (const [index, section] of sections.entries()) {
38827
- const path15 = `${at2}.record.sections.${index}`;
38828
- findings.push(...checkWhen(model.entities, entity, section.when, `${path15}.when`, "offer"));
38840
+ const path16 = `${at2}.record.sections.${index}`;
38841
+ findings.push(...checkWhen(model.entities, entity, section.when, `${path16}.when`, "offer"));
38829
38842
  if (section.fields === void 0 && section.blocks === void 0 && section.acts === void 0) {
38830
- findings.push(error51("record.section-holds", path15, `"${section.title}" holds no fields, blocks or acts \u2014 a section is where the reader fills, reads or does something; give it one, or drop it`));
38843
+ findings.push(error51("record.section-holds", path16, `"${section.title}" holds no fields, blocks or acts \u2014 a section is where the reader fills, reads or does something; give it one, or drop it`));
38831
38844
  }
38832
- findings.push(...checkSectionAt(entity, display, section.at, `${path15}.at`));
38845
+ findings.push(...checkSectionAt(entity, display, section.at, `${path16}.at`));
38833
38846
  for (const [order, name] of (section.fields ?? []).entries()) {
38834
- const fieldPath = `${path15}.fields.${order}`;
38847
+ const fieldPath = `${path16}.fields.${order}`;
38835
38848
  const fact = own(name, fieldPath);
38836
38849
  if (fact !== void 0) place(name, `the section "${section.title}"`, fieldPath);
38837
38850
  if (fact !== void 0) inFacts.set(name, fieldPath);
38838
38851
  if (fact !== void 0) findings.push(...checkOwnRows(model, entity, fact, fieldPath));
38839
38852
  }
38840
38853
  }
38841
- for (const [name, path15] of inFacts) {
38854
+ for (const [name, path16] of inFacts) {
38842
38855
  const fact = fieldOf(entity, name);
38843
38856
  if (fact?.type !== "lookup" || !(display.subtitle ?? []).includes(fact.source_field_alias)) continue;
38844
38857
  const link = fieldOf(entity, fact.source_field_alias);
@@ -38846,9 +38859,9 @@ function checkApp(model, app, at2) {
38846
38859
  const figure = target?.figure;
38847
38860
  const bound = figure === void 0 ? void 0 : target?.limits?.[figure];
38848
38861
  if (fact.lookup_field_alias !== figure && fact.lookup_field_alias !== bound) continue;
38849
- findings.push(error51("record.fact-restates", path15, `"${name}" looks up the ${fact.lookup_field_alias === figure ? "figure" : "bound"} of "${fact.source_field_alias}", which the header's line already reads with its meter \u2014 drop it from the section`));
38862
+ findings.push(error51("record.fact-restates", path16, `"${name}" looks up the ${fact.lookup_field_alias === figure ? "figure" : "bound"} of "${fact.source_field_alias}", which the header's line already reads with its meter \u2014 drop it from the section`));
38850
38863
  }
38851
- for (const [name, path15] of inFacts) {
38864
+ for (const [name, path16] of inFacts) {
38852
38865
  const fact = fieldOf(entity, name);
38853
38866
  if (fact?.type !== "lookup" || !inFacts.has(fact.source_field_alias)) continue;
38854
38867
  const link = fieldOf(entity, fact.source_field_alias);
@@ -38856,22 +38869,22 @@ function checkApp(model, app, at2) {
38856
38869
  if (target === void 0) continue;
38857
38870
  const part = fact.lookup_field_alias === target.title ? "title" : (target.subtitle ?? []).includes(fact.lookup_field_alias) ? "line under the title" : void 0;
38858
38871
  if (part === void 0) continue;
38859
- findings.push(error51("record.fact-restates", path15, `"${name}" looks up the ${part} of "${fact.source_field_alias}", whose row already reads it where the link stands \u2014 drop it from the section`));
38872
+ findings.push(error51("record.fact-restates", path16, `"${name}" looks up the ${part} of "${fact.source_field_alias}", whose row already reads it where the link stands \u2014 drop it from the section`));
38860
38873
  }
38861
38874
  const blocksPlaced = /* @__PURE__ */ new Map();
38862
38875
  for (const [index, section] of sections.entries()) {
38863
38876
  for (const [order, block] of (section.blocks ?? []).entries()) {
38864
- const path15 = `${at2}.record.sections.${index}.blocks.${order}`;
38865
- findings.push(...checkWhen(model.entities, entity, block.when, `${path15}.when`, "offer"));
38877
+ const path16 = `${at2}.record.sections.${index}.blocks.${order}`;
38878
+ findings.push(...checkWhen(model.entities, entity, block.when, `${path16}.when`, "offer"));
38866
38879
  const listed = "timeline" in block ? block.timeline : isRowsBlock(block) ? block.rows : isAgendaBlock(block) ? block.agenda : void 0;
38867
38880
  if (listed !== void 0 && listed === display.status?.history) {
38868
- findings.push(error51("record.history", path15, `reads "${listed}", the record's status history \u2014 never a section: \`record.history\` draws it beside the record; drop the block`));
38881
+ findings.push(error51("record.history", path16, `reads "${listed}", the record's status history \u2014 never a section: \`record.history\` draws it beside the record; drop the block`));
38869
38882
  }
38870
38883
  const identity = blockIdentity(block);
38871
38884
  const first2 = identity === void 0 ? void 0 : blocksPlaced.get(identity);
38872
- if (first2 !== void 0) findings.push(error51("record.placed-once", path15, `places the same block a second time on the record (it is already in ${first2}) \u2014 a block is read once`));
38885
+ if (first2 !== void 0) findings.push(error51("record.placed-once", path16, `places the same block a second time on the record (it is already in ${first2}) \u2014 a block is read once`));
38873
38886
  else if (identity !== void 0) blocksPlaced.set(identity, `the section "${section.title}"`);
38874
- findings.push(...checkBlock(model, entity, block, path15, place, app.reads !== "shared", app.checks ?? []));
38887
+ findings.push(...checkBlock(model, entity, block, path16, place, app.reads !== "shared", app.checks ?? []));
38875
38888
  if (!isRowsBlock(block)) continue;
38876
38889
  const child = entityOf(model, block.rows);
38877
38890
  const link = child === void 0 ? void 0 : childLink(child, entity.alias, block.via);
@@ -38894,34 +38907,34 @@ function checkApp(model, app, at2) {
38894
38907
  const children = new Set(recordChildren(model, entity.alias, recordBlocks(app.record)));
38895
38908
  const acts = /* @__PURE__ */ new Map();
38896
38909
  for (const [index, act] of (app.acts ?? []).entries()) {
38897
- const path15 = `${at2}.acts.${index}`;
38898
- if (acts.has(act.alias)) findings.push(error51("app.alias-unique", `${path15}.alias`, `"${act.alias}" names two acts of this app \u2014 an act alias is unique`));
38910
+ const path16 = `${at2}.acts.${index}`;
38911
+ if (acts.has(act.alias)) findings.push(error51("app.alias-unique", `${path16}.alias`, `"${act.alias}" names two acts of this app \u2014 an act alias is unique`));
38899
38912
  acts.set(act.alias, act.of);
38900
38913
  if (act.of !== void 0 && !children.has(act.of)) {
38901
- findings.push(error51("act.of", `${path15}.of`, `names "${act.of}", which this record lists no rows of \u2014 an act of a child works on rows the record lists`));
38914
+ findings.push(error51("act.of", `${path16}.of`, `names "${act.of}", which this record lists no rows of \u2014 an act of a child works on rows the record lists`));
38902
38915
  continue;
38903
38916
  }
38904
38917
  if (act.of !== void 0 && act.on === "view") {
38905
- findings.push(error51("act.of", `${path15}.on`, `is "${act.on}", and an act of a child works on the one child row it is pressed on \u2014 drop it`));
38918
+ findings.push(error51("act.of", `${path16}.on`, `is "${act.on}", and an act of a child works on the one child row it is pressed on \u2014 drop it`));
38906
38919
  }
38907
38920
  if (act.of !== void 0 && act.on === "rows" && !listsRows(app, act.of)) {
38908
- findings.push(error51("act.of", `${path15}.on`, `is "rows", and the record lists no "${act.of}" rows to pick together \u2014 list them in a rows block without \`expect\``));
38921
+ findings.push(error51("act.of", `${path16}.on`, `is "rows", and the record lists no "${act.of}" rows to pick together \u2014 list them in a rows block without \`expect\``));
38909
38922
  }
38910
38923
  const on = entityOf(model, actEntity(app, act));
38911
38924
  if (on === void 0) continue;
38912
- findings.push(...checkAct(model, on, act, path15), ...checkIntake(model, app, act, path15), ...checkRecord(model, app, act, path15));
38925
+ findings.push(...checkAct(model, on, act, path16), ...checkIntake(model, app, act, path16), ...checkRecord(model, app, act, path16));
38913
38926
  for (const expected of expectedKeys(model, on.alias)) {
38914
38927
  const writes = [...Object.keys(act.set ?? {}), ...(act.asks ?? []).filter((ask) => typeof ask === "string")];
38915
38928
  for (const name of [expected.via, expected.key].filter((one) => writes.includes(one))) {
38916
- findings.push(error51("act.writes-line-key", path15, `writes "${name}", which names the line a "${on.alias}" row is under its record \u2014 a line's key is stated by filling it`));
38929
+ findings.push(error51("act.writes-line-key", path16, `writes "${name}", which names the line a "${on.alias}" row is under its record \u2014 a line's key is stated by filling it`));
38917
38930
  }
38918
38931
  }
38919
- findings.push(...checkHistoryCopies(model, on, act, path15));
38932
+ findings.push(...checkHistoryCopies(model, on, act, path16));
38920
38933
  if (act.template !== void 0 && act.workflow !== void 0) {
38921
- findings.push(error51("act.makes-or-runs", `${path15}.template`, "makes a document and runs an authored workflow \u2014 the workflow makes what it needs"));
38934
+ findings.push(error51("act.makes-or-runs", `${path16}.template`, "makes a document and runs an authored workflow \u2014 the workflow makes what it needs"));
38922
38935
  }
38923
38936
  if (act.on === "view" && act.template === void 0 && act.set === void 0 && act.workflow === void 0) {
38924
- findings.push(error51("act.view", `${path15}.on`, "runs over every row in view and neither writes nor makes a document \u2014 give it `set`, a `template` or a `workflow`"));
38937
+ findings.push(error51("act.view", `${path16}.on`, "runs over every row in view and neither writes nor makes a document \u2014 give it `set`, a `template` or a `workflow`"));
38925
38938
  }
38926
38939
  }
38927
38940
  const actsPlaced = /* @__PURE__ */ new Map();
@@ -38939,20 +38952,20 @@ function checkApp(model, app, at2) {
38939
38952
  for (const [index, section] of sections.entries()) {
38940
38953
  const drawn = /* @__PURE__ */ new Set([...drawnBy(section), ...section.at === void 0 ? unstaged : []]);
38941
38954
  for (const [order, name] of (section.acts ?? []).entries()) {
38942
- const path15 = `${at2}.record.sections.${index}.acts.${order}`;
38955
+ const path16 = `${at2}.record.sections.${index}.acts.${order}`;
38943
38956
  const act = (app.acts ?? []).find((one) => one.alias === name);
38944
38957
  const home = act === void 0 ? void 0 : actHome(model, app, act);
38945
- if (act === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not an act of this app`));
38946
- else if (act.of !== void 0) findings.push(error51("record.section-acts", path15, `names "${name}", an act of each "${act.of}" row \u2014 it stands on that row, never at a section's foot`));
38947
- else if ((act.on ?? "record") !== "record") findings.push(error51("record.section-acts", path15, `names "${name}", which runs over the register's rows \u2014 a section's foot holds the acts on this record`));
38948
- else if (home !== void 0 && home.section !== index) findings.push(error51("record.move-home", path15, misplacedMove(act, sections[home.section]?.title ?? "", home.entry)));
38949
- else if (home !== void 0 || isCorrectionAct(model, app, act)) findings.push(...checkFootNeeds(model, entity, section, drawn, act, path15));
38950
- else findings.push(...checkStagedAct(display, section, act, path15), ...checkFootNeeds(model, entity, section, drawn, act, path15));
38958
+ if (act === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not an act of this app`));
38959
+ else if (act.of !== void 0) findings.push(error51("record.section-acts", path16, `names "${name}", an act of each "${act.of}" row \u2014 it stands on that row, never at a section's foot`));
38960
+ else if ((act.on ?? "record") !== "record") findings.push(error51("record.section-acts", path16, `names "${name}", which runs over the register's rows \u2014 a section's foot holds the acts on this record`));
38961
+ else if (home !== void 0 && home.section !== index) findings.push(error51("record.move-home", path16, misplacedMove(act, sections[home.section]?.title ?? "", home.entry)));
38962
+ else if (home !== void 0 || isCorrectionAct(model, app, act)) findings.push(...checkFootNeeds(model, entity, section, drawn, act, path16));
38963
+ else findings.push(...checkStagedAct(display, section, act, path16), ...checkFootNeeds(model, entity, section, drawn, act, path16));
38951
38964
  if (act !== void 0 && hiddenWhenOffered(section.when, act.when)) {
38952
- findings.push(error51("record.act-hidden", path15, `"${name}" is offered only while "${section.title}" is hidden by its \`when\` \u2014 its button is never drawn; drop the section's \`when\`, or stand the act elsewhere`));
38965
+ findings.push(error51("record.act-hidden", path16, `"${name}" is offered only while "${section.title}" is hidden by its \`when\` \u2014 its button is never drawn; drop the section's \`when\`, or stand the act elsewhere`));
38953
38966
  }
38954
38967
  const first2 = actsPlaced.get(name);
38955
- if (first2 !== void 0) findings.push(error51("record.placed-once", path15, `places "${name}" a second time on the record (it is already at the foot of ${first2})`));
38968
+ if (first2 !== void 0) findings.push(error51("record.placed-once", path16, `places "${name}" a second time on the record (it is already at the foot of ${first2})`));
38956
38969
  else actsPlaced.set(name, `the section "${section.title}"`);
38957
38970
  }
38958
38971
  }
@@ -38973,27 +38986,27 @@ function checkApp(model, app, at2) {
38973
38986
  );
38974
38987
  }
38975
38988
  for (const [index, check2] of (app.checks ?? []).entries()) {
38976
- const path15 = `${at2}.checks.${index}`;
38977
- findings.push(...checkResolve(model, app, check2, `${path15}.resolve`));
38989
+ const path16 = `${at2}.checks.${index}`;
38990
+ findings.push(...checkResolve(model, app, check2, `${path16}.resolve`));
38978
38991
  if (check2.of !== void 0) {
38979
- findings.push(...checkChildCheck(model, check2, children, acts, path15));
38992
+ findings.push(...checkChildCheck(model, check2, children, acts, path16));
38980
38993
  continue;
38981
38994
  }
38982
- const field = own(check2.field, `${path15}.field`);
38995
+ const field = own(check2.field, `${path16}.field`);
38983
38996
  const result = field?.type === "formula" ? resolvedFieldType(field, entity, model.entities) : void 0;
38984
38997
  if (field !== void 0 && (result === void 0 || !["boolean", "text"].includes(result))) {
38985
- findings.push(error51("check.field", `${path15}.field`, `"${check2.field}" is a ${field.type} field \u2014 a check is a yes/no or text formula that stands while true or non-empty`));
38998
+ findings.push(error51("check.field", `${path16}.field`, `"${check2.field}" is a ${field.type} field \u2014 a check is a yes/no or text formula that stands while true or non-empty`));
38986
38999
  }
38987
39000
  const drawn = placed.get(check2.field);
38988
39001
  if (drawn !== void 0) {
38989
- findings.push(error51("check.not-placed", `${path15}.field`, `"${check2.field}" is a check, whose line under the header says it, and it is also in ${drawn} \u2014 drop it there`));
39002
+ findings.push(error51("check.not-placed", `${path16}.field`, `"${check2.field}" is a check, whose line under the header says it, and it is also in ${drawn} \u2014 drop it there`));
38990
39003
  }
38991
- findings.push(...checkRestatesMeter(model, entity, check2, path15));
39004
+ findings.push(...checkRestatesMeter(model, entity, check2, path16));
38992
39005
  if (check2.blocks === void 0 || check2.blocks === "all") continue;
38993
39006
  for (const [at22, target] of check2.blocks.entries()) {
38994
39007
  if (target === "edit" || target === "delete") continue;
38995
- if (!acts.has(target)) findings.push(error51("check.blocks", `${path15}.blocks.${at22}`, `names "${target}", which is not an act of this app, "edit", "delete" or "all"`));
38996
- else if (acts.get(target) !== void 0) findings.push(error51("check.blocks", `${path15}.blocks.${at22}`, `names "${target}", an act of "${acts.get(target) ?? ""}" rows \u2014 a check of that child refuses it`));
39008
+ if (!acts.has(target)) findings.push(error51("check.blocks", `${path16}.blocks.${at22}`, `names "${target}", which is not an act of this app, "edit", "delete" or "all"`));
39009
+ else if (acts.get(target) !== void 0) findings.push(error51("check.blocks", `${path16}.blocks.${at22}`, `names "${target}", an act of "${acts.get(target) ?? ""}" rows \u2014 a check of that child refuses it`));
38997
39010
  }
38998
39011
  }
38999
39012
  return findings;
@@ -39002,14 +39015,14 @@ function noteChildLists(model, app, at2) {
39002
39015
  const placed = (app.record?.sections ?? []).flatMap(
39003
39016
  (section, index) => (section.blocks ?? []).flatMap((block, order) => isRowsBlock(block) ? [{ block, path: `${at2}.record.sections.${index}.blocks.${order}` }] : [])
39004
39017
  );
39005
- const planned = placed.flatMap(({ block, path: path15 }) => {
39018
+ const planned = placed.flatMap(({ block, path: path16 }) => {
39006
39019
  const child = entityOf(model, block.rows);
39007
39020
  const axis = child === void 0 ? void 0 : agendaAxis(model, child);
39008
39021
  if (axis === void 0 || "refused" in axis || axis.time === void 0 && axis.part === void 0) return [];
39009
39022
  const within = axis.time === void 0 ? `in a "${axis.part ?? ""}"` : "at an hour";
39010
- return [note("block.agenda-note", path15, `lists "${block.rows}" rows, each on a day ("${axis.day}") ${within} \u2014 a table reads them as rows; an \`agenda\` draws them by day`)];
39023
+ return [note("block.agenda-note", path16, `lists "${block.rows}" rows, each on a day ("${axis.day}") ${within} \u2014 a table reads them as rows; an \`agenda\` draws them by day`)];
39011
39024
  });
39012
- const covering = placed.flatMap(({ block, path: path15 }) => {
39025
+ const covering = placed.flatMap(({ block, path: path16 }) => {
39013
39026
  if (!(entityOf(model, block.rows)?.fields.some((field) => field.type === "files") ?? false)) return [];
39014
39027
  if (Array.isArray(block.create)) return [];
39015
39028
  const covers = placed.find((other) => {
@@ -39022,7 +39035,7 @@ function noteChildLists(model, app, at2) {
39022
39035
  return !("refused" in shape) && shape.entity === block.rows;
39023
39036
  });
39024
39037
  });
39025
- return covers === void 0 ? [] : [note("block.under-note", path15, `"${block.rows}" rows cover "${covers.block.rows}" lines listed in ${covers.path} \u2014 draw them over the lines they cover: \`"under"\` on that block`)];
39038
+ return covers === void 0 ? [] : [note("block.under-note", path16, `"${block.rows}" rows cover "${covers.block.rows}" lines listed in ${covers.path} \u2014 draw them over the lines they cover: \`"under"\` on that block`)];
39026
39039
  });
39027
39040
  return [...planned, ...covering];
39028
39041
  }
@@ -39036,10 +39049,10 @@ function checkDocumentsListed(model, entity, sections, at2) {
39036
39049
  return shape === void 0 || "refused" in shape ? [] : [[shape.entity, block.title ?? child?.label ?? block.rows]];
39037
39050
  })
39038
39051
  );
39039
- return placed.flatMap(({ block, path: path15 }) => {
39052
+ return placed.flatMap(({ block, path: path16 }) => {
39040
39053
  if (!isRowsBlock(block)) return [];
39041
39054
  const lines = over.get(block.rows);
39042
- return lines === void 0 ? [] : [error51("block.under-once", path15, `lists "${block.rows}" rows, which stand over their lines in "${lines}" (\`under\`) \u2014 a second list says them twice; drop it`)];
39055
+ return lines === void 0 ? [] : [error51("block.under-once", path16, `lists "${block.rows}" rows, which stand over their lines in "${lines}" (\`under\`) \u2014 a second list says them twice; drop it`)];
39043
39056
  });
39044
39057
  }
39045
39058
  function hiddenWhenOffered(shown, offered) {
@@ -39050,20 +39063,20 @@ function hiddenWhenOffered(shown, offered) {
39050
39063
  return !held.some((option) => options.includes(option));
39051
39064
  });
39052
39065
  }
39053
- function checkFilter(model, entity, stated, path15, around) {
39066
+ function checkFilter(model, entity, stated, path16, around) {
39054
39067
  const name = filterField(stated);
39055
39068
  const filter2 = fieldOf(entity, name);
39056
- if (filter2 === void 0) return [error51("model.names-declared", typeof stated === "string" ? path15 : `${path15}.field`, `names "${name}", which is not a field of "${entity.alias}"`)];
39069
+ if (filter2 === void 0) return [error51("model.names-declared", typeof stated === "string" ? path16 : `${path16}.field`, `names "${name}", which is not a field of "${entity.alias}"`)];
39057
39070
  const type = resolvedFieldType(filter2, entity, model.entities) ?? filter2.type;
39058
- if (name === around.chips) return [error51("register.filter", path15, `"${name}" is the status, already the register's chips \u2014 drop it`)];
39071
+ if (name === around.chips) return [error51("register.filter", path16, `"${name}" is the status, already the register's chips \u2014 drop it`)];
39059
39072
  if (!FILTER_TYPES.has(type)) {
39060
39073
  const read = filter2.type === "lookup" ? readField(model, entity, filter2) : void 0;
39061
39074
  const kind = read === void 0 ? `a ${filter2.type} field` : `a lookup of a ${read.field.type} field`;
39062
- return [error51("register.filter", path15, `"${name}" is ${kind} \u2014 ${around.owner} narrows by a select, a member, a link, a date, a yes/no or a number`)];
39075
+ return [error51("register.filter", path16, `"${name}" is ${kind} \u2014 ${around.owner} narrows by a select, a member, a link, a date, a yes/no or a number`)];
39063
39076
  }
39064
- if (typeof stated !== "string") return checkTiers(model, entity, filter2, type, `${path15}.tiers`);
39077
+ if (typeof stated !== "string") return checkTiers(model, entity, filter2, type, `${path16}.tiers`);
39065
39078
  if (around.splits.has(name) && type !== "select_member") {
39066
- return [note("register.filter-split", path15, `"${name}" is also what a picture above the rows splits them by \u2014 a press on its part narrows by it`)];
39079
+ return [note("register.filter-split", path16, `"${name}" is also what a picture above the rows splits them by \u2014 a press on its part narrows by it`)];
39067
39080
  }
39068
39081
  return [];
39069
39082
  }
@@ -39071,36 +39084,36 @@ function checkOrder(entity, sort, base) {
39071
39084
  const findings = [];
39072
39085
  const sorted = /* @__PURE__ */ new Set();
39073
39086
  for (const [index, key] of registerSortKeys(sort).entries()) {
39074
- const path15 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
39087
+ const path16 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
39075
39088
  const field = fieldOf(entity, key.field);
39076
- if (field === void 0) findings.push(error51("model.names-declared", path15, `names "${key.field}", which is not a field of "${entity.alias}"`));
39077
- else if (!isSortableFieldType(field.type)) findings.push(error51("register.sort", path15, `"${key.field}" is a ${field.type} field, which holds no order`));
39078
- if (sorted.has(key.field)) findings.push(error51("register.sort", path15, `"${key.field}" is already a key of this order \u2014 a field orders the rows once`));
39089
+ if (field === void 0) findings.push(error51("model.names-declared", path16, `names "${key.field}", which is not a field of "${entity.alias}"`));
39090
+ else if (!isSortableFieldType(field.type)) findings.push(error51("register.sort", path16, `"${key.field}" is a ${field.type} field, which holds no order`));
39091
+ if (sorted.has(key.field)) findings.push(error51("register.sort", path16, `"${key.field}" is already a key of this order \u2014 a field orders the rows once`));
39079
39092
  sorted.add(key.field);
39080
39093
  }
39081
39094
  return findings;
39082
39095
  }
39083
- function checkSearch(model, entity, name, path15) {
39096
+ function checkSearch(model, entity, name, path16) {
39084
39097
  const field = fieldOf(entity, name);
39085
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
39098
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39086
39099
  const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
39087
- return SEARCH_TYPES.has(type) ? [] : [error51("register.search", path15, `"${name}" is a ${field.type} field \u2014 a search matches words: text, a code, or a link by its row's title`)];
39100
+ return SEARCH_TYPES.has(type) ? [] : [error51("register.search", path16, `"${name}" is a ${field.type} field \u2014 a search matches words: text, a code, or a link by its row's title`)];
39088
39101
  }
39089
- function checkGroup(model, entity, display, name, path15, around) {
39102
+ function checkGroup(model, entity, display, name, path16, around) {
39090
39103
  const field = fieldOf(entity, name);
39091
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
39104
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39092
39105
  const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
39093
39106
  const read = readField(model, entity, field);
39094
39107
  const held = read?.field ?? field;
39095
39108
  const several = read?.several === true || (held.type === "select" || held.type === "select_member") && held.multi === true;
39096
39109
  const many = held.type === "select_record_link" && held.cardinality !== "one";
39097
39110
  if (several || many || !GROUP_TYPES.has(type)) {
39098
- return [error51("register.group", path15, `"${name}" is a ${several || many ? `${field.type} field holding several values` : `${field.type} field`} \u2014 a row stands in one group: a single select, a one-row link, one member, a date, a yes/no or text`)];
39111
+ return [error51("register.group", path16, `"${name}" is a ${several || many ? `${field.type} field holding several values` : `${field.type} field`} \u2014 a row stands in one group: a single select, a one-row link, one member, a date, a yes/no or text`)];
39099
39112
  }
39100
- if (name === around.chips) return [error51("register.group", path15, `"${name}" is the status, whose chips already split the rows \u2014 group by another field`)];
39101
- if (around.layout !== "table" && around.layout !== "gantt") return [error51("register.group", path15, `groups a ${around.layout} register \u2014 rows are grouped in a table, and a gantt's lanes`)];
39113
+ if (name === around.chips) return [error51("register.group", path16, `"${name}" is the status, whose chips already split the rows \u2014 group by another field`)];
39114
+ if (around.layout !== "table" && around.layout !== "gantt") return [error51("register.group", path16, `groups a ${around.layout} register \u2014 rows are grouped in a table, and a gantt's lanes`)];
39102
39115
  if (name === display.title && (display.subtitle ?? []).length === 0) {
39103
- return [error51("register.group", path15, `groups by "${name}", the row's title, so each row leads with its first subtitle \u2014 state \`records.${entity.alias}.subtitle\` (the person, the party)`)];
39116
+ return [error51("register.group", path16, `groups by "${name}", the row's title, so each row leads with its first subtitle \u2014 state \`records.${entity.alias}.subtitle\` (the person, the party)`)];
39104
39117
  }
39105
39118
  return [];
39106
39119
  }
@@ -39122,16 +39135,16 @@ function likelySection(model, app, act, drawn) {
39122
39135
  const best = Math.max(0, ...scores);
39123
39136
  return best === 0 ? void 0 : scores.indexOf(best);
39124
39137
  }
39125
- function checkResolve(model, app, check2, path15) {
39138
+ function checkResolve(model, app, check2, path16) {
39126
39139
  if (check2.resolve === void 0) return [];
39127
39140
  const act = (app.acts ?? []).find((one) => one.alias === check2.resolve);
39128
- if (act === void 0) return [error51("model.names-declared", path15, `names "${check2.resolve}", which is not an act of this app`)];
39141
+ if (act === void 0) return [error51("model.names-declared", path16, `names "${check2.resolve}", which is not an act of this app`)];
39129
39142
  if (act.of !== check2.of) {
39130
- return [error51("check.resolve", path15, `"${act.alias}" works on "${act.of ?? app.entity}" rows and the check stands on "${check2.of ?? app.entity}" rows \u2014 its way out acts on the rows it stands on`)];
39143
+ return [error51("check.resolve", path16, `"${act.alias}" works on "${act.of ?? app.entity}" rows and the check stands on "${check2.of ?? app.entity}" rows \u2014 its way out acts on the rows it stands on`)];
39131
39144
  }
39132
- if ((act.on ?? "record") !== "record") return [error51("check.resolve", path15, `"${act.alias}" runs over the register's rows \u2014 a check's way out acts on the one row it stands on`)];
39133
- if (checkRefuses(check2, act.alias)) return [error51("check.resolve", path15, `"${act.alias}" is an act this check refuses \u2014 its way out is one it lets through`)];
39134
- if (isCorrectionAct(model, app, act)) return [error51("check.resolve", path15, `"${act.alias}" corrects what is done and waits in a \u22EF, where the check's link cannot point \u2014 its way out is an act drawn as a button`)];
39145
+ if ((act.on ?? "record") !== "record") return [error51("check.resolve", path16, `"${act.alias}" runs over the register's rows \u2014 a check's way out acts on the one row it stands on`)];
39146
+ if (checkRefuses(check2, act.alias)) return [error51("check.resolve", path16, `"${act.alias}" is an act this check refuses \u2014 its way out is one it lets through`)];
39147
+ if (isCorrectionAct(model, app, act)) return [error51("check.resolve", path16, `"${act.alias}" corrects what is done and waits in a \u22EF, where the check's link cannot point \u2014 its way out is an act drawn as a button`)];
39135
39148
  return [];
39136
39149
  }
39137
39150
  function checkPages(door, sections, at2) {
@@ -39139,16 +39152,16 @@ function checkPages(door, sections, at2) {
39139
39152
  const addressed = /* @__PURE__ */ new Map();
39140
39153
  for (const [index, section] of sections.entries()) {
39141
39154
  if (section.page !== true) continue;
39142
- const path15 = `${at2}.sections.${index}.page`;
39155
+ const path16 = `${at2}.sections.${index}.page`;
39143
39156
  if (door === "drawer" || door === "beside") {
39144
39157
  const has = door === "drawer" ? "a drawer" : "a record beside the list";
39145
- findings.push(error51("record.page", path15, `"${section.title}" is a page of its own, and ${has} has no pages; drop \`page\`, or make the door a page`));
39158
+ findings.push(error51("record.page", path16, `"${section.title}" is a page of its own, and ${has} has no pages; drop \`page\`, or make the door a page`));
39146
39159
  }
39147
- if (section.at !== void 0) findings.push(error51("record.page", path15, `"${section.title}" is a step of the record's path, read in its scroll \u2014 drop \`page\`, or \`at\``));
39160
+ if (section.at !== void 0) findings.push(error51("record.page", path16, `"${section.title}" is a step of the record's path, read in its scroll \u2014 drop \`page\`, or \`at\``));
39148
39161
  const segment = pageSegment(section.title);
39149
39162
  const first2 = addressed.get(segment);
39150
- if (segment === "") findings.push(error51("record.page", path15, `"${section.title}" holds no letter or digit to name its page's address \u2014 retitle it`));
39151
- else if (first2 !== void 0) findings.push(error51("record.page", path15, `"${section.title}" and "${first2}" make one page address, "${segment}" \u2014 retitle one`));
39163
+ if (segment === "") findings.push(error51("record.page", path16, `"${section.title}" holds no letter or digit to name its page's address \u2014 retitle it`));
39164
+ else if (first2 !== void 0) findings.push(error51("record.page", path16, `"${section.title}" and "${first2}" make one page address, "${segment}" \u2014 retitle one`));
39152
39165
  else addressed.set(segment, section.title);
39153
39166
  }
39154
39167
  if (sections.length > 0 && sections.every((section) => section.page === true)) {
@@ -39175,20 +39188,20 @@ function checkRecordLayout(app, at2) {
39175
39188
  }
39176
39189
  return findings;
39177
39190
  }
39178
- function checkSectionAt(entity, display, at2, path15) {
39191
+ function checkSectionAt(entity, display, at2, path16) {
39179
39192
  if (at2 === void 0) return [];
39180
39193
  const status = display.status;
39181
- if (status === void 0) return [error51("record.at", path15, `stages the section by the status, and "${entity.alias}" has none \u2014 state its \`records\` status, or drop \`at\``)];
39194
+ if (status === void 0) return [error51("record.at", path16, `stages the section by the status, and "${entity.alias}" has none \u2014 state its \`records\` status, or drop \`at\``)];
39182
39195
  const stages = milestonesOf(display);
39183
39196
  if (stages !== void 0) {
39184
39197
  const named = new Set(stages.map((stage) => stage.field));
39185
- return at2.flatMap((name) => named.has(name) ? [] : [error51("record.at", path15, `names "${name}", which is not one of "${entity.alias}"'s milestones (${[...named].join(", ")})`)]);
39198
+ return at2.flatMap((name) => named.has(name) ? [] : [error51("record.at", path16, `names "${name}", which is not one of "${entity.alias}"'s milestones (${[...named].join(", ")})`)]);
39186
39199
  }
39187
39200
  const field = status.field === void 0 ? void 0 : fieldOf(entity, status.field);
39188
39201
  const options = new Set(field?.type === "select" ? (field.options ?? []).map((option) => option.alias) : []);
39189
- return at2.flatMap((name) => options.has(name) ? [] : [error51("record.at", path15, `names "${name}", which is not an option of the status "${status.field ?? ""}"`)]);
39202
+ return at2.flatMap((name) => options.has(name) ? [] : [error51("record.at", path16, `names "${name}", which is not an option of the status "${status.field ?? ""}"`)]);
39190
39203
  }
39191
- function checkStagedAct(display, section, act, path15) {
39204
+ function checkStagedAct(display, section, act, path16) {
39192
39205
  const at2 = section.at;
39193
39206
  const status = display.status;
39194
39207
  if (at2 === void 0 || status === void 0) return [];
@@ -39201,16 +39214,16 @@ function checkStagedAct(display, section, act, path15) {
39201
39214
  return before !== void 0 && at2.includes(before);
39202
39215
  });
39203
39216
  if (inStep) return [];
39204
- return [error51("record.act-staged", path15, `"${act.alias}" stamps no step after ${at2.map((one) => `"${one}"`).join(" or ")}, so it is offered while "${section.title}" is not the work \u2014 ${elsewhere}`)];
39217
+ return [error51("record.act-staged", path16, `"${act.alias}" stamps no step after ${at2.map((one) => `"${one}"`).join(" or ")}, so it is offered while "${section.title}" is not the work \u2014 ${elsewhere}`)];
39205
39218
  }
39206
39219
  const field = status.field;
39207
39220
  if (field === void 0) return [];
39208
39221
  const held = heldOptions(act.when, field);
39209
39222
  if (held === void 0) {
39210
- return [error51("record.act-staged", path15, `"${act.alias}" is offered whatever "${field}" holds, and "${section.title}" is the work only while it is ${at2.join(" or ")} \u2014 narrow its \`when\` to that, or ${elsewhere}`)];
39223
+ return [error51("record.act-staged", path16, `"${act.alias}" is offered whatever "${field}" holds, and "${section.title}" is the work only while it is ${at2.join(" or ")} \u2014 narrow its \`when\` to that, or ${elsewhere}`)];
39211
39224
  }
39212
39225
  const outside = held.filter((option) => !at2.includes(option));
39213
- return outside.length === 0 ? [] : [error51("record.act-staged", path15, `"${act.alias}" is offered while "${field}" is ${outside.join(" or ")}, when "${section.title}" is not the work \u2014 ${elsewhere}`)];
39226
+ return outside.length === 0 ? [] : [error51("record.act-staged", path16, `"${act.alias}" is offered while "${field}" is ${outside.join(" or ")}, when "${section.title}" is not the work \u2014 ${elsewhere}`)];
39214
39227
  }
39215
39228
  function blockIdentity(block) {
39216
39229
  if (isRowsBlock(block)) {
@@ -39224,20 +39237,20 @@ function blockIdentity(block) {
39224
39237
  }
39225
39238
  function checkWhere(child, block, at2) {
39226
39239
  return Object.entries(block.where ?? {}).flatMap(([name, options]) => {
39227
- const path15 = `${at2}.where.${name}`;
39240
+ const path16 = `${at2}.where.${name}`;
39228
39241
  const field = fieldOf(child, name);
39229
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${child.alias}"`)];
39242
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`)];
39230
39243
  if (field.type !== "select" || field.multi === true) {
39231
- return [error51("block.where", path15, `"${name}" is a ${field.type === "select" ? "multi-select" : `${field.type} field`} \u2014 a block narrows its rows by a single select, each row holding one option`)];
39244
+ return [error51("block.where", path16, `"${name}" is a ${field.type === "select" ? "multi-select" : `${field.type} field`} \u2014 a block narrows its rows by a single select, each row holding one option`)];
39232
39245
  }
39233
39246
  if (field.required !== true) {
39234
- return [error51("block.where", path15, `"${name}" is optional on "${child.alias}" \u2014 a row holding none would stand in no block while every unnarrowed total counts it; make it required`)];
39247
+ return [error51("block.where", path16, `"${name}" is optional on "${child.alias}" \u2014 a row holding none would stand in no block while every unnarrowed total counts it; make it required`)];
39235
39248
  }
39236
39249
  const held = new Set((field.options ?? []).map((option) => option.alias));
39237
39250
  const stray = options.filter((option) => !held.has(option));
39238
39251
  return [
39239
- ...stray.length === 0 ? [] : [error51("block.where", path15, `names ${stray.map((one) => `"${one}"`).join(", ")}, which "${name}" has no option of`)],
39240
- ...options.length === 1 && (block.columns ?? []).includes(name) ? [error51("block.where", path15, `holds every row at "${options[0] ?? ""}", so its column says one thing on every row \u2014 drop the column`)] : []
39252
+ ...stray.length === 0 ? [] : [error51("block.where", path16, `names ${stray.map((one) => `"${one}"`).join(", ")}, which "${name}" has no option of`)],
39253
+ ...options.length === 1 && (block.columns ?? []).includes(name) ? [error51("block.where", path16, `holds every row at "${options[0] ?? ""}", so its column says one thing on every row \u2014 drop the column`)] : []
39241
39254
  ];
39242
39255
  });
39243
39256
  }
@@ -39248,19 +39261,19 @@ function checkChildColumns(model, child, names, back, expecting, at2) {
39248
39261
  const bounds = meterBounds(display, names);
39249
39262
  const columns = /* @__PURE__ */ new Map();
39250
39263
  for (const [index, name] of names.entries()) {
39251
- const path15 = `${at2}.columns.${index}`;
39264
+ const path16 = `${at2}.columns.${index}`;
39252
39265
  if (fieldOf(child, name) === void 0) {
39253
- findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${child.alias}"`));
39266
+ findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`));
39254
39267
  continue;
39255
39268
  }
39256
39269
  const part = rowParts.get(name);
39257
39270
  const inPlace = expecting && name === display?.figure && expectedColumns(model, child, [name]).length > 0;
39258
- if (part !== void 0 && !inPlace) findings.push(error51("block.column-drawn", path15, `"${name}" is already drawn by the "${child.alias}" row itself, as ${part} \u2014 drop the column`));
39271
+ if (part !== void 0 && !inPlace) findings.push(error51("block.column-drawn", path16, `"${name}" is already drawn by the "${child.alias}" row itself, as ${part} \u2014 drop the column`));
39259
39272
  const meter = bounds.get(name);
39260
- if (meter !== void 0) findings.push(error51("block.column-drawn", path15, `"${name}" is the bound of the meter "${meter}" on the same row, which states it \u2014 drop the column`));
39261
- if (name === back) findings.push(error51("block.column-drawn", path15, `"${name}" is the link back to this record, the same on every row of the block \u2014 drop the column`));
39273
+ if (meter !== void 0) findings.push(error51("block.column-drawn", path16, `"${name}" is the bound of the meter "${meter}" on the same row, which states it \u2014 drop the column`));
39274
+ if (name === back) findings.push(error51("block.column-drawn", path16, `"${name}" is the link back to this record, the same on every row of the block \u2014 drop the column`));
39262
39275
  const first2 = columns.get(name);
39263
- if (first2 !== void 0) findings.push(error51("block.column-drawn", path15, `"${name}" is already column ${first2} \u2014 a field is one column`));
39276
+ if (first2 !== void 0) findings.push(error51("block.column-drawn", path16, `"${name}" is already column ${first2} \u2014 a field is one column`));
39264
39277
  else columns.set(name, index);
39265
39278
  }
39266
39279
  return findings;
@@ -39278,9 +39291,9 @@ function checkChildCreate(model, child, create, via, at2) {
39278
39291
  }
39279
39292
  return findings;
39280
39293
  }
39281
- function checkUnder(model, entity, child, block, path15) {
39294
+ function checkUnder(model, entity, child, block, path16) {
39282
39295
  if (block.under === void 0) return [];
39283
- const at2 = `${path15}.under`;
39296
+ const at2 = `${path16}.under`;
39284
39297
  const shape = documentUnder(model, entity.alias, child, block.under);
39285
39298
  if ("refused" in shape) return [error51("block.under", at2, shape.refused)];
39286
39299
  const findings = [];
@@ -39292,11 +39305,11 @@ function checkUnder(model, entity, child, block, path15) {
39292
39305
  for (const [index, name] of (block.columns ?? []).entries()) {
39293
39306
  const column = fieldOf(child, name);
39294
39307
  if (column?.type === "text" && column.format === "markdown") {
39295
- findings.push(error51("block.under", `${path15}.columns.${index}`, `"${name}" is long text, drawn whole on a card per row, and lines under a document are a table of short facts \u2014 drop the column or \`under\``));
39308
+ findings.push(error51("block.under", `${path16}.columns.${index}`, `"${name}" is long text, drawn whole on a card per row, and lines under a document are a table of short facts \u2014 drop the column or \`under\``));
39296
39309
  continue;
39297
39310
  }
39298
39311
  if (name !== block.under && !(column?.type === "lookup" && column.source_field_alias === block.under)) continue;
39299
- findings.push(error51("block.under", `${path15}.columns.${index}`, `"${name}" reads the "${shape.entity}" each line stands under, which its heading already reads \u2014 drop the column`));
39312
+ findings.push(error51("block.under", `${path16}.columns.${index}`, `"${name}" reads the "${shape.entity}" each line stands under, which its heading already reads \u2014 drop the column`));
39300
39313
  }
39301
39314
  return findings;
39302
39315
  }
@@ -39314,22 +39327,22 @@ function checkRoster(model, entity, app, at2) {
39314
39327
  const layout = app.register?.layout;
39315
39328
  const roster = app.register?.roster;
39316
39329
  const expect = app.register?.expect;
39317
- const path15 = `${at2}.register.roster`;
39330
+ const path16 = `${at2}.register.roster`;
39318
39331
  if (roster === void 0) {
39319
39332
  return [
39320
- ...layout === "roster" ? [error51("register.roster", path15, `a roster fills each day with the rows of another entity \u2014 name it in \`roster\` (a shift, a trip, an attendance)`)] : [],
39333
+ ...layout === "roster" ? [error51("register.roster", path16, `a roster fills each day with the rows of another entity \u2014 name it in \`roster\` (a shift, a trip, an attendance)`)] : [],
39321
39334
  ...expect === void 0 ? [] : [error51("register.roster", `${at2}.register.expect`, "names the columns of a roster's rows, and the register states no `roster` \u2014 name the rows first")],
39322
39335
  ...app.register?.of === void 0 || expect !== void 0 ? [] : [error51("register.roster", `${at2}.register.of`, "narrows a roster's expected columns, and the register expects none \u2014 state `expect` first")]
39323
39336
  ];
39324
39337
  }
39325
- if (layout !== "roster") return [error51("register.roster", path15, `names the rows a roster's days hold, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "roster"\`, or drop it`)];
39338
+ if (layout !== "roster") return [error51("register.roster", path16, `names the rows a roster's days hold, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "roster"\`, or drop it`)];
39326
39339
  if ((app.register?.columns ?? []).length > 0) return [error51("register.roster", `${at2}.register.columns`, `a roster's width is its days, so it draws no columns \u2014 drop \`columns\`; a fact of each row stands in its record`)];
39327
39340
  if (app.register?.of !== void 0 && expect === void 0) return [error51("register.roster", `${at2}.register.of`, "narrows a roster's expected columns, and the register expects none \u2014 state `expect` first")];
39328
39341
  const shape = rosterShape(model, entity.alias, roster, expect === void 0);
39329
- if ("refused" in shape) return [error51("register.roster", path15, shape.refused)];
39342
+ if ("refused" in shape) return [error51("register.roster", path16, shape.refused)];
39330
39343
  const child = entityOf(model, shape.entity);
39331
39344
  if (child === void 0) return [];
39332
- return [...checkRosterColumns(entity, child, app, at2), ...app.reads === "shared" ? [] : unruledChild(model, entity, child, shape.via, path15)];
39345
+ return [...checkRosterColumns(entity, child, app, at2), ...app.reads === "shared" ? [] : unruledChild(model, entity, child, shape.via, path16)];
39333
39346
  }
39334
39347
  function checkRosterColumns(entity, child, app, at2) {
39335
39348
  const expect = app.register?.expect;
@@ -39354,16 +39367,16 @@ function checkLanes(model, entity, app, at2) {
39354
39367
  const layout = app.register?.layout;
39355
39368
  const lanes = app.register?.lanes;
39356
39369
  const loads = app.register?.loads;
39357
- const path15 = `${at2}.register.lanes`;
39370
+ const path16 = `${at2}.register.lanes`;
39358
39371
  if (lanes === void 0) {
39359
39372
  return [
39360
- ...layout === "lanes" ? [error51("register.lanes", path15, "a lanes board stands each row in the lane its one-link names \u2014 name that link in `lanes` (a chair, a machine, a room)")] : [],
39373
+ ...layout === "lanes" ? [error51("register.lanes", path16, "a lanes board stands each row in the lane its one-link names \u2014 name that link in `lanes` (a chair, a machine, a room)")] : [],
39361
39374
  ...loads === void 0 ? [] : [error51("register.loads", `${at2}.register.loads`, "names what a lane carries, and the register has no `lanes` \u2014 state the one-link its rows stand in, or drop it")]
39362
39375
  ];
39363
39376
  }
39364
- if (layout !== "lanes") return [error51("register.lanes", path15, `names the lanes a board stands its rows in, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "lanes"\`, or drop it`)];
39377
+ if (layout !== "lanes") return [error51("register.lanes", path16, `names the lanes a board stands its rows in, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "lanes"\`, or drop it`)];
39365
39378
  const shape = lanesShape(model, entity.alias, lanes);
39366
- if ("refused" in shape) return [error51("register.lanes", path15, shape.refused)];
39379
+ if ("refused" in shape) return [error51("register.lanes", path16, shape.refused)];
39367
39380
  if (loads === void 0) return [];
39368
39381
  const count2 = Object.keys(loads).length;
39369
39382
  if (count2 === 0 || count2 > LANE_LOADS) return [error51("register.loads", `${at2}.register.loads`, `names ${count2} loads \u2014 a lane reads one or ${LANE_LOADS}, the ones it runs out of first (its weight, its volume)`)];
@@ -39372,22 +39385,22 @@ function checkLanes(model, entity, app, at2) {
39372
39385
  function checkGantt(model, entity, app, at2) {
39373
39386
  const layout = app.register?.layout;
39374
39387
  const gantt = app.register?.gantt;
39375
- const path15 = `${at2}.register.gantt`;
39388
+ const path16 = `${at2}.register.gantt`;
39376
39389
  if (gantt === void 0) {
39377
- return layout === "gantt" ? [error51("register.gantt", path15, "a gantt draws each row as a bar between two dates \u2014 name them in `gantt` (`start`, and `end` where the entity states no `due`)")] : [];
39390
+ return layout === "gantt" ? [error51("register.gantt", path16, "a gantt draws each row as a bar between two dates \u2014 name them in `gantt` (`start`, and `end` where the entity states no `due`)")] : [];
39378
39391
  }
39379
- if (layout !== "gantt") return [error51("register.gantt", path15, `names the dates a gantt's bars run between, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "gantt"\`, or drop it`)];
39392
+ if (layout !== "gantt") return [error51("register.gantt", path16, `names the dates a gantt's bars run between, and the register is ${layout === void 0 ? "a table" : `\`${layout}\``} \u2014 state \`layout: "gantt"\`, or drop it`)];
39380
39393
  const shape = ganttShape(model, entity.alias, gantt);
39381
- return "refused" in shape ? shape.refused.map((one) => error51("register.gantt", one.at === "" ? path15 : `${path15}.${one.at}`, one.reason)) : [];
39394
+ return "refused" in shape ? shape.refused.map((one) => error51("register.gantt", one.at === "" ? path16 : `${path16}.${one.at}`, one.reason)) : [];
39382
39395
  }
39383
- function checkTiers(model, entity, field, type, path15) {
39384
- if (type !== "number") return [error51("register.tiers", path15, `"${field.alias}" is a ${field.type} field \u2014 tiers band a number; name it bare`)];
39396
+ function checkTiers(model, entity, field, type, path16) {
39397
+ if (type !== "number") return [error51("register.tiers", path16, `"${field.alias}" is a ${field.type} field \u2014 tiers band a number; name it bare`)];
39385
39398
  const select = contractFigureSelect(field, entity, model.entities);
39386
39399
  if (select === void 0) return [];
39387
39400
  return [
39388
39401
  error51(
39389
39402
  "register.tiers",
39390
- path15,
39403
+ path16,
39391
39404
  `"${field.alias}" is read in each row's own ${select.vocabulary} ("${select.alias}"), so a breakpoint is a different amount on each row \u2014 name it bare: its range is typed in the ${select.vocabulary} the reader picks`
39392
39405
  )
39393
39406
  ];
@@ -39397,29 +39410,29 @@ function checkReports(model, entity, app, status, at2) {
39397
39410
  if (reports === void 0 || reports === true) return [];
39398
39411
  const named = /* @__PURE__ */ new Map();
39399
39412
  return reports.flatMap((report, index) => {
39400
- const path15 = `${at2}.register.export.${index}`;
39413
+ const path16 = `${at2}.register.export.${index}`;
39401
39414
  const first2 = named.get(report.label);
39402
39415
  if (first2 === void 0) named.set(report.label, index);
39403
- const folder = Object.keys(report.where ?? {}).includes(app.scope ?? "") ? [error51("app.scope", `${path15}.where.${app.scope}`, `"${app.scope}" is the folder the app is inside \u2014 it already narrows every read; drop it here`)] : [];
39416
+ const folder = Object.keys(report.where ?? {}).includes(app.scope ?? "") ? [error51("app.scope", `${path16}.where.${app.scope}`, `"${app.scope}" is the folder the app is inside \u2014 it already narrows every read; drop it here`)] : [];
39404
39417
  return [
39405
- ...first2 === void 0 ? [] : [error51("register.export-report", `${path15}.label`, `names "${report.label}", as report ${first2} does \u2014 a menu entry is one report`)],
39406
- ...report.template === void 0 ? [] : checkExportTemplate(model, entity.alias, (app.register?.tabs ?? []).map((tab) => tab.tab), report.template, `${path15}.template`),
39407
- ...checkNarrowing(model.entities, entity, report.where, `${path15}.where`),
39418
+ ...first2 === void 0 ? [] : [error51("register.export-report", `${path16}.label`, `names "${report.label}", as report ${first2} does \u2014 a menu entry is one report`)],
39419
+ ...report.template === void 0 ? [] : checkExportTemplate(model, entity.alias, (app.register?.tabs ?? []).map((tab) => tab.tab), report.template, `${path16}.template`),
39420
+ ...checkNarrowing(model.entities, entity, report.where, `${path16}.where`),
39408
39421
  ...folder,
39409
- ...report.per === void 0 ? [] : checkPer(model, entity, report.per, { status, scope: app.scope, fixed: report.where?.[report.per] !== void 0 }, `${path15}.per`),
39410
- ...checkReportBooks(model, app, report, path15)
39422
+ ...report.per === void 0 ? [] : checkPer(model, entity, report.per, { status, scope: app.scope, fixed: report.where?.[report.per] !== void 0 }, `${path16}.per`),
39423
+ ...checkReportBooks(model, app, report, path16)
39411
39424
  ];
39412
39425
  });
39413
39426
  }
39414
- function checkPer(model, entity, name, around, path15) {
39427
+ function checkPer(model, entity, name, around, path16) {
39415
39428
  const field = fieldOf(entity, name);
39416
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`)];
39429
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39417
39430
  const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
39418
- if (name === around.status) return [error51("register.export-report", path15, `picks a value of "${name}", the status \u2014 its chips pick one; name it in \`where\``)];
39419
- if (name === around.scope) return [error51("app.scope", path15, `"${name}" is the folder the app is inside \u2014 it already narrows every read; drop it here`)];
39420
- if (type !== "select" && type !== "select_member" && type !== "select_record_link") return [error51("register.export-report", path15, `picks a value of "${name}", a ${type} field \u2014 one value is an option, a person or a linked row`)];
39421
- if (field.type === "select_record_link" && field.cardinality === "many") return [error51("register.export-report", path15, `picks a value of "${name}", a link holding several rows \u2014 a file of one value names one row per row of the report`)];
39422
- return around.fixed ? [error51("register.export-report", path15, `picks a value of "${name}", which its \`where\` already fixes \u2014 drop one`)] : [];
39431
+ if (name === around.status) return [error51("register.export-report", path16, `picks a value of "${name}", the status \u2014 its chips pick one; name it in \`where\``)];
39432
+ if (name === around.scope) return [error51("app.scope", path16, `"${name}" is the folder the app is inside \u2014 it already narrows every read; drop it here`)];
39433
+ if (type !== "select" && type !== "select_member" && type !== "select_record_link") return [error51("register.export-report", path16, `picks a value of "${name}", a ${type} field \u2014 one value is an option, a person or a linked row`)];
39434
+ if (field.type === "select_record_link" && field.cardinality === "many") return [error51("register.export-report", path16, `picks a value of "${name}", a link holding several rows \u2014 a file of one value names one row per row of the report`)];
39435
+ return around.fixed ? [error51("register.export-report", path16, `picks a value of "${name}", which its \`where\` already fixes \u2014 drop one`)] : [];
39423
39436
  }
39424
39437
  function checkRegisterNarrowing(model, entity, app, at2) {
39425
39438
  const where = app.register?.where;
@@ -39427,9 +39440,9 @@ function checkRegisterNarrowing(model, entity, app, at2) {
39427
39440
  const filters = (app.register?.filters ?? []).map(filterField);
39428
39441
  const findings = [...checkWhen(model.entities, entity, where, `${at2}.register.where`, "offer"), ...checkNarrowing(model.entities, entity, opens, `${at2}.register.opens`)];
39429
39442
  for (const [name, kept] of Object.entries(where ?? {})) {
39430
- const path15 = `${at2}.register.where.${name}`;
39431
- if (filters.includes(name)) findings.push(error51("register.where-opens", path15, `"${name}" is fixed by \`where\`, so a chip on it narrows nothing the reader can change \u2014 drop it from \`filters\``));
39432
- if (name === app.scope) findings.push(error51("app.scope", path15, `"${name}" is the folder the app is inside \u2014 it already narrows every read; drop it here`));
39443
+ const path16 = `${at2}.register.where.${name}`;
39444
+ if (filters.includes(name)) findings.push(error51("register.where-opens", path16, `"${name}" is fixed by \`where\`, so a chip on it narrows nothing the reader can change \u2014 drop it from \`filters\``));
39445
+ if (name === app.scope) findings.push(error51("app.scope", path16, `"${name}" is the folder the app is inside \u2014 it already narrows every read; drop it here`));
39433
39446
  const opened = opens?.[name];
39434
39447
  if (opened === void 0) continue;
39435
39448
  const stray = Array.isArray(opened) && Array.isArray(kept) ? opened.filter((option) => !kept.includes(option)) : void 0;
@@ -39457,10 +39470,10 @@ function checkFiltersShown(model, entity, app, at2) {
39457
39470
  ...(register.filters ?? []).map((stated, index) => ({ name: filterField(stated), path: `${at2}.register.filters.${index}`, says: unshown })),
39458
39471
  ...Object.entries(register.opens ?? {}).flatMap(([name, held]) => held === NARROW_ME ? [] : [{ name, path: `${at2}.register.opens.${name}`, says: unshown }]),
39459
39472
  ...(register.readings ?? []).flatMap((reading, index) => {
39460
- const path15 = `${at2}.register.readings.${index}`;
39461
- if (readingEntity(reading) !== entity.alias || checkReading(model, reading, path15).length > 0) return [];
39473
+ const path16 = `${at2}.register.readings.${index}`;
39474
+ if (readingEntity(reading) !== entity.alias || checkReading(model, reading, path16).length > 0) return [];
39462
39475
  const part = "metric" in reading ? "figure" : "pivot" in reading ? "cell" : "part";
39463
- return pressFields(modelPressed(reading)).map((name) => ({ name, path: path15, says: pressed(part) }));
39476
+ return pressFields(modelPressed(reading)).map((name) => ({ name, path: path16, says: pressed(part) }));
39464
39477
  })
39465
39478
  ];
39466
39479
  return unshownFilters(model, entity, narrowed, shown, fixed);
@@ -39480,11 +39493,11 @@ function rowShows(model, entity, display, columns, more) {
39480
39493
  ]);
39481
39494
  }
39482
39495
  function unshownFilters(model, entity, narrowed, shown, fixed) {
39483
- return narrowed.flatMap(({ name, path: path15, says }) => {
39496
+ return narrowed.flatMap(({ name, path: path16, says }) => {
39484
39497
  const field = fieldOf(entity, name);
39485
39498
  const type = field === void 0 ? void 0 : resolvedFieldType(field, entity, model.entities) ?? field.type;
39486
39499
  if (shown.has(name) || fixed.has(name) || type === void 0 || !FILTER_TYPES.has(type)) return [];
39487
- return [error51("register.filter-unshown", path15, says(name))];
39500
+ return [error51("register.filter-unshown", path16, says(name))];
39488
39501
  });
39489
39502
  }
39490
39503
  function checkBlock(model, entity, block, at2, place, scoped, checks) {
@@ -39555,11 +39568,11 @@ function checkBlock(model, entity, block, at2, place, scoped, checks) {
39555
39568
  }
39556
39569
  if ("files" in block) {
39557
39570
  for (const [index, name] of block.files.entries()) {
39558
- const path15 = `${at2}.files.${index}`;
39571
+ const path16 = `${at2}.files.${index}`;
39559
39572
  const field = fieldOf(entity, name);
39560
- if (field === void 0) findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${entity.alias}"`));
39561
- else if (field.type !== "files") findings.push(error51("block.field-kind", path15, `"${name}" is a ${field.type} field \u2014 a files block holds files fields`));
39562
- else place(name, FILES_BLOCK, path15);
39573
+ if (field === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
39574
+ else if (field.type !== "files") findings.push(error51("block.field-kind", path16, `"${name}" is a ${field.type} field \u2014 a files block holds files fields`));
39575
+ else place(name, FILES_BLOCK, path16);
39563
39576
  }
39564
39577
  return findings;
39565
39578
  }
@@ -39581,19 +39594,19 @@ function checkBlockNarrowing(model, child, block, back, checks, at2) {
39581
39594
  }
39582
39595
  const chips = isTaskBlock(model, block) ? display.status?.field : void 0;
39583
39596
  const fixed = /* @__PURE__ */ new Set([...back === void 0 ? [] : [back], ...Object.entries(block.where ?? {}).flatMap(([field, options]) => options.length === 1 ? [field] : [])]);
39584
- const same = (name, path15) => fixed.has(name) ? [error51("block.narrow", path15, `"${name}" is ${name === back ? "the link back to this record" : "held at one option by the block's `where`"}, the same on every row it narrows \u2014 drop it here`)] : [];
39597
+ const same = (name, path16) => fixed.has(name) ? [error51("block.narrow", path16, `"${name}" is ${name === back ? "the link back to this record" : "held at one option by the block's `where`"}, the same on every row it narrows \u2014 drop it here`)] : [];
39585
39598
  for (const [index, stated] of filters.entries()) {
39586
- const path15 = `${at2}.filters.${index}`;
39587
- findings.push(...checkFilter(model, child, stated, path15, { owner: "a rows block", chips, splits: /* @__PURE__ */ new Set() }), ...same(filterField(stated), path15));
39599
+ const path16 = `${at2}.filters.${index}`;
39600
+ findings.push(...checkFilter(model, child, stated, path16, { owner: "a rows block", chips, splits: /* @__PURE__ */ new Set() }), ...same(filterField(stated), path16));
39588
39601
  }
39589
39602
  for (const [index, name] of (block.search ?? []).entries()) findings.push(...checkSearch(model, child, name, `${at2}.search.${index}`));
39590
39603
  findings.push(...checkOrder(child, block.sort, `${at2}.sort`));
39591
39604
  if (block.group !== void 0) {
39592
- const path15 = `${at2}.group`;
39593
- const fixedGroup = same(block.group, path15);
39594
- if (block.under !== void 0) findings.push(error51("block.narrow", path15, `stands its lines under their documents ("${block.under}"), which already group them \u2014 drop \`group\``));
39605
+ const path16 = `${at2}.group`;
39606
+ const fixedGroup = same(block.group, path16);
39607
+ if (block.under !== void 0) findings.push(error51("block.narrow", path16, `stands its lines under their documents ("${block.under}"), which already group them \u2014 drop \`group\``));
39595
39608
  else if (fixedGroup.length > 0) findings.push(...fixedGroup);
39596
- else findings.push(...checkGroup(model, child, display, block.group, path15, { chips, layout: "table" }));
39609
+ else findings.push(...checkGroup(model, child, display, block.group, path16, { chips, layout: "table" }));
39597
39610
  }
39598
39611
  const shown = rowShows(model, child, display, block.columns ?? [], [
39599
39612
  // A child row's line says the warnings standing on it.
@@ -39611,15 +39624,15 @@ function checkBlockEdits(model, child, block, at2) {
39611
39624
  const figure = recordDisplay(model, child.alias)?.figure;
39612
39625
  const shown = /* @__PURE__ */ new Set([...block.columns ?? [], ...figure === void 0 ? [] : [figure]]);
39613
39626
  return edits.flatMap((name, index) => {
39614
- const path15 = `${at2}.edits.${index}`;
39627
+ const path16 = `${at2}.edits.${index}`;
39615
39628
  const field = fieldOf(child, name);
39616
- if (field === void 0) return [error51("model.names-declared", path15, `names "${name}", which is not a field of "${child.alias}"`)];
39617
- if (!shown.has(name)) return [error51("block.edits", path15, `"${name}" is drawn nowhere on the line \u2014 make it a column, or drop it here`)];
39618
- if (!isAsked(field)) return [error51("block.edits", path15, `"${name}" is a ${field.type} field nobody writes \u2014 a line writes a value a person keys`)];
39629
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`)];
39630
+ if (!shown.has(name)) return [error51("block.edits", path16, `"${name}" is drawn nowhere on the line \u2014 make it a column, or drop it here`)];
39631
+ if (!isAsked(field)) return [error51("block.edits", path16, `"${name}" is a ${field.type} field nobody writes \u2014 a line writes a value a person keys`)];
39619
39632
  const long = field.type === "text" && field.format === "markdown";
39620
39633
  const several = field.type === "select" && field.multi === true;
39621
39634
  if (!LINE_EDIT_TYPES.has(field.type) || long || several) {
39622
- return [error51("block.edits", path15, `"${name}" is a ${long ? "long text" : several ? "multi-select" : `${field.type}`} field \u2014 a line holds a number, a date, a few words or one option as its control; the row opens to write the rest`)];
39635
+ return [error51("block.edits", path16, `"${name}" is a ${long ? "long text" : several ? "multi-select" : `${field.type}`} field \u2014 a line holds a number, a date, a few words or one option as its control; the row opens to write the rest`)];
39623
39636
  }
39624
39637
  return [];
39625
39638
  });
@@ -39698,14 +39711,14 @@ var workspaceModelFileSchema = workspaceModelContractSchema.extend({
39698
39711
  }).strict();
39699
39712
  function modelIssueFindings(issues) {
39700
39713
  const unfold = (issue2, at2) => {
39701
- const path15 = [...at2, ...issue2.path];
39702
- if (issue2.code !== "invalid_union") return [{ path: path15, message: issue2.message }];
39714
+ const path16 = [...at2, ...issue2.path];
39715
+ if (issue2.code !== "invalid_union") return [{ path: path16, message: issue2.message }];
39703
39716
  const meant = issue2.errors.filter((arm) => !arm.some((one) => one.path.length === 0 && one.code === "invalid_type"));
39704
39717
  const fewest = Math.min(...meant.map((arm) => arm.length));
39705
39718
  const closest = meant.filter((arm) => arm.length === fewest);
39706
- return closest.length === 1 ? closest[0].flatMap((one) => unfold(one, path15)) : [{ path: path15, message: issue2.message }];
39719
+ return closest.length === 1 ? closest[0].flatMap((one) => unfold(one, path16)) : [{ path: path16, message: issue2.message }];
39707
39720
  };
39708
- return issues.flatMap((issue2) => unfold(issue2, [])).map(({ path: path15, message: message2 }) => error51("model.schema", path15.length === 0 ? "the file" : path15.map(String).join("."), message2));
39721
+ return issues.flatMap((issue2) => unfold(issue2, [])).map(({ path: path16, message: message2 }) => error51("model.schema", path16.length === 0 ? "the file" : path16.map(String).join("."), message2));
39709
39722
  }
39710
39723
  var RETIRED_MODEL_KEYS = {
39711
39724
  field_roles: "`records` replaced it \u2014 state how each entity's row is recognised (title, subtitle, image, status, figure, due) once per entity; docs/app_system.md",
@@ -39775,13 +39788,13 @@ function validateModelRows(model, rows) {
39775
39788
  }
39776
39789
  seenRefs.add(row.ref);
39777
39790
  for (const [fieldAlias2, value] of Object.entries(row.fields)) {
39778
- const path15 = `rows.${alias2}.${row.ref}.${fieldAlias2}`;
39791
+ const path16 = `rows.${alias2}.${row.ref}.${fieldAlias2}`;
39779
39792
  const field = fieldByAlias.get(fieldAlias2);
39780
39793
  if (field === void 0) {
39781
- errors.push(error51("model.names-declared", path15, `is not a field of entity "${alias2}" \u2014 a row's keys are field aliases (${fieldByAlias.size > 0 ? [...fieldByAlias.keys()].join(", ") : "this entity declares none"})`));
39794
+ errors.push(error51("model.names-declared", path16, `is not a field of entity "${alias2}" \u2014 a row's keys are field aliases (${fieldByAlias.size > 0 ? [...fieldByAlias.keys()].join(", ") : "this entity declares none"})`));
39782
39795
  continue;
39783
39796
  }
39784
- checkValue(field, value, path15, refs, errors);
39797
+ checkValue(field, value, path16, refs, errors);
39785
39798
  }
39786
39799
  for (const field of entity.fields) {
39787
39800
  if (field.required !== true || COMPUTED_FIELD_TYPES2.has(field.type)) continue;
@@ -39793,13 +39806,13 @@ function validateModelRows(model, rows) {
39793
39806
  return errors;
39794
39807
  }
39795
39808
  var COMPUTED_FIELD_TYPES2 = /* @__PURE__ */ new Set(["formula", "rollup", "lookup", "autonumber"]);
39796
- function checkValue(field, value, path15, refs, errors) {
39809
+ function checkValue(field, value, path16, refs, errors) {
39797
39810
  switch (field.type) {
39798
39811
  case "select": {
39799
39812
  const declared = new Set(field.options.map((option) => option.alias));
39800
39813
  for (const raw of asList(value)) {
39801
39814
  if (typeof raw !== "string" || !declared.has(raw)) {
39802
- errors.push(error51("rows.value", path15, `${quote(raw)} is not an option of this field \u2014 use an option alias (${declared.size > 0 ? [...declared].join(", ") : "this field declares none"})`));
39815
+ errors.push(error51("rows.value", path16, `${quote(raw)} is not an option of this field \u2014 use an option alias (${declared.size > 0 ? [...declared].join(", ") : "this field declares none"})`));
39803
39816
  }
39804
39817
  }
39805
39818
  return;
@@ -39807,30 +39820,30 @@ function checkValue(field, value, path15, refs, errors) {
39807
39820
  case "select_record_link": {
39808
39821
  for (const raw of asList(value)) {
39809
39822
  if (typeof raw !== "string" || !refs.has(raw)) {
39810
- errors.push(error51("rows.value", path15, `${quote(raw)} names no row in this request \u2014 use "<entity-alias>:<ref>"`));
39823
+ errors.push(error51("rows.value", path16, `${quote(raw)} names no row in this request \u2014 use "<entity-alias>:<ref>"`));
39811
39824
  continue;
39812
39825
  }
39813
39826
  const named = raw.slice(0, raw.indexOf(":"));
39814
39827
  if (named === field.target_entity) continue;
39815
- errors.push(error51("rows.value", path15, `names a row of entity "${named}", but this field links to "${field.target_entity}"`));
39828
+ errors.push(error51("rows.value", path16, `names a row of entity "${named}", but this field links to "${field.target_entity}"`));
39816
39829
  }
39817
39830
  return;
39818
39831
  }
39819
39832
  case "select_member": {
39820
39833
  if (value !== "self") {
39821
- errors.push(error51("rows.value", path15, `${quote(value)} is not a member this model can name \u2014 use "self"`));
39834
+ errors.push(error51("rows.value", path16, `${quote(value)} is not a member this model can name \u2014 use "self"`));
39822
39835
  }
39823
39836
  return;
39824
39837
  }
39825
39838
  case "files": {
39826
39839
  for (const raw of asList(value)) {
39827
39840
  if (typeof raw !== "string" || raw === "") {
39828
- errors.push(error51("rows.value", path15, `${quote(raw)} is not a document \u2014 a path beside this file, or a fil_ id already uploaded here`));
39841
+ errors.push(error51("rows.value", path16, `${quote(raw)} is not a document \u2014 a path beside this file, or a fil_ id already uploaded here`));
39829
39842
  continue;
39830
39843
  }
39831
39844
  if (isStoredFileId(raw)) continue;
39832
39845
  if (raw.startsWith("/") || raw.split(/[\\/]/).includes("..")) {
39833
- errors.push(error51("rows.value", path15, `"${raw}" must be a relative path beside this file, with no ".."`));
39846
+ errors.push(error51("rows.value", path16, `"${raw}" must be a relative path beside this file, with no ".."`));
39834
39847
  }
39835
39848
  }
39836
39849
  return;
@@ -39839,23 +39852,23 @@ function checkValue(field, value, path15, refs, errors) {
39839
39852
  case "rollup":
39840
39853
  case "lookup":
39841
39854
  case "autonumber": {
39842
- errors.push(error51("rows.value", path15, `is a ${field.type} field, which the platform computes \u2014 drop the value`));
39855
+ errors.push(error51("rows.value", path16, `is a ${field.type} field, which the platform computes \u2014 drop the value`));
39843
39856
  return;
39844
39857
  }
39845
39858
  default: {
39846
39859
  if (!isFixtureScalarValue(value)) {
39847
- errors.push(error51("rows.value", path15, `must be a string, number, boolean or null on a ${field.type} field`));
39860
+ errors.push(error51("rows.value", path16, `must be a string, number, boolean or null on a ${field.type} field`));
39848
39861
  return;
39849
39862
  }
39850
39863
  const expression = field.type === "date" ? parseFixtureDateExpression(value) : void 0;
39851
39864
  if (expression?.kind === "invalid") {
39852
- errors.push(error51("rows.value", path15, `${quote(value)} is not a date \u2014 use ${FIXTURE_DATE_GRAMMAR}`));
39865
+ errors.push(error51("rows.value", path16, `${quote(value)} is not a date \u2014 use ${FIXTURE_DATE_GRAMMAR}`));
39853
39866
  } else if (expression?.kind === "relative" && field.type === "date") {
39854
39867
  const clock = field.format === "datetime";
39855
39868
  if (clock && expression.time === void 0) {
39856
- errors.push(error51("rows.value", path15, `${quote(value)} places a date that holds its hour at no hour \u2014 state it after the day: "${String(value)} 09:00"`));
39869
+ errors.push(error51("rows.value", path16, `${quote(value)} places a date that holds its hour at no hour \u2014 state it after the day: "${String(value)} 09:00"`));
39857
39870
  } else if (!clock && expression.time !== void 0) {
39858
- errors.push(error51("rows.value", path15, `${quote(value)} states an hour on a date that holds none \u2014 drop the hour`));
39871
+ errors.push(error51("rows.value", path16, `${quote(value)} states an hour on a date that holds none \u2014 drop the hour`));
39859
39872
  }
39860
39873
  }
39861
39874
  }
@@ -40128,17 +40141,17 @@ function validateWorkspaceModel(model, rows) {
40128
40141
  errors.push(...checkModelSingulars(contract.entities, apps ?? []));
40129
40142
  return [...contractErrors, ...errors];
40130
40143
  }
40131
- function unknownModelKeys(raw, kept, path15 = "") {
40144
+ function unknownModelKeys(raw, kept, path16 = "") {
40132
40145
  if (Array.isArray(raw)) {
40133
40146
  if (!Array.isArray(kept)) return [];
40134
- return raw.flatMap((item, i) => unknownModelKeys(item, kept[i], path15 === "" ? String(i) : `${path15}.${i}`));
40147
+ return raw.flatMap((item, i) => unknownModelKeys(item, kept[i], path16 === "" ? String(i) : `${path16}.${i}`));
40135
40148
  }
40136
40149
  if (!isPlainObject2(raw) || !isPlainObject2(kept)) return [];
40137
40150
  if (isBareFilterNode(raw) && kept.node_type === "group" && Array.isArray(kept.children) && kept.children.length === 1) {
40138
- return unknownModelKeys(raw, kept.children[0], path15);
40151
+ return unknownModelKeys(raw, kept.children[0], path16);
40139
40152
  }
40140
40153
  return Object.keys(raw).flatMap((key) => {
40141
- const here = path15 === "" ? key : `${path15}.${key}`;
40154
+ const here = path16 === "" ? key : `${path16}.${key}`;
40142
40155
  if (!(key in kept)) return [error51("model.schema", here, "unknown key \u2014 not part of the model")];
40143
40156
  return unknownModelKeys(raw[key], kept[key], here);
40144
40157
  });
@@ -40251,7 +40264,7 @@ function resultSideEffects(result) {
40251
40264
  }
40252
40265
 
40253
40266
  // src/version.ts
40254
- var VERSION = "0.280.0";
40267
+ var VERSION = "0.281.0";
40255
40268
  var APP_SDK_VERSION = "0.108.0";
40256
40269
 
40257
40270
  // src/timezone.ts
@@ -40277,7 +40290,7 @@ var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent nee
40277
40290
  var building_an_app_default = '# Building an app, end to end\n\nThe other references here describe **contracts** \u2014 what a model may state, what a tool takes. This\none describes the **sequence**: the order the steps go in, and why. Read it once for the shape, then\nreach for the area doc (`lotics docs`) whenever you need the detail.\n\n**The live app is the only edit surface.** Every change to an app \u2014 applying a model, deploying a\nbuild, setting one query or workflow \u2014 mints a new version of it, and rolling back to an earlier\nversion is the undo. Nothing about an app lives in a local directory the platform reads back, so\nthere is no project to keep in sync and no deploy to find out whether something works.\n\n**Rolling back restores the app, not the data.** A table change the model made, and every row a\nworkflow wrote while you tried it, stay where they are. Try a write on a throwaway record.\n\n---\n\n## 1 \u2014 Two kinds of app\n\n- **An app stated in a model** \u2014 the default, and the right one for almost every job. `model.json`\n says how a row of each entity is recognised (`records`) and, in `apps`, one register over one\n entity and the record its rows open: its columns and filters, its sections in the order its work\n reaches them, its acts and the checks that guard them (`lotics docs model/apps`). The platform\n compiles each app and draws it with the runtime every such app shares, so how each piece looks is\n the platform\'s, one way per concept, and no key changes it. Every write the app makes is a\n generated workflow that re-checks on the server what the model states.\n- **A custom-code app** \u2014 React you write, for a surface the model\'s vocabulary cannot state. It\n reads and writes through `@lotics/app-sdk` (queries, workflows, files, AI) and draws with whatever\n React you choose.\n\n## 2 \u2014 Clarify what is being asked, before modelling it\n\n**A metric name is not a definition.** "Revenue", "in stock", "active", "overdue" \u2014 each is a\nbusiness rule the person asking owns, and the cost of guessing is a screen that is confidently\nwrong. Ask until there is no ambiguity left:\n\n- Which rows count, keyed off which field \u2014 a date, a status, a flag?\n- Does the same metric need a **different rule per table**? One table may key off a date and\n another off a status; one rule rarely covers both.\n- Snapshot or flow? "Current stock" is as-of-now; "revenue this month" is a window. They compile\n to different filters.\n- If the data cannot support the definition asked for \u2014 the field simply is not there \u2014 **say so\n and show the options.** Silently substituting a near-miss produces a number nobody can trace.\n\nThis is the step that gets skipped under time pressure, and it is the only one whose mistakes are\ninvisible in review: every later artifact is correct with respect to the wrong definition.\n\n## 3 \u2014 The data model, before any app\n\nGet this wrong and nothing above it can be precise. Each entity is its own table with links into\nthe spine; attributes and evidence are fields on their owner. A single table with a `type` column\nstanding in for three entities collapses the distinctions every later query needs.\n\n**Verify real VALUES, never just that a field exists.** `lotics run query_records` a sample and\nlook at fill rates \u2014 a field that is present and empty on 90% of rows will not support the screen\nyou are about to design.\n\n**That includes imagery.** If the entity has a likeness \u2014 a product, a property, a vehicle, a\nperson \u2014 its picture is the strongest identifier a register row can carry, and an empty image field\nis a data gap to fill before you design around it: `lotics file upload`, then `update_records`.\n\n**One fact, one column \u2014 and check before you add one.** Read the table\'s existing fields before\nadding any, because the fact is often already there in another shape: a place written as text\nbeside a link to the place record, a status word beside the select that decides it, a total beside\nthe formula that computes it. Two columns for one fact never stay equal. Prefer the link, the\nselect, or the formula, and compose the text when you READ.\n\n**Match on ids and option keys, never on rendered text.** Resolve a name to its `rec_\u2026` or `opt_\u2026`\nonce, at the boundary, and compare those. Treat an unresolved name as UNKNOWN, never as a wildcard.\n\n**How the tables RELATE is `lotics docs data_model`** \u2014 read it before designing a schema; those\ndecisions outlive any one app.\n\n**Empty is not the same as redundant.** A field nothing fills may still be the only home for a real\ndistinction. Read what a field MEANS before you remove it.\n\n## 4 \u2014 An app stated in a model\n\n```\nlotics docs model # how to write model.json, with a worked example\nlotics model apply model.json # check it, apply the tables, mint a version of every app\nlotics model apply model.json --app orders # only the apps named; the tables are applied whole\nlotics model apply model.json --plan # what the apply would change, writing nothing\nlotics model pull -o model.json # the workspace\'s model, as the file apply reads\n```\n\n`model apply` checks the whole file first \u2014 every problem in one run, before anything is uploaded\nor written. It then adopts or creates each table (the table this workspace bound it to, else one of the same\nlabel, is adopted and given what it lacks; no stored value changes), writes the first rows only where every bound\ntable is empty, and mints one version per app, printing each app\'s id, the version minted\n(`unchanged` when there was nothing to mint) and its address. Applying the same file again mints nothing.\nA table change is not undone by rolling an app back, so `--plan` first says what the apply would\ncreate or change in the tables and which apps it would create, update or refuse \u2014 writing nothing.\n\n**The model changes after the app exists, and applying it again is how it lands.** Edit the file,\napply it. An act whose write the model cannot say names its own `workflow`; that workflow\'s body is\nthe live one, and `lotics run set_app_workflow` changes it. An act that reads papers runs an agent\napply owns: made, replaced and removed with the act, and an agent of that alias apply did not make\nis refused, never rewritten.\n\n## 5 \u2014 A custom-code app\n\n```\nlotics app create "<name>" --custom # the app, plus a Vite + React + TS project using @lotics/app-sdk\ncd <dir>\n# edit src/App.tsx \u2014 node_modules/@lotics/app-sdk/AGENTS.md is the reference\nnpm run typecheck && npm run lint && npm test\nlotics app deploy -m "<what changed>" # build, upload, a new version live\n```\n\nWhat the app reads and writes is bound on the app, never in the project: `lotics run\nset_app_query` binds a named query, `lotics run set_app_workflow` a workflow body, and each mints a\nversion. `useQuery("<alias>")` and `useWorkflow("<alias>")` call them. A deploy uploads the build\nand carries every binding forward unchanged.\n\n**Named queries.** Author them as `kind: "project"` with a `filter`, naming each projected column\n(`{ "source": "fld_\u2026", "output": "total" }`), so a row reads as `r.total`. Scope per-user reads with\n`is_current_member` **inside the query** \u2014 a member id passed from the client is chosen by the\ncaller. Write one `description` per alias: it is the line a chat or MCP caller chooses by.\n\n**Workflows are the only way an app writes.** Every workflow bound to an app is also the chat\nagent\'s write surface, so its shape is an agent-facing decision: take a list where one job covers\nmany records, say in an optional input\'s `description` what omitting it means, make the write\nsurvive running twice, and gate anything irreversible with `wait_for_approval`.\n\n## 6 \u2014 Prove it, without a screen\n\nStatic checks prove parse, types and names \u2014 they evaluate nothing. Rehearse a write first:\n\n```\nlotics run dry_run_workflow \'{"trigger_type":"app_workflow","trigger_payload":{\u2026},"live_reads":true}\'\n```\n\nIt walks the real step tree and returns the resolved plan plus expression and tool-input errors,\ndispatching no write. **`live_reads: true` matters whenever the body reads anything**: without it\nevery read returns a stub, and every data-gated branch takes the empty path.\n\nThen run it end to end, on a throwaway record:\n\n```\nlotics run run_app_query \'{"app_id":"app_\u2026","alias":"\u2026","params":{\u2026}}\'\nlotics run run_app_workflow \'{"app_id":"app_\u2026","alias":"\u2026","inputs":{\u2026}}\'\n# exits non-zero when the run failed, so it is assertable\n```\n\nA workflow is also how an app **produces a document** \u2014 the `generate_*_from_template` tools fill a\ntemplate you registered once (`lotics docs document_templates`).\n\n## 7 \u2014 Look at it, then name it\n\nLook at every app you apply, at a desktop width and at a phone\'s \u2014 every check above reads the\ndefinition, none of them the pixels:\n\n```\nlotics run screenshot_app \'{"app_id":"app_\u2026"}\'\n# one PNG per width, saved as a workspace file; "path":"/rec_\u2026" opens a screen inside the app\nlotics download <file_id>\n```\n\nEach shot is one screen, as a member sees it; `"full_page":true` grows it to the list scrolling\ninside the app. Beside each image comes its `snapshot`, the screen\'s accessibility tree as text \u2014\nread it to check labels and values without opening the picture.\n\nThe app is drawn live, as you, read-only: a screen that writes when it opens shows that write\nrefused, and `errors` lists what failed on the page. A version that looks wrong is one\n`lotics run rollback_app` away from the one before.\n\nThen set the icon, the colour and the app\'s own `description` through `lotics run update_app`. The\n`description` heads the capability listing the member\'s chat agent reads on **every** turn, so a\nstanding process the app expects that agent to carry out belongs there and nowhere else.\n\n**A caller outside the team gets its own app.** Sharing an app publicly, or giving an API key\naccess to it, reaches every alias the app declares; no alias can be held back. So whatever\noutsiders may call is a SECOND app over the same tables: only the queries they may read and the\nworkflows they may run. The desk the team works in stays a separate, private app.\n';
40278
40291
 
40279
40292
  // docs/cli_reference.md
40280
- var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. Start at [AGENTS.md](../AGENTS.md) for the model this reference assumes; `lotics --help` is the authoritative, always-current verb list.\n\n| Command | What it does |\n|---|---|\n| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (\xA7 COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics model --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |\n| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open \u2014 `https://lotics.ai/cli_login/<request_id>`, also mailed \u2014 and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential \u2014 `docs` among them \u2014 claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |\n| `lotics auth api-key [key]` | `whoami` \u2192 **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth web` | Send a magic link email to access the web app (requires auth) |\n| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key \u2014 read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |\n| `lotics auth logout [<name\\|id>]` | In a pinned dir: delete the local pin. Else: remove the profile (default the active org), `--all` for every one. What happens server-side depends on which KIND of credential it is. A **sign-in** (`auth login` / `auth signup`) is revoked \u2014 logging that terminal out ends its credential rather than leaving a live one behind; a server that cannot be reached, or a credential already dead, never blocks the local forget, and one line names the org and Settings \u2192 Security \u2192 *Keys and terminals*. An **API key** (`auth api-key`) is only forgotten here \u2014 an admin issued it and it is routinely on a server and on other machines, so one terminal signing out must not kill it for everyone; the line says it is still active and names both pages, because Settings \u2192 API keys is admin-only and the credential may well be the holder's own sign-in, which they revoke themselves at Settings \u2192 Security \u2192 *Keys and terminals*. A profile saved before the kind was recorded states nothing, so the SERVER is asked (`auth whoami`) and it is revoked only if the answer is a sign-in: an older server, a credential minted before the column, and a request that fails all leave it alone. |\n| \u2014 | **A refused credential says which of three ways it is dead, and names the remedy that ends its kind.** `This credential expired.` / `was revoked.` / `belongs to a member who is no longer active in this organization.` carries `Run \\`lotics auth login <email>\\` to sign in again.` for a sign-in and `Ask an admin for a new API key (Settings \u2192 API keys).` for an issued key. A credential minted before that was recorded still gets BOTH in one sentence, because nothing on the row tells them apart \u2014 so a headless box is never sent looking for a browser alone. A key the server does not recognize at all gets one flat `Invalid or disabled API key.` \u2014 deliberately, so a guessed key learns nothing, not even that it named a row. The body carries `reason` for a script to branch on, since the code stays `unauthorized` for every 401. |\n| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |\n| `LOTICS_ORG=<name\\|id>` | Scope every command in this shell to one saved org. **Resolved once, before any command dispatches**, so a value matching no saved credential refuses every verb with one sentence \u2014 a read, a write, and a local check that needs no credential alike \u2014 and refuses it before the first byte is written. It refuses even when a credential arrives another way, because `--api-key` / `LOTICS_API_KEY` outrank it in the precedence chain and a write must never fall through to whatever THOSE name while the variable says otherwise; when the variable resolves and a key is also given, the key decides and the command says so. The refusal lists the orgs this machine holds, so it is answerable without another command (`lotics org` is refused by the same rule). A name is whatever the credential was SAVED under \u2014 a server-side rename never moves it, and the new name resolves too, so both keep working and `lotics org` prints the pair. |\n| `lotics org use <name\\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous \u2192 error) or id. No flag \u2192 global `active_org`; `--local` \u2192 a `.lotics/config.json` pointer in the current dir. |\n| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |\n| `lotics workspace select <id>` | Set the workspace in the **active scope** \u2014 a local pin if the dir has one, else the active org's global profile. Records the workspace's NAME beside its id, which is what the `lotics \u2192 <org> / <workspace>` echo prints; `workspace list`, `workspace create`, `workspace rename` and `org use` record it too, so a target is named rather than identified. Until one command has listed it, the echo prints the id and says the name is not known yet. |\n| `lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]` | Create a new workspace (admin only), auto-switches to it. Neither flag is defaulted from THIS machine, unlike signup: an extra workspace is routinely created by an operator for somebody else. Without `--timezone` the new workspace inherits the zone of the org's OLDEST workspace; without `--currency` it takes the org's default. Both ride the create, so the workspace is never briefly denominated in a currency nobody asked for. `--currency` takes an ISO-4217 code (case-insensitive; anything else is refused). |\n| `lotics workspace rename <name>` | Rename the **current** workspace (admin only) \u2014 the endpoint takes its target from the request's workspace, never a path id, so switch with `workspace select <id>` first and read the `lotics \u2192 <org> / <workspace>` echo before trusting it \u2014 both halves are names, and the rename moves the cached one in the same act. Carries the workspace's existing `default_currency` and `timezone` through unchanged: the endpoint takes the whole settings triple, so sending only a name would blank the other two. |\n| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone \u2014 `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |\n| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** \u2014 `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too \u2014 every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |\n| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` \u2014 every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> \"<name>\" (<id>) \u2192 <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |\n| `lotics tools` | List tools by category with descriptions |\n| `lotics tools <name>` | Full description + JSON Schema for one tool |\n| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \\| lotics run <tool> -`) \u2014 both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone \u2014 the error names both escapes. |\n| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |\n| `lotics run <tool>` \u2014 **file cells** | A file in a tool's result carries its `fil_\u2026` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file \u2014 this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |\n| \u2014 | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_query`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model pull`, `app create --custom`, `app deploy`. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> \u2192 <status>: <message>` to stderr, so `lotics run \u2026 && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE \u2014 an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |\n| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |\n| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes \u2014 harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |\n| `lotics file upload <file\\|dir...>` (alias `lotics upload`) \xB7 `--stdin` \xB7 `--base64` \xB7 `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files` in one request, and several such files go in the same one; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose \u2014 one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** \u2014 an attachment decoded in memory, a generated document, a signed download link \u2014 each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY \u2014 `Buffer.from(s, \"base64\")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |\n| `lotics file download <file_id> [<path>]` \xB7 `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` \u2192 fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there \u2014 the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once \u2014 a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_key> [-o <dir>]` spreads every file on a record's file field over a DIRECTORY \u2014 there is no single file for N files to be. |\n| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first \u2014 id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset \u2014 pass it back as given \u2014 so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to \"what is in here\" short of reading Postgres. |\n| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. **Refused while a record cell, a comment, a knowledge doc, a document template or a voice session still references it** \u2014 the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what \"deleted\" means here. There is no `lotics delete`: the verb needs its noun. |\n| `lotics knowledge list [--include-hidden]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not the `list_knowledge` tool**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. |\n| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \\| --content <str>)` | Read the body client-side (a file XOR an inline string \u2014 exactly one required), then call `create_knowledge` with `{ name, description, content, tags? }` (description defaults to `\"\"`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |\n| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) \u2192 the doc with its **hydrated `content`** (the one content-read path for a non-sandbox client). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |\n| `lotics knowledge update <id> [--from <file.md> \\| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set \u2014 the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |\n| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` \u2014 one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |\n| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** \u2014 the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches \u2014 while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as \"everything\". |\n| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes \u2014 `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** \u2014 it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as \u2014 that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created, because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name \u2014 the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning \u2014 `setup` writes nothing to disk \u2014 so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** \u2014 what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup \u2026`. |\n| `lotics model apply <model.json> [--app <alias> ...] [--plan] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked with the validator the server runs, every problem in one run, before anything is uploaded. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares \u2014 `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app \u2014 alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at \u2014 then the model's notes once, as the server states them. **`--plan` writes nothing and uploads nothing**: it prints what the apply would do to the tables (what it would create, what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, and what it would refuse an app for, exiting 1 when it would refuse one; workflow bodies are checked only at apply, since they name fields a plan has not created. **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout (with `--plan`, each table and app is what the apply would do), or `{ok: false, findings}` when the file does not check. |\n| `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** \u2014 the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version \u2014 through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. |\n| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty \u2014 before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`get_app_types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged \u2014 those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live; to ship this directory over it, set `current_version_id` to that version and deploy again. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |\n| `lotics docs` \\| `lotics docs <area>[/<section>]` | **This CLI's own references, carried inside the binary** \u2014 the model reference, this index, and every doc under `docs/` \u2014 so the doc a reader opens always describes the binary answering. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |\n| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script \u2014 the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe \u2014 a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |\n| `lotics docs model` \\| `lotics docs model/<section>[/\u2026]` | **The model reference, from inside the binary** \u2014 the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs \u2192 entities and fields \u2192 `records` \u2192 one app per job \u2192 `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<entity-alias>:<ref>\"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |\n| `lotics report '<json>'` \\| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` \u2014 invoking it IS the consent that passive collection needs an opt-in for \u2014 but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** \u2014 a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |\n\n";
40293
+ var cli_reference_default = "# @lotics/cli \u2014 CLI Command Reference\n\nPer-command syntax, flags, contracts, and gotchas for the public `lotics` CLI. Start at [AGENTS.md](../AGENTS.md) for the model this reference assumes; `lotics --help` is the authoritative, always-current verb list.\n\n| Command | What it does |\n|---|---|\n| `lotics` / `lotics --help` | Show full help: capabilities, the verb list (\xA7 COMMANDS), flags, config. `lotics <verb> --help` prints that verb's entries alone (`lotics model --help`, `lotics file download --help`); `lotics report --help` prints the report frame. |\n| `lotics auth signup <email>` | Create account + org + API key, sends magic link email. Registers the new org as a profile; `--local` pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth login <email>` | Sign in an account that already exists, on a machine holding no key. **Two steps, and it does not wait for the person.** The first prints the page to open \u2014 `https://lotics.ai/cli_login/<request_id>`, also mailed \u2014 and the code that page must show, records the request, and exits 0. They sign in there if asked, check the code and press Confirm. **Then the next command that needs a credential collects the key** before it does its own work, so the second step is just re-running whatever was wanted; a command run before Confirm exits 1 naming the page and the code again, and once the 15 minutes are up it says to ask again. The handful that run WITHOUT a credential \u2014 `docs` among them \u2014 claim nothing, so one of those run after Confirm still answers as though signed out. `--wait` keeps one command instead, holding the terminal until Confirm; `--local` pins this directory to that org rather than setting the global default, and implies `--wait` (a pin names THIS directory, so only the terminal that stays in it can write one). `--json` prints `organization_id`, `workspace_id` and `organization_name` when it finishes signed in, and `request_id`, `confirm_url`, `code`, `email`, `expires_at` when it is the first step. The request's secret is never printed and the org's key never leaves the store. |\n| `lotics auth api-key [key]` | `whoami` \u2192 **upsert** the key's org as a profile in the global store (never overwrites). The profile records the instance the key was verified against (`LOTICS_API_URL`, default `https://api.lotics.ai`), and every later command for that org goes there. `--local` additionally pins this directory to it (pointer) instead of setting the global default. |\n| `lotics auth web` | Send a magic link email to access the web app (requires auth) |\n| `lotics auth whoami` | Print active account name, email, org, resolved workspace, the instance the credential belongs to, which **kind** of credential this machine holds (a sign-in from `auth login`, or an API key \u2014 read from the saved profile, and from the server when the profile does not say, which covers `--api-key`/`LOTICS_API_KEY` and a profile saved before the field existed; unknown only when neither can say), and the resolution **source** (flag/env/local/app-manifest/global). `--json` adds `workspace_id`, `api_url`, `credential_kind` + `source`. |\n| `lotics auth logout [<name\\|id>]` | In a pinned dir: delete the local pin. Else: remove the profile (default the active org), `--all` for every one. What happens server-side depends on which KIND of credential it is. A **sign-in** (`auth login` / `auth signup`) is revoked \u2014 logging that terminal out ends its credential rather than leaving a live one behind; a server that cannot be reached, or a credential already dead, never blocks the local forget, and one line names the org and Settings \u2192 Security \u2192 *Keys and terminals*. An **API key** (`auth api-key`) is only forgotten here \u2014 an admin issued it and it is routinely on a server and on other machines, so one terminal signing out must not kill it for everyone; the line says it is still active and names both pages, because Settings \u2192 API keys is admin-only and the credential may well be the holder's own sign-in, which they revoke themselves at Settings \u2192 Security \u2192 *Keys and terminals*. A profile saved before the kind was recorded states nothing, so the SERVER is asked (`auth whoami`) and it is revoked only if the answer is a sign-in: an older server, a credential minted before the column, and a request that fails all leave it alone. |\n| \u2014 | **A refused credential says which of three ways it is dead, and names the remedy that ends its kind.** `This credential expired.` / `was revoked.` / `belongs to a member who is no longer active in this organization.` carries `Run \\`lotics auth login <email>\\` to sign in again.` for a sign-in and `Ask an admin for a new API key (Settings \u2192 API keys).` for an issued key. A credential minted before that was recorded still gets BOTH in one sentence, because nothing on the row tells them apart \u2014 so a headless box is never sent looking for a browser alone. A key the server does not recognize at all gets one flat `Invalid or disabled API key.` \u2014 deliberately, so a guessed key learns nothing, not even that it named a row. The body carries `reason` for a script to branch on, since the code stays `unauthorized` for every 401. |\n| `lotics org` | List saved orgs (profiles) from the global store with the instance each belongs to, marks active for this directory (a local pin wins over the global default). |\n| `LOTICS_ORG=<name\\|id>` | Scope every command in this shell to one saved org. **Resolved once, before any command dispatches**, so a value matching no saved credential refuses every verb with one sentence \u2014 a read, a write, and a local check that needs no credential alike \u2014 and refuses it before the first byte is written. It refuses even when a credential arrives another way, because `--api-key` / `LOTICS_API_KEY` outrank it in the precedence chain and a write must never fall through to whatever THOSE name while the variable says otherwise; when the variable resolves and a key is also given, the key decides and the command says so. The refusal lists the orgs this machine holds, so it is answerable without another command (`lotics org` is refused by the same rule). A name is whatever the credential was SAVED under \u2014 a server-side rename never moves it, and the new name resolves too, so both keep working and `lotics org` prints the pair. |\n| `lotics org use <name\\|id> [--local]` | Switch the active org by org name (case-insensitive, ambiguous \u2192 error) or id. No flag \u2192 global `active_org`; `--local` \u2192 a `.lotics/config.json` pointer in the current dir. |\n| `lotics workspace` | List workspaces in the active org, marks current with `(current)` |\n| `lotics workspace select <id>` | Set the workspace in the **active scope** \u2014 a local pin if the dir has one, else the active org's global profile. Records the workspace's NAME beside its id, which is what the `lotics \u2192 <org> / <workspace>` echo prints; `workspace list`, `workspace create`, `workspace rename` and `org use` record it too, so a target is named rather than identified. Until one command has listed it, the echo prints the id and says the name is not known yet. |\n| `lotics workspace create <name> [--timezone <Area/City>] [--currency <ISO>]` | Create a new workspace (admin only), auto-switches to it. Neither flag is defaulted from THIS machine, unlike signup: an extra workspace is routinely created by an operator for somebody else. Without `--timezone` the new workspace inherits the zone of the org's OLDEST workspace; without `--currency` it takes the org's default. Both ride the create, so the workspace is never briefly denominated in a currency nobody asked for. `--currency` takes an ISO-4217 code (case-insensitive; anything else is refused). |\n| `lotics workspace rename <name>` | Rename the **current** workspace (admin only) \u2014 the endpoint takes its target from the request's workspace, never a path id, so switch with `workspace select <id>` first and read the `lotics \u2192 <org> / <workspace>` echo before trusting it \u2014 both halves are names, and the rename moves the cached one in the same act. Carries the workspace's existing `default_currency` and `timezone` through unchanged: the endpoint takes the whole settings triple, so sending only a name would blank the other two. |\n| `lotics workspace settings [--name <n>] [--currency <ISO>] [--timezone <Area/City>]` | Change the CURRENT workspace's name, default currency or timezone \u2014 `PATCH /v1/workspace`, admin only. Only what you name changes; the endpoint takes the whole triple, so the CLI carries the two you did not. `rename` is this verb with the name alone, which is why it can never forget the other two. Both values are invisible once they are wrong: the currency decides how every money field RENDERS and the zone decides how every date BUCKETS, on a workspace whose whole purpose may be to look like the customer's own. `--json` prints the updated workspace. |\n| `lotics workspace delete <id> --yes` | Delete a workspace by id (admin only). **Soft delete** \u2014 `archived_at` is set, so it drops out of listings, can no longer be selected, and its tables/records go dark, while the data is retained and recoverable. Its **apps are cascade-archived** too \u2014 every app entry point (embedded, public link, standalone subdomain, incl. anonymous public links) stops serving. Refuses the org's **only** active workspace (400) and any workspace outside the caller's org (404). Requires `--yes` to confirm (destructive; the CLI is used non-interactively). |\n| `lotics workspace doctor` | Report workspace-wide dangling schema references via `GET /v1/workspaces/dangling-references` \u2014 every active app/workflow artifact whose prefixed schema id no longer resolves, printed as `<referent.kind> \"<name>\" (<id>) \u2192 <namespace> <id> (missing)`; healthy prints a one-line all-clear. **Exits non-zero (exit 1) on findings** so scripts can gate on it. Resolves the first workspace like every data command (runs before the global workspace resolution). Admin-only. |\n| `lotics tools` | List tools by category with descriptions |\n| `lotics tools <name>` | Full description + JSON Schema for one tool |\n| `lotics run <tool> '<json>'` | Execute a tool (text output via toModelOutput). Args may also come from a file (`lotics run <tool> @args.json`) or stdin behind the `-` sentinel (`cat args.json \\| lotics run <tool> -`) \u2014 both bypass the OS `ARG_MAX` limit for large payloads (a knowledge-doc `content`, a bulk update). A leading `@` on the args is unambiguously a file path (JSON args start with `{`). **Stdin is asked for, never guessed.** `lotics run <tool>` with no payload runs the tool with no arguments and returns at once. `lotics report` takes the same sentinel. In PowerShell use `@file`: quotes inside an inline argument are consumed by the shell, and the CLI reports the JSON it received with its quotes gone \u2014 the error names both escapes. |\n| `lotics run <tool> --json '<json>'` | Execute a tool (full JSON output) |\n| `lotics run <tool>` \u2014 **file cells** | A file in a tool's result carries its `fil_\u2026` id and metadata and **no `url`**, on every tool and in both output modes. That is not a broken file \u2014 this surface resolves no URL for a cell. Reach the bytes with `lotics file download <file_id>`, which takes the id straight from the cell; the text output says so whenever a result carries one. |\n| \u2014 | **Every tool is invoked here, including the ones that RUN something** (`run_app_workflow`, `run_app_agent`, `run_app_query`) and every one that changes an app (`set_app_query`, `set_app_workflow`, `set_app_agent`, `update_app`, `rollback_app`). A command exists only for work that touches a local file: `model apply`, `model pull`, `app create --custom`, `app pull`, `app deploy`. |\n| \u2014 | **The exit code reports the WORK, not just the call \u2014 for the two tools that RUN one.** `run_app_workflow` and `run_app_agent` whose envelope carries a failed `status` (`error`/`failed`/`cancelled`) exit non-zero and print `<tool> \u2192 <status>: <message>` to stderr, so `lotics run \u2026 && next-step` cannot walk past a refused run. The rule is an allowlist of FAILURE \u2014 an unrecognized status exits 0, so a status added later never turns a working script red. A parked run (`awaiting_input`) is not a failure: it is waiting for an answer and the work is still live. Only a TOP-LEVEL `status` counts; one inside the data belongs to the data. Any OTHER tool's `status` is data, and exits 0. |\n| `lotics run <tool> --print-created` | Report the records the call created, grouped by table, with a paste-ready `delete_records` per table and the mandatory caveat naming what cannot be auto-undone (external integrations, notifications, possible sub-workflows). Works for any tool that returns a `side_effects` block, not workflows alone. |\n| `lotics run <tool> --cleanup` | Implies `--print-created`, then runs those deletes \u2014 harvested records **only**, never files / external calls / notifications. **Not a rollback**; a rollback is structurally impossible here. A partial cleanup exits non-zero so a script cannot read it as success. |\n| `lotics file upload <file\\|dir...>` (alias `lotics upload`) \xB7 `--stdin` \xB7 `--base64` \xB7 `--url <url>` | Upload files/directories. **The transport is chosen by size and is not a flag**: under 8 MiB the file is POSTed to `/v1/files` in one request, and several such files go in the same one; at or above it the CLI takes presigned part URLs and PUTs the bytes straight to object storage, so they never pass through the API. That threshold matches the AWS CLI's own `multipart_threshold`, and the number matters less than there being nothing to choose \u2014 one verb, any size, up to the 2 GiB a workspace may store. A large upload reads one part at a time, so memory stays flat regardless of file size, and a failure part-way abandons the parts already sent rather than leaving them billable and invisible. A directory expands to its immediate files; `--as <name>` renames a single upload. **Three alternative byte sources, for a caller that never had the bytes on disk** \u2014 an attachment decoded in memory, a generated document, a signed download link \u2014 each mutually exclusive with the others and with a path argument: `--stdin` takes raw bytes on stdin, `--base64` takes base64 on stdin (the shape attachments arrive in), `--url <url>` fetches the URL first. `--stdin`/`--base64` REQUIRE `--as`, because stdin carries no filename and the mime type is derived from it; `--url` falls back to `Content-Disposition` then the URL's last path segment. `--base64` decodes STRICTLY \u2014 `Buffer.from(s, \"base64\")` silently skips invalid characters and truncates on bad padding, so a corrupted pipe would otherwise store a short file that only fails when a human opens it. The `--url` fetch happens in the CLI, not the server: the URL comes from the operator running the command, so routing it through the backend would add an SSRF surface to buy what `curl` already does. |\n| `lotics file download <file_id> [<path>]` \xB7 `-o <dir>` | (alias `lotics download`) Download a stored file: `GET /v1/files/{id}/signed_url` \u2192 fetch the presigned URL and write it where you asked. **The two spellings mean two different things, and neither is read by shape: the positional `<path>` is the FILE to write, `-o <dir>` is the DIRECTORY to save into.** That is `cp` and `curl -o`, so nothing here consults an extension. A named file is written as named, its parent created, overwriting what is there \u2014 the point of naming it is that the next command opens that exact path. A directory is created if missing and written into under the stored filename (the response's `Content-Disposition`), taking a free spelling beside a file of that name already there so a repeat download never clobbers the first; with no destination at all, that filename lands in cwd. Give the destination once \u2014 a positional and `-o` together is refused, as is a positional that names an existing directory or ends in a separator (`a directory goes in -o`). The first argument is a **file id**, so a path in that slot is refused rather than sent as an id. The written path goes to **stdout** (under `--json`, `{file_id, path, filename, stored_filename}`) and the narration to stderr, so a download pipes into whatever opens it. `lotics file download record <record_id> <field_key> [-o <dir>]` spreads every file on a record's file field over a DIRECTORY \u2014 there is no single file for N files to be. |\n| `lotics file list [--limit <n>] [--cursor <token>]` | The workspace's files, newest first \u2014 id, upload time, bytes, MIME type, filename on stdout, one per line (`--json` for the object). `GET /v1/files` with no `file_ids`. A file holding the content of a knowledge doc or template you cannot use is left out. **A page, not a dump**: the store only ever grows, so the last line prints the command for the next page and `next_cursor` is null on the last one. The cursor is opaque and keyset \u2014 pass it back as given \u2014 so an upload landing mid-sweep cannot make a walk skip or repeat a row. Every other file verb takes an id, so this is the only answer to \"what is in here\" short of reading Postgres. |\n| `lotics file delete <file_id>` | Archive a stored file, over the `delete_file` tool. **Refused while a record cell, a comment, a knowledge doc, a document template or a voice session still references it** \u2014 the refusal names the referents, so this is safe to try. The bytes are left in object storage; the row no longer serves them, which is what \"deleted\" means here. There is no `lotics delete`: the verb needs its noun. |\n| `lotics knowledge list [--include-hidden]` | `GET /v1/knowledge_docs` \u2014 a table of id, name, tags, description (`--json` for the docs). **REST, not the `list_knowledge` tool**: the tool answers what the ASSISTANT may browse, and a hidden doc is out of that corpus by definition, so a tool-backed listing could never show one and the person who hid it would have no way back to it. Hidden docs are left out unless `--include-hidden` asks; those rows are marked `(hidden)`. |\n| `lotics knowledge create --name <n> [--description <d>] [--tags <a,b>] (--from <file.md> \\| --content <str>)` | Read the body client-side (a file XOR an inline string \u2014 exactly one required), then call `create_knowledge` with `{ name, description, content, tags? }` (description defaults to `\"\"`). `--tags` files the doc as it is made, which is the only moment a corpus reliably gets labelled. Prints the new id to stdout. Large files ride the POST body fine. |\n| `lotics knowledge get <id> [-o <file.md>]` | `GET /v1/knowledge_docs/{id}` (`getKnowledgeDoc`) \u2192 the doc with its **hydrated `content`** (the one content-read path for a non-sandbox client). `-o` writes the body via `writeFileAtomic`; else the body goes to stdout. `--json` prints the full doc instead. |\n| `lotics knowledge update <id> [--from <file.md> \\| --content <str>] [--name <n>] [--description <d>] [--tags <a,b>]` | Call `update_knowledge` with **only** the provided fields (a body from --from/--content becomes `content`; the tool diffs + CASes the content change internally, so the CLI passes no `expected_content_file_id`). `--tags` REPLACES the doc's label set \u2014 the single-doc form, where the caller is looking at one doc and can state what it should carry. At least one field required; --from and --content are mutually exclusive. |\n| `lotics knowledge tag <id...> [--add <a,b>] [--remove <c,d>]` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, add_tags?, remove_tags? }` \u2014 one transaction over the whole set. A **DIFF applied to each doc's own labels**, never a replacement: the docs named on one command line carry different labels, so one array across them would strip whatever the others were filed under. Removal matches case-insensitively; adding a label a doc already carries writes nothing. Ids may be separate arguments or comma-separated. At least one of --add/--remove required. |\n| `lotics knowledge hide <id...>` / `lotics knowledge unhide <id...>` | `PATCH /v1/knowledge_docs` with `{ knowledge_doc_ids, hidden }`. Hiding takes docs out of every **listing** \u2014 the Library's list, `list_knowledge`, and the corpus `grep_knowledge` searches \u2014 while leaving IAM untouched and keeping them readable **by id** (`read_knowledge` with an id, a code run staging one, an app agent's declared set). So it can never silently break an app that depends on a doc, and unhiding costs nothing. Refuses the no-argument form rather than reading it as \"everything\". |\n| `lotics knowledge rm <id>` | Archive the doc via `delete_knowledge` (`{ knowledge_doc_id }`). The REST execute path does not gate `needsApproval`, so this runs unattended. |\n| `lotics setup <model.json> [--email <addr>] [--json]` | **The whole first run, in one command.** Creates an account when this machine has no credential (the same call `auth signup` makes \u2014 `--name` and `--timezone` apply), then applies the model to its workspace, then prints the one-time sign-in link. **When that email already has an account it hands over to the `lotics auth login` flow** \u2014 it prints the sign-in page to open and the code it must show, and **exits 1 having created nothing**; the person presses Confirm and runs the same command again, which collects the key and carries on into the model. (`--wait` holds the terminal through the Confirm instead, finishing in one command.) The re-run is not refused for naming an `--email` it is now signed in as \u2014 that address IS the account it holds, not a second one. The file is a workspace MODEL, and it is read and checked before an account is created, because a file with a typo in it must not leave an organization behind. Then it is `lotics model apply` run on the new workspace: its tables, rows and apps; the sign-in link lands on its app when it has one, else on the workspace's app list. It exists because the two-command form has a seam where the FIRST command exists only to produce a credential for the second, and a caller pasting a prompt has to get both right. **`--email` is only for creating an account**: with a credential already resolvable it is REFUSED rather than obeyed, because the two can name different organizations and preferring either one silently writes into an org the caller did not name \u2014 the message says how to do each thing on purpose. Without it, `setup` applies the model to the account you already have. A path positional after the file is accepted and IGNORED with a warning \u2014 `setup` writes nothing to disk \u2014 so a prompt that passes one still runs. **`--json` prints one object on stdout and nothing else** \u2014 what `lotics model apply --json` does (`tables`; `apps`, each with `alias`, `app_id`, `version_id`, `origin`, `address` and `findings`; and `findings`) plus `organization_id`, `workspace_id` and `signin_url`, and a `warnings` array carrying everything the prose form would have said out of band, such as a sign-in link that could not be minted. A warning is never merely silenced: when the command fails with an error before it can emit, the ones it had collected go to stderr alongside it. Reachable with no install: `npx -y @lotics/cli setup \u2026`. |\n| `lotics model apply <model.json> [--app <alias> ...] [--plan] [--json]` | **The model, applied to this workspace, through the `apply_model` tool.** The file is read and checked with the validator the server runs, every problem in one run, before anything is uploaded. Documents a row attaches by a path beside the file are uploaded first and the rows sent with their `fil_` ids; a path this workspace already recorded keeps its id, so a re-apply uploads nothing twice. Then the tool adopts or creates every table (the table this workspace bound the entity to, else an existing table of the same label, is ADOPTED and given the fields, options and views it lacks; no stored value changes), writes first rows only where every bound table is empty, and mints a new version of each app the model declares \u2014 `--app` (repeatable, comma-separated) narrows which apps, while the tables are applied whole. Prints one line per app \u2014 alias, `app_id`, `created`/`updated` with the version minted or `unchanged`, and the address it is served at \u2014 then the model's notes once, as the server states them. **`--plan` writes nothing and uploads nothing**: it prints what the apply would do to the tables (what it would create, what it leaves as the workspace has it, what the two disagree about), which apps it would create or update, and what it would refuse an app for, exiting 1 when it would refuse one; workflow bodies are checked only at apply, since they name fields a plan has not created. **A rollback restores an app's earlier version** (`lotics run rollback_app`); table changes and data writes stay. Resolves and ANNOUNCES its workspace first. `--json` prints `{workspace_id, tables, apps, findings}` on stdout (with `--plan`, each table and app is what the apply would do), or `{ok: false, findings}` when the file does not check. |\n| `lotics model pull [-o <model.json>]` | **This workspace's model, rebuilt from what owns each part** \u2014 the tables, fields, options, templates and roles the workspace holds, how rows are recognised, and each app's body from its current version \u2014 through the `get_model` tool, as the file `model apply` reads: to stdout, or to the file `-o` names. What the workspace holds that a model cannot state is printed on stderr, never written into the file. Applying what it wrote changes nothing. |\n| `lotics app create <name> --custom [path]` | **A custom-code app**: creates the app (`POST /v1/apps`), scaffolds a Vite + React + TypeScript project into `[path]` (default `./<name>`, refused when not empty \u2014 before the app row exists) that depends on `@lotics/app-sdk` alone and draws with plain React, and installs it (`npm install --ignore-scripts`), then writes the declarations of the app's live bindings (`get_app_types`) into `.lotics/`, which the project's `tsconfig.json` includes. `package.json#lotics` names the app and its workspace, which is how `app deploy` in that directory finds both. The app has no version until the first `lotics app deploy`. `--custom` is required: an app the runtime draws from a model is made by `lotics model apply`. The SDK's reference is `node_modules/@lotics/app-sdk/AGENTS.md` inside the project. |\n| `lotics app pull [app_id] [path]` | **A custom-code app's live source, as a project ready to deploy on it.** The target is `[path]`, else this directory when it is the app's project (or no app is named), else `./<name>`. **The app's own project is brought up to date in place** \u2014 but only when it holds no edit since the version `package.json#lotics.current_version_id` names: its source (packed as a deploy packs it) is compared with that version's archive, ignoring `.lotics/` and `package.json#lotics`, and any difference refuses the pull, naming the changed files; a pull never merges, so local work is never lost. Already at the live version is a no-op that says so. **An empty or new directory receives the source whole**; any other directory is refused. Downloads come from `GET /v1/apps/{id}/versions/{version_id}/source`. `package.json#lotics` is then set to exactly the app, its workspace and the pulled version, dropping every other key an older CLI wrote there, so the next `lotics app deploy` builds on the live version; then `.lotics/` is written (`get_app_types`) and dependencies installed (`npm ci` with a lockfile, else `npm install`, both `--ignore-scripts`). A JSON app has no source tree: the server's refusal names `get_model`, and `lotics model pull` is its pull. |\n| `lotics app deploy [-m <message>]` | **Build this directory and upload it as a new version of the live app.** Rewrites `.lotics/` with the declarations of the app's live bindings (`get_app_types`, replacing each file there), then runs the project's `npm run typecheck` (warned about when absent) and `npm run build`, tars the source (without `node_modules`, `dist`, `.git`, `*.tsbuildinfo`) and `dist/`, and posts both to `POST /v1/apps/{id}/versions` on the version `package.json#lotics.current_version_id` names, then stamps the new one there. The version carries the app's queries, workflows, agents and capabilities forward unchanged \u2014 those are written through their tools. A project with no `build` script, or whose `package.json#lotics` still declares `queries`, `workflows`, `agents` or `capabilities`, is refused before anything is built; the refusal names the tool that sets each. **A 409 because another version went live since this directory's last deploy** (a deploy from elsewhere, a rollback) prints the server's sentence and the version that is live, and names `lotics app pull`: run in this directory, it brings an unedited project up to date, and lists the files a project with edits changed, to carry over into a fresh pull. `-m` (or a bare positional) is the version's message, optional. The workspace comes from `package.json#lotics.workspace_id` unless `--workspace` / `LOTICS_WORKSPACE` names another. |\n| `lotics docs` \\| `lotics docs <area>[/<section>]` | **This CLI's own references, carried inside the binary** \u2014 the model reference, this index, and every doc under `docs/` \u2014 so the doc a reader opens always describes the binary answering. Capped at ONE PAGE: a doc that does not fit prints its opening and the addresses of what it holds (`lotics docs <area>/<section>`, each section's size beside it, or a table's row names), and every address prints within a page. A custom-code app's SDK reference ships inside `@lotics/app-sdk` in the app's `node_modules`. |\n| `lotics upgrade` | Update this CLI in place. Runs the same installer a person would, chosen by how THIS copy arrived: an npm install upgrades through npm, a script install re-runs the script \u2014 the runtime knows which (the executable is compiled, the npm bin runs under node), so nobody has to. It downloads nothing itself; resolving a version, verifying the checksum and replacing a running executable already exist in the installers, and a second copy of that inside the binary would be a second thing to get right. Replacing the binary while it runs is safe \u2014 a rename leaves the running image mapped on unix, and on Windows the installer moves the old aside precisely because the file is in use. Already current is a no-op that says so. Needs no auth. |\n| `lotics docs model` \\| `lotics docs model/<section>[/\u2026]` | **The model reference, from inside the binary** \u2014 the one doc this CLI carries rather than resolves, because it describes this CLI's own model checker; listed first by `lotics docs`, at this CLI's version. Its first page is what a model composes with, the working order (jobs \u2192 entities and fields \u2192 `records` \u2192 one app per job \u2192 `model apply`) and the section addresses; every page of it is whole. Every top-level key of a `model.json`, every field `type` the contract admits with the config each one needs, the option / view / role / inline-template shapes, `records` (how a row of each entity is recognised), `write_rules`, `apps` (each register, record, act and check), the row format (relative dates `@today` / `@month-start` with whole-day offsets; links as `\"<entity-alias>:<ref>\"`), the rules, and one complete worked example; it points at `https://lotics.ai/presets/index.json` for complete example models of several trades. **Offline, no account.** |\n| `lotics report '<json>'` \\| `lotics report @report.json` | File a report with the Lotics team about what got in your way. **Covers the classes telemetry structurally cannot see**: a capability that does not exist (no command ran, so nothing was recorded), a command that exited 0 having done the wrong thing, an error whose message did not name the remedy, and anything that made authoring slower than it should be. **A frame, not a paragraph** \u2014 `{goal, actual, expected?, tried?, wanted?}`, `goal` and `actual` required, unknown keys dropped rather than refused. **No severity or category.** Ingest is inline JSON, `@file`, or `-` for stdin. A bare sentence is refused with the frame printed beside it, so the fix is one step; a bare invocation prints the frame BEFORE asking for a credential, since someone whose key will not resolve is exactly who has something to report. **Not spooled**: unlike telemetry it posts inline, prints whether it landed, and exits non-zero if it did not, echoing the report back so a failed send never loses it. Runs regardless of `LOTICS_TELEMETRY` \u2014 invoking it IS the consent that passive collection needs an opt-in for \u2014 but with telemetry off there are no recorded commands to attach, and it says so rather than implying context it does not have. Requires auth. Never paste records, file contents, or credentials. **Prints the id of each frame filed** \u2014 a filing nobody can cite cannot be answered about. The ids come from the server, so an instance that only logs the frames prints the count alone; the CLI never mints one of its own, which would hand back a token that resolves to nothing. |\n\n";
40281
40294
 
40282
40295
  // docs/data_model.md
40283
40296
  var data_model_default = '# The data model \u2014 tables, their fields, and how they relate\n\nThe decisions here outlive any one app, and most become expensive the moment a second screen depends\non them. They stand apart from building an app on purpose: **every workspace starts with tables\nand many never get an app**, so schema design is not a chapter of app building.\n\nThe rules below share one signature. **Both sides read correctly on their own**, so nothing reports\nthe problem \u2014 no error, no empty column, no failing query. Each is found by looking for it. Then,\nunder **Fields**, what each field type takes.\n\n## One fact, one column\n\n**Read the table\'s existing fields first, and add nothing that stores a fact the table already\nstores.** A value written as text and the same value held as a `select_record_link` are one fact in\ntwo columns \u2014 a customer\'s city typed into a text box beside a link to the city record, a status\nword beside the select that decides it, a total beside the formula that computes it.\n\nTwo columns for one fact do not stay equal. Some writer sets only one of them, and nothing reports\nthe divergence: both rows still look correct on their own. A reader that then matches on the text\nhalf treats "Acme" and "Acme Ltd" as different records, so an import creates a duplicate every time\nit runs.\n\n- **Prefer the link, the select, or the formula.** Text is a RENDERING of a record; compose it when\n you read, rather than storing it a second time.\n- **Renaming, retyping or re-pointing the existing field beats adding another.** A field\'s key is\n stable, so a rename breaks nothing that addresses it by key.\n- **Superseding a field means DELETING it**, not leaving it beside its replacement with a\n description that says which one is real.\n- **Empty is not the same as redundant.** A field nothing fills may still be the only home for a\n real distinction \u2014 read what it MEANS before removing it.\n\n## One entity, one table \u2014 and the test is measurable\n\nVariation belongs in a column \u2014 a multi-select role, a kind, a stage \u2014 not in a second table. Two\ntables for one kind of thing give the same real-world entity two rows, two ids and two halves of its\nhistory, and each screen shows whichever half it happens to link to.\n\nSplit tables are often right. A supplier book beside a customer book is a normal shape, and merging\non suspicion is a large repoint bought for nothing. So do not argue it in the abstract:\n\n> **List both tables\' names and look for one that appears in both.**\n\nNone means the split is holding. One means it has broken \u2014 the usual cause is a party you begin to\ninvoice as well as buy from \u2014 and the fix is to merge before a second screen depends on the copy.\n\nWorth writing as a test rather than a note, because a note about a condition nobody re-checks goes\nstale in silence.\n\n## One vocabulary wherever values are COPIED between tables\n\nTwo `select` fields for one concept carry DIFFERENT option keys even when their labels match \u2014 keys\nare minted per field. So anything moving a value between them needs a hand-written key map.\n\nThat map is code. Put it in one named module with a test; written inline at the copy it is invisible,\nuntested, and silently wrong the first time somebody renames an option, because a rename leaves the\nkey intact and the map still compiling. Prefer a link to a shared reference table where the set is\nopen or growing; keep a map only for a small closed set.\n\nWhere one side genuinely holds MORE values than the other, that is not drift \u2014 it is the model\ntelling the truth. The wider side must **refuse** what the narrower one cannot express rather than\nquietly picking the nearest value.\n\n## A copy boundary accounts for EVERY source field\n\nEach field on the source gets a column on the destination, a deliberate drop with the reason written\ndown, or a refusal.\n\nA field with nowhere to land is data destroyed at the boundary, and it is invisible afterwards: the\ndestination is not empty and not obviously wrong \u2014 just a number that no longer agrees with where it\ncame from.\n\n## Provenance is a LINK, not a flag and not a copy\n\nA row created BY another row carries a link to it.\n\nThat link is what makes "is this the estimate or the actual", "where did this come from" and "have we\nalready imported this" answerable at all. A boolean records that something was true once; a link\nstays true, survives a rename, and lets the next write UPDATE the original instead of adding a second\nrow beside it.\n\n## Say what makes two rows the SAME row\n\nDeclare the natural key in the table\'s description.\n\nAnything that imports, reconciles or de-duplicates has to decide identity, and with no declared key\nit falls back to comparing displayed text \u2014 which is how one company arrives three times under three\nspellings. Name the key: a reference number, a tax id, a link plus a period. Then match on `rec_\u2026`\nand `opt_\u2026`, never on rendered labels.\n\n## A state\'s HISTORY is rows, not columns\n\nA `changed at` column says only how long a row has been where it is now \u2014 the next move overwrites\nit \u2014 and a date column per state holds until something re-enters a state it already left.\n\nMeasuring time-in-state or conversion needs one ROW per move: a link to the subject, the state left,\nthe state entered, when. Hold those states as the source field\'s own `opt_` keys so the log carries\nno second vocabulary, and write the rows from that table\'s own lifecycle workflows, which covers\nevery writer rather than one app\'s.\n\n## Keep derived chains shallow\n\nFormulas and rollups are computed and STORED when a row is written, and one that reads another\nrecomputes with it. A rollup over a formula over a formula is paid three times on every touch, and\nagain for every row upstream of it.\n\n**Depth costs more than row count.** This is the optimisation lever that actually exists here; row\nscanning is the platform\'s problem, chain depth is yours.\n\n## Changing a money formula on a live table\n\nFormula edits recompute every row, so the only honest proof that one changed nothing it should not is\nthe numbers themselves:\n\n1. Snapshot the affected totals to a file.\n2. Make the change.\n3. Diff. Identical is the pass.\n\nAnd when a formula gains a new field, **test that the value IS the one you want, never that it\ndiffers from it** \u2014 an empty cell reads as `""`, which differs from every option key, so the inverted\nspelling silently zeroes every row written before the field existed.\n\n## Fields\n\nWhat `create_table` and `update_table` take in `add_fields`, and `update_table` in `update_fields`.\n\n### Types and formats\n\n`type` is one of `text`, `number`, `date`, `boolean`, `select`, `select_member`,\n`select_record_link`, `files`, `formula`, `rollup`, `lookup`, `autonumber`. `button` is retired: an\nexisting button field keeps running, but none is created, converted to or edited.\n\nA URL, an email, markdown, a checkbox, a datetime, a currency or a percentage is a `format` on\nanother type, never a `type`:\n\n| Wanted | Field |\n|---|---|\n| URL or external link | `{ type: "text", format: "link" }` |\n| Email or phone | `{ type: "text" }` |\n| Markdown | `{ type: "text", format: "markdown" }` |\n| Checkbox | `{ type: "boolean" }` |\n| Money | `{ type: "number", format: "currency", currency: "USD" }` |\n| Percentage | `{ type: "number", format: "percentage" }` |\n| Datetime | `{ type: "date", format: "datetime" }` |\n| Date range | `{ type: "date", format: "date_range" }` |\n\n### Properties\n\nA type\'s properties sit **directly on the field object** \u2014 there is no `config` wrapper. A property\nmay itself hold an object (`formula`, `aggregate_option`, `filter`, `order_by`); its inner keys stay\ninside it. Each type takes only its own:\n\n- **text** \u2014 `format?` (`"text"` | `"link"` | `"markdown"`), `unique?`, `default_value?` (a string).\n- **number** \u2014 `format?` (`"number"` | `"currency"` | `"percentage"`), `currency?` (an ISO 4217\n code), `unit?`, `unit_field?`, `currency_field?`, `default_value?` (a number). On `update_fields`,\n null clears `currency`, `unit`, `unit_field` or `currency_field`.\n - `unit`, beside format `"number"`: a measured code \u2014 g, kg, t, l, m3, cbm, mm, cm, m, km, m2,\n min, h, day \u2014 or a counted noun such as `ki\u1EC7n`. A change between two units of one dimension\n converts every stored figure; any other change relabels.\n - `unit_field`: A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s unit: every option label is a unit as `unit` takes one. In place of `unit`; only beside format "number".\n - `currency_field`: A single select on the same row, or a lookup of one through a one-link, whose chosen option is that row\'s currency: every option label is an ISO 4217 code. In place of `currency`; only beside format "currency".\n- **date** \u2014 `format?` (`"date"` | `"datetime"` | `"date_range"` | `"datetime_range"`), `timezone?`,\n `derive_from?`, `default_value?` (a date string). `derive_from: "created_at"` stamps the row\'s\n creation once, `"updated_at"` re-stamps on every update; the field is then read-only, takes no\n `default_value`, and holds only the `date` and `datetime` formats.\n- **boolean** \u2014 `default_value?` (`true` | `false`).\n- **select** \u2014 `options` `[{ name, color?, mark? }]`, `multi?`, `default_value?`.\n- **select_member** \u2014 `multi?`, `default_value?` (member ids).\n- **select_record_link** \u2014 `table_id`, `display_field_keys?`, `sync_both_ways?`, `cardinality?`,\n `paired_field_name?`, `paired_field_display_field_keys?`. Without `display_field_keys` a link shows\n the target\'s autonumber, else its first text field. `sync_both_ways: true` on an existing one-way\n link makes it two-way \u2014 never add a second link for that. `cardinality: "one"` marks the child\n side of a parent-child pair (the linked record is this one\'s parent); two paired sides cannot both\n be `"one"`. The `paired_*` keys name and label the back-reference the target gets.\n- **files** \u2014 no properties.\n- **autonumber** \u2014 `template` (`"INV-{YEAR}-{N:4}"`; tokens `{N}`, `{N:W}` zero-padded to W,\n `{YEAR}`, `{YEAR:2}`, `{MONTH}`, `{DAY}`), or `prefix` + `padding` (1\u201320) for `PREFIX-0001`.\n- **any type**, at create \u2014 `confirm_before_update?`: a person confirms each later edit.\n\n`default_value` fills the field on a NEW record that states no value; existing records are never\nbackfilled, and null clears it. A select\'s or a member field\'s default is an ARRAY even when the\nfield holds one (`["Unread"]`, never `"Unread"`) \u2014 option names in `add_fields`, where the keys do\nnot exist yet, and option keys (`opt_\u2026`) in `update_fields`.\n\n### Computed fields\n\nRead-only in records. `formula` is one key holding an object; a rollup\'s and a lookup\'s properties\nare separate keys on the field \u2014 `rollup: {\u2026}` and `lookup: {\u2026}` are refused as unrecognized.\n\nformula: { expression, format?, currency?, unit?, unit_field?, currency_field? }\n\n- `{Field Name}` or `{fld_key}` names a field of the same table, stored as its key, so a rename never\n breaks the formula. A reference to no field, or a call to something that is no helper, is refused.\n- A select reads as an ARRAY of option keys: `{Status}[0]` for a single select,\n `includes({Tags}, "opt_\u2026")` for a multi.\n- The operators, the helpers and how an empty cell reads are the `model` reference, section `formula` \u2014 a model\n names fields by alias where a table tool names them by name or key; the language is the same.\n- On `update_fields` a key left out keeps its stored value, and null clears `currency`, `unit`,\n `unit_field` or `currency_field`. `get_table` marks a formula that is null when every field it\n reads is empty.\n\nrollup \u2014 `source_field_key`, `aggregate_option: { field_key?, operation }`, `filter?`\n\n- `source_field_key` is a `select_record_link` of this table; `aggregate_option.field_key` is a\n field of the linked table, required by every operation but `count`, which counts linked records.\n- Operations by the aggregated field\'s type \u2014 none: `count`; any: `empty`, `filled`,\n `percent_empty`, `percent_filled`, `unique`, `percent_unique`; number: `sum`, `avg`, `median`,\n `min`, `max`, `range`; date: `earliest`, `latest`, `date_range`.\n- The cell\'s type comes from the OPERATION: `earliest` and `latest` hold a date, every other\n operation a number (`date_range` counts days). A "most recent linked date" is `latest` \u2014 `min` and\n `max` are numeric and refuse a date. A rollup takes no format, currency or unit of its own: `sum`\n over money carries the aggregated field\'s currency, over a weight its unit.\n- Over a figure read in each row\'s own unit or currency (`unit_field` / `currency_field`), `sum`,\n `avg`, `median`, `min`, `max` and `range` hold only where that select is a lookup, through the\n link paired with `source_field_key`, of a single select on this table \u2014 the total reads in this\n row\'s option of it. A lookup of such a field has no unit.\n- `filter` aggregates only the linked records it matches: a full group over the LINKED table\'s\n fields \u2014 `{ node_type: "group", logic, children: [{ node_type: "condition", type, field_key,\n operator, value }] }`, a select condition using `has_any_of` with an ARRAY value.\n\nlookup \u2014 `source_field_key`, `lookup_field_key`, `order_by?`\n\n- `lookup_field_key` is a field of the linked table. Without `order_by` the cell holds every linked\n record\'s value; with `order_by: { field_key, direction }` (`desc` latest, `asc` earliest) it holds\n the picked field of the single extreme record \u2014 a "latest linked X" that maintains itself.\n Ordered lookups sharing one `order_by` resolve to the SAME record.\n\n### Examples\n\n```\n{ name: "Gi\xE1 b\xE1n", type: "number", format: "currency", currency: "VND" }\n{ name: "Tr\u1ECDng l\u01B0\u1EE3ng", type: "number", unit: "kg" }\n{ name: "S\u1ED1 l\u01B0\u1EE3ng", type: "number", unit_field: "\u0110VT" }\n{ name: "Tr\u1EA1ng th\xE1i", type: "select", options: [{ name: "M\u1EDBi" }, { name: "Xong" }] }\n{ name: "M\xE3 \u0111\u01A1n", type: "autonumber", template: "SR-{YEAR}-{N:4}" }\n{ name: "Kh\xE1ch h\xE0ng", type: "select_record_link", table_id: "tbl_x", sync_both_ways: true }\n{ name: "Total", type: "formula", formula: { expression: "{Price} * {Qty}", format: "currency", currency: "VND" } }\n{ name: "SL \u0111\xE3 giao", type: "rollup", source_field_key: "Giao h\xE0ng", aggregate_option: { operation: "sum", field_key: "S\u1ED1 l\u01B0\u1EE3ng" } }\n{ name: "Customer Name", type: "lookup", source_field_key: "Customer", lookup_field_key: "Name" }\n{ name: "Latest note", type: "lookup", source_field_key: "Calls", lookup_field_key: "Note", order_by: { field_key: "At", direction: "desc" } }\n```\n\n### Changing a field\n\nA field added by `update_table` shows in a view only when `add_to_views` names it, at the view\'s far\nright; `update_view` with `move_field` places it beside the columns it belongs with.\n\nAn `update_fields` entry is `{ field_key, name?, description?, convert_to?, \u2026 }` with the same flat\nproperties as `add_fields`, plus what only an update does:\n\n- A select\'s options change through `add_options` `[{ name, color?, mark? }]`, `update_options`\n `[{ key, name?, color?, mark? }]`, `remove_option_keys` and `reorder_options` (every existing\n option once, in order; options added in the same call follow). An option is named by its `opt_\u2026`\n key (its name also resolves); a rename keeps every record\'s value. `mark` is the option\'s brand\n (`{ kind: "brand", name: "tiktok" }`) or kit glyph (`{ kind: "icon", name: "wrench" }`), drawn in\n place of its colour dot \u2014 every option of a field has one or none does, so marking sets them all\n in one call and `mark: null` on each clears them.\n- A link\'s `sync_both_ways: false` disconnects the pair; a new `table_id` re-points it and clears its\n record data; `cardinality` is `"one"` for the child side of a parent-child pair, `"many"` for peers.\n';
@@ -40302,8 +40315,8 @@ var migration_default = "# @lotics/cli \u2014 migration notes\n\nWhat to DO when
40302
40315
 
40303
40316
  // ../shared/src/doc_pages.ts
40304
40317
  var DOC_LINKS = {
40305
- cli: (path15) => `lotics docs ${path15}`,
40306
- tool: (path15) => `docs path="${path15}"`
40318
+ cli: (path16) => `lotics docs ${path16}`,
40319
+ tool: (path16) => `docs path="${path16}"`
40307
40320
  };
40308
40321
  var SOURCES = [
40309
40322
  { area: "model", file: "src/model_reference.md", marker: "a workspace starts here", readers: ["cli", "tool"] },
@@ -40505,8 +40518,8 @@ function renderDocIndex(areas, usage) {
40505
40518
  const lines = areas.map((a, index) => ` ${names[index].padEnd(width)}${titleOf(a.text)}`);
40506
40519
  return [...lines, "", ...usage].join("\n");
40507
40520
  }
40508
- function readDocs(areas, path15, surface) {
40509
- const asked = (path15 ?? "").trim();
40521
+ function readDocs(areas, path16, surface) {
40522
+ const asked = (path16 ?? "").trim();
40510
40523
  if (asked === "") return { text: oneNewline(renderDocIndex(areas, surface.usage)) };
40511
40524
  let hits = matchDocArea(areas, asked);
40512
40525
  let section;
@@ -40517,11 +40530,11 @@ function readDocs(areas, path15, surface) {
40517
40530
  section = asked.slice(cut + 1);
40518
40531
  }
40519
40532
  }
40520
- if (hits.length === 0) return { error: `No reference area matching "${path15}".
40533
+ if (hits.length === 0) return { error: `No reference area matching "${path16}".
40521
40534
  ${surface.listHint}` };
40522
40535
  if (hits.length > 1) {
40523
40536
  return {
40524
- error: `"${path15}" matches more than one reference:
40537
+ error: `"${path16}" matches more than one reference:
40525
40538
  ${hits.map((a) => ` \u2022 ${surface.link(a.area)}`).join("\n")}`
40526
40539
  };
40527
40540
  }
@@ -40702,7 +40715,11 @@ var COMMANDS = [
40702
40715
  " @lotics/app-sdk, installed, and typed by the app's",
40703
40716
  " bindings in .lotics/. An app stated in a model is made",
40704
40717
  " by lotics model apply",
40705
- " lotics app deploy [-m <msg>] Rewrite .lotics/ from the app's live bindings, then",
40718
+ " lotics app pull [app_id] [path] The live version's source, installed and typed, ready to",
40719
+ " deploy on it: this app's project is brought up to date in",
40720
+ " place (refused, naming the files, when it has edits since",
40721
+ " its version); an empty or new [path] receives it whole",
40722
+ " lotics app deploy [-m <msg>] Rewrite .lotics/ from the app's live bindings, then",
40706
40723
  " typecheck + build this directory and upload it as a new",
40707
40724
  " version of the live app. Its queries, workflows, agents",
40708
40725
  " and capabilities carry forward unchanged \u2014 each is set",
@@ -40768,8 +40785,8 @@ function helpEntries(help) {
40768
40785
  }
40769
40786
  return entries2;
40770
40787
  }
40771
- function commandHelp(path15) {
40772
- const words = path15.filter((word) => word !== "");
40788
+ function commandHelp(path16) {
40789
+ const words = path16.filter((word) => word !== "");
40773
40790
  if (words.length === 0) return null;
40774
40791
  const group = COMMANDS.find(
40775
40792
  (g) => g.verbs.includes(words[0]) || (g.aliases ?? []).includes(words[0])
@@ -40982,6 +40999,9 @@ function readAppWorkspaceId(projectDir) {
40982
40999
  return null;
40983
41000
  }
40984
41001
  }
41002
+ function readProjectAppId(projectDir) {
41003
+ return stringOrNull(readLotics(projectDir)?.app_id);
41004
+ }
40985
41005
  function readAppMeta(projectDir) {
40986
41006
  const lotics = readLotics(projectDir);
40987
41007
  const appId = stringOrNull(lotics?.app_id);
@@ -41000,6 +41020,15 @@ function readAppMeta(projectDir) {
41000
41020
  version_number: typeof versionNumber === "number" ? versionNumber : null
41001
41021
  };
41002
41022
  }
41023
+ function writePulledManifest(projectDir, manifest) {
41024
+ const pkgPath = path7.join(projectDir, "package.json");
41025
+ if (!fs7.existsSync(pkgPath)) fail(`The pulled source has no package.json at ${pkgPath}.`);
41026
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
41027
+ if (typeof pkg !== "object" || pkg === null) fail(`${pkgPath} is not a JSON object.`);
41028
+ const lotics = { ...manifest, version_number: null };
41029
+ fs7.writeFileSync(pkgPath, `${JSON.stringify({ ...pkg, lotics }, null, 2)}
41030
+ `);
41031
+ }
41003
41032
  function writeDeployedVersion(projectDir, version2) {
41004
41033
  const pkgPath = path7.join(projectDir, "package.json");
41005
41034
  const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
@@ -41395,6 +41424,10 @@ function flushPendingWarnings() {
41395
41424
  }
41396
41425
 
41397
41426
  // src/app_toolchain.ts
41427
+ var ARCHIVE_EXCLUDES = ["node_modules", "dist", "*.tsbuildinfo", ".git"];
41428
+ function packSource(projectDir, archivePath) {
41429
+ return runTar(["-czf", archivePath, ...ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`), "."], projectDir);
41430
+ }
41398
41431
  function runTar(args, cwd) {
41399
41432
  return new Promise((resolve, reject) => {
41400
41433
  const proc = spawn("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -41580,10 +41613,110 @@ Ready. Next steps:`);
41580
41613
  return { app_id: app.id, path: targetPath };
41581
41614
  }
41582
41615
 
41583
- // src/app_deploy.ts
41616
+ // src/app_pull.ts
41584
41617
  import fs11 from "node:fs";
41585
41618
  import path11 from "node:path";
41586
41619
  import { tmpdir } from "node:os";
41620
+ var NAMED_CHANGES = 20;
41621
+ async function appPull(client, args) {
41622
+ const cwdAppId = readProjectAppId(process.cwd());
41623
+ const appId = args.appId ?? cwdAppId;
41624
+ if (appId === null) fail("This directory is not an app project. Name the app: lotics app pull <app_id> [path]");
41625
+ const app = await client.getApp(appId);
41626
+ const live = app.current_version_id;
41627
+ if (live === null) fail(`App ${app.id} has no version yet, so there is nothing to pull.`);
41628
+ const targetPath = path11.resolve(
41629
+ args.targetPath ?? (args.appId === void 0 || cwdAppId === app.id ? process.cwd() : appDirName(app.name))
41630
+ );
41631
+ const targetAppId = readProjectAppId(targetPath);
41632
+ if (targetAppId !== null && targetAppId !== app.id) {
41633
+ fail(`${targetPath} is the project of ${targetAppId}, not ${app.id}.`);
41634
+ }
41635
+ if (targetAppId === null && fs11.existsSync(targetPath) && fs11.readdirSync(targetPath).length > 0) {
41636
+ fail(`${targetPath} is not empty and is not this app's project. Pull into a new directory.`);
41637
+ }
41638
+ const scratch = fs11.mkdtempSync(path11.join(tmpdir(), "lotics-pull-"));
41639
+ try {
41640
+ if (targetAppId !== null) {
41641
+ const base = readAppMeta(targetPath).current_version_id;
41642
+ if (base === live) {
41643
+ note2(`${targetPath} is already at the live version (${live}).`);
41644
+ return { app_id: app.id, version_id: live, path: targetPath, outcome: "unchanged" };
41645
+ }
41646
+ if (base === null) {
41647
+ fail(`${targetPath} was never deployed, so its edits cannot be told apart from ${live}. Pull into a new directory.`);
41648
+ }
41649
+ const local = await unpack(scratch, "local", (archive2) => packSource(targetPath, archive2));
41650
+ const baseTree = await unpack(
41651
+ scratch,
41652
+ "base",
41653
+ async (archive2) => fs11.writeFileSync(archive2, await client.downloadAppVersionSource(app.id, base))
41654
+ );
41655
+ const changed = changedSourcePaths(baseTree, local);
41656
+ if (changed.length > 0) {
41657
+ const named = changed.slice(0, NAMED_CHANGES).map((file2) => ` ${file2}`);
41658
+ const rest = changed.length > NAMED_CHANGES ? [` \u2026and ${changed.length - NAMED_CHANGES} more`] : [];
41659
+ fail(
41660
+ `${targetPath} has changes since ${base}, the version it was based on:
41661
+ ${[...named, ...rest].join("\n")}
41662
+ Version ${live} is live. Pull it into a new directory (lotics app pull ${app.id} <new-directory>), carry these changes over, and deploy from there.`
41663
+ );
41664
+ }
41665
+ for (const file2 of listFiles(local)) fs11.rmSync(path11.join(targetPath, file2));
41666
+ removeEmptyDirs(targetPath, listDirs(local));
41667
+ }
41668
+ const archive = path11.join(scratch, "live.tar.gz");
41669
+ fs11.writeFileSync(archive, await client.downloadAppVersionSource(app.id, live));
41670
+ fs11.mkdirSync(targetPath, { recursive: true });
41671
+ await runTar(["-xzf", archive], targetPath);
41672
+ } finally {
41673
+ fs11.rmSync(scratch, { recursive: true, force: true });
41674
+ }
41675
+ writePulledManifest(targetPath, { app_id: app.id, workspace_id: app.workspace_id, current_version_id: live });
41676
+ await writeAppTypes(client, targetPath, app.id);
41677
+ note2(`Pulled ${app.name} (${live}) into ${targetPath}`);
41678
+ await runNpmInstall(targetPath);
41679
+ return { app_id: app.id, version_id: live, path: targetPath, outcome: "pulled" };
41680
+ }
41681
+ async function unpack(scratch, name, write) {
41682
+ const archive = path11.join(scratch, `${name}.tar.gz`);
41683
+ const dir = path11.join(scratch, name);
41684
+ await write(archive);
41685
+ fs11.mkdirSync(dir);
41686
+ await runTar(["-xzf", archive], dir);
41687
+ return dir;
41688
+ }
41689
+ function changedSourcePaths(baseDir, localDir) {
41690
+ const files = /* @__PURE__ */ new Set([...listFiles(baseDir), ...listFiles(localDir)]);
41691
+ return [...files].filter((file2) => !file2.startsWith(`.lotics${path11.sep}`)).filter((file2) => !sameSource(path11.join(baseDir, file2), path11.join(localDir, file2), file2 === "package.json")).sort();
41692
+ }
41693
+ function sameSource(a, b, isManifest) {
41694
+ if (!fs11.existsSync(a) || !fs11.existsSync(b)) return false;
41695
+ if (!isManifest) return fs11.readFileSync(a).equals(fs11.readFileSync(b));
41696
+ return JSON.stringify(withoutOwnRecord(a)) === JSON.stringify(withoutOwnRecord(b));
41697
+ }
41698
+ function withoutOwnRecord(pkgPath) {
41699
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
41700
+ if (typeof pkg !== "object" || pkg === null) return pkg;
41701
+ return Object.fromEntries(Object.entries(pkg).filter(([key]) => key !== "lotics"));
41702
+ }
41703
+ function listFiles(dir) {
41704
+ return fs11.readdirSync(dir, { recursive: true, withFileTypes: true }).filter((entry) => !entry.isDirectory()).map((entry) => path11.relative(dir, path11.join(entry.parentPath, entry.name)));
41705
+ }
41706
+ function listDirs(dir) {
41707
+ return fs11.readdirSync(dir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path11.relative(dir, path11.join(entry.parentPath, entry.name)));
41708
+ }
41709
+ function removeEmptyDirs(root, dirs) {
41710
+ for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
41711
+ const full = path11.join(root, dir);
41712
+ if (fs11.existsSync(full) && fs11.readdirSync(full).length === 0) fs11.rmdirSync(full);
41713
+ }
41714
+ }
41715
+
41716
+ // src/app_deploy.ts
41717
+ import fs12 from "node:fs";
41718
+ import path12 from "node:path";
41719
+ import { tmpdir as tmpdir2 } from "node:os";
41587
41720
  var BINDING_KEY_OWNERS = {
41588
41721
  queries: "set_app_query",
41589
41722
  workflows: "set_app_workflow",
@@ -41595,19 +41728,18 @@ function declaredBindingKeys(pkg) {
41595
41728
  if (typeof lotics !== "object" || lotics === null) return [];
41596
41729
  return Object.entries(BINDING_KEY_OWNERS).filter(([key]) => key in lotics).map(([key, tool]) => `package.json#lotics.${key}: the app's ${key} are set live with \`lotics run ${tool}\`.`);
41597
41730
  }
41598
- var ARCHIVE_EXCLUDES = ["node_modules", "dist", "*.tsbuildinfo", ".git"];
41599
41731
  async function appDeploy(client, args) {
41600
- const projectDir = path11.resolve(args.projectDir ?? process.cwd());
41732
+ const projectDir = path12.resolve(args.projectDir ?? process.cwd());
41601
41733
  const meta3 = readAppMeta(projectDir);
41602
- const pkg = JSON.parse(fs11.readFileSync(path11.join(projectDir, "package.json"), "utf-8"));
41734
+ const pkg = JSON.parse(fs12.readFileSync(path12.join(projectDir, "package.json"), "utf-8"));
41603
41735
  const scripts = typeof pkg === "object" && pkg !== null && "scripts" in pkg ? pkg.scripts : void 0;
41604
41736
  if (typeof scripts !== "object" || scripts === null || !("build" in scripts)) {
41605
- fail(`${path11.join(projectDir, "package.json")} declares no \`build\` script, so there is no bundle to deploy.`);
41737
+ fail(`${path12.join(projectDir, "package.json")} declares no \`build\` script, so there is no bundle to deploy.`);
41606
41738
  }
41607
41739
  const declared = declaredBindingKeys(pkg);
41608
41740
  if (declared.length > 0) {
41609
41741
  fail(
41610
- `${path11.join(projectDir, "package.json")} still declares bindings a deploy does not ship:
41742
+ `${path12.join(projectDir, "package.json")} still declares bindings a deploy does not ship:
41611
41743
  ${declared.join("\n ")}
41612
41744
  Delete these keys from package.json#lotics, then deploy again.`
41613
41745
  );
@@ -41616,26 +41748,26 @@ Delete these keys from package.json#lotics, then deploy again.`
41616
41748
  await runAppTypecheck(projectDir);
41617
41749
  note2("Building...");
41618
41750
  await runNpm(["run", "build"], projectDir);
41619
- const distDir = path11.join(projectDir, "dist");
41620
- if (!fs11.existsSync(distDir)) {
41751
+ const distDir = path12.join(projectDir, "dist");
41752
+ if (!fs12.existsSync(distDir)) {
41621
41753
  fail(`\`npm run build\` produced no dist/ in ${projectDir} \u2014 Vite writes one by default; check its outDir.`);
41622
41754
  }
41623
41755
  const stamp = Date.now();
41624
- const tmpSource = path11.join(tmpdir(), `lotics-source-${stamp}.tar.gz`);
41625
- const tmpDist = path11.join(tmpdir(), `lotics-dist-${stamp}.tar.gz`);
41756
+ const tmpSource = path12.join(tmpdir2(), `lotics-source-${stamp}.tar.gz`);
41757
+ const tmpDist = path12.join(tmpdir2(), `lotics-dist-${stamp}.tar.gz`);
41626
41758
  try {
41627
41759
  note2("Packaging...");
41628
- await runTar(["-czf", tmpSource, ...ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`), "."], projectDir);
41760
+ await packSource(projectDir, tmpSource);
41629
41761
  await runTar(["-czf", tmpDist, "-C", distDir, "."], projectDir);
41630
41762
  note2("Uploading...");
41631
41763
  const result = await client.deployAppVersion({
41632
41764
  app_id: meta3.app_id,
41633
- source_archive: fs11.readFileSync(tmpSource),
41634
- dist_archive: fs11.readFileSync(tmpDist),
41765
+ source_archive: fs12.readFileSync(tmpSource),
41766
+ dist_archive: fs12.readFileSync(tmpDist),
41635
41767
  prev_version_id: meta3.current_version_id,
41636
41768
  ...args.message === void 0 ? {} : { message: args.message }
41637
41769
  }).catch((error52) => {
41638
- throw staleRefusal(error52, meta3.current_version_id);
41770
+ throw staleRefusal(error52, meta3.current_version_id, meta3.app_id);
41639
41771
  });
41640
41772
  writeDeployedVersion(projectDir, result);
41641
41773
  note2(`Deployed v${result.version_number} (${result.version_id})`);
@@ -41643,18 +41775,18 @@ Delete these keys from package.json#lotics, then deploy again.`
41643
41775
  if (result.origin !== void 0) note2(`Address: ${result.origin}`);
41644
41776
  return result;
41645
41777
  } finally {
41646
- fs11.rmSync(tmpSource, { force: true });
41647
- fs11.rmSync(tmpDist, { force: true });
41778
+ fs12.rmSync(tmpSource, { force: true });
41779
+ fs12.rmSync(tmpDist, { force: true });
41648
41780
  }
41649
41781
  }
41650
- function staleRefusal(error52, deployedFrom) {
41782
+ function staleRefusal(error52, deployedFrom, appId) {
41651
41783
  if (!(error52 instanceof LoticsRequestError) || error52.status !== 409) return error52;
41652
41784
  const served = error52.body.current_version_id;
41653
41785
  if (typeof served !== "string" || served === deployedFrom) return error52;
41654
41786
  const said = typeof error52.body.message === "string" ? error52.body.message : error52.message;
41655
41787
  return new CliError(
41656
41788
  `${said}
41657
- Version ${served} went live after this directory's last deploy. To ship this directory over it, set package.json#lotics.current_version_id to "${served}" and deploy again.`
41789
+ Version ${served} went live after this directory's last deploy. Run \`lotics app pull\` here: it brings this directory up to date when it has no edits, and names the files it changed when it has, to carry over into \`lotics app pull ${appId} <new-directory>\`.`
41658
41790
  );
41659
41791
  }
41660
41792
 
@@ -41910,8 +42042,8 @@ function parseArgs(argv2) {
41910
42042
  }
41911
42043
 
41912
42044
  // src/model_commands.ts
41913
- import fs12 from "node:fs";
41914
- import path12 from "node:path";
42045
+ import fs13 from "node:fs";
42046
+ import path13 from "node:path";
41915
42047
 
41916
42048
  // ../shared/src/app_compile/history_rows.ts
41917
42049
  var LANDED = "@today 00:00";
@@ -42011,8 +42143,8 @@ function count(n, noun) {
42011
42143
  return `${n} ${noun}${n === 1 ? "" : "s"}`;
42012
42144
  }
42013
42145
  function besideFile(file2) {
42014
- const dir = path12.dirname(path12.resolve(file2));
42015
- return { documents: { dir, exists: (relative) => fs12.existsSync(path12.resolve(dir, relative)) } };
42146
+ const dir = path13.dirname(path13.resolve(file2));
42147
+ return { documents: { dir, exists: (relative) => fs13.existsSync(path13.resolve(dir, relative)) } };
42016
42148
  }
42017
42149
  function oneFinding(pathName, message2) {
42018
42150
  return [{ path: pathName, message: message2, severity: "error" }];
@@ -42020,7 +42152,7 @@ function oneFinding(pathName, message2) {
42020
42152
  function readModelFile(file2) {
42021
42153
  let raw;
42022
42154
  try {
42023
- raw = JSON.parse(fs12.readFileSync(file2, "utf-8"));
42155
+ raw = JSON.parse(fs13.readFileSync(file2, "utf-8"));
42024
42156
  } catch (error52) {
42025
42157
  return { kind: "error", findings: oneFinding(file2, `cannot read: ${error52 instanceof Error ? error52.message : String(error52)}`) };
42026
42158
  }
@@ -42074,9 +42206,9 @@ async function uploadDocuments(client, read, file2) {
42074
42206
  const idByPath = new Map(Object.entries(known).filter(([relative]) => paths.includes(relative)));
42075
42207
  const fresh = paths.filter((relative) => !idByPath.has(relative));
42076
42208
  if (fresh.length > 0) note2(`Uploading ${count(fresh.length, "document")} the rows attach\u2026`);
42077
- const baseDir = path12.dirname(path12.resolve(file2));
42209
+ const baseDir = path13.dirname(path13.resolve(file2));
42078
42210
  for (const relative of fresh) {
42079
- const uploaded = await client.uploadFiles([path12.resolve(baseDir, relative)]);
42211
+ const uploaded = await client.uploadFiles([path13.resolve(baseDir, relative)]);
42080
42212
  const refused = uploaded.errors[0];
42081
42213
  if (refused !== void 0) fail(`"${relative}" could not be uploaded (${refused.error}), so nothing was applied.`);
42082
42214
  const id = uploaded.files[0]?.id;
@@ -42201,8 +42333,8 @@ async function modelPull(client, output) {
42201
42333
  process.stdout.write(text);
42202
42334
  return;
42203
42335
  }
42204
- fs12.writeFileSync(output, text);
42205
- note2(`Wrote ${path12.resolve(output)}.`);
42336
+ fs13.writeFileSync(output, text);
42337
+ note2(`Wrote ${path13.resolve(output)}.`);
42206
42338
  }
42207
42339
 
42208
42340
  // src/signin_link.ts
@@ -42382,7 +42514,7 @@ async function validateOrgWorkspacePin(client, orgId, profile) {
42382
42514
  }
42383
42515
 
42384
42516
  // src/knowledge.ts
42385
- import fs14 from "node:fs";
42517
+ import fs15 from "node:fs";
42386
42518
 
42387
42519
  // ../shared/src/knowledge_tags.ts
42388
42520
  function isHiddenDoc(doc) {
@@ -42390,17 +42522,17 @@ function isHiddenDoc(doc) {
42390
42522
  }
42391
42523
 
42392
42524
  // src/cli_io.ts
42393
- import fs13 from "node:fs";
42394
- import path13 from "node:path";
42525
+ import fs14 from "node:fs";
42526
+ import path14 from "node:path";
42395
42527
  function writeFileAtomic(filePath, bytes) {
42396
- const dir = path13.dirname(path13.resolve(filePath));
42397
- const tmp = path13.join(dir, `.${path13.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
42398
- fs13.writeFileSync(tmp, bytes);
42528
+ const dir = path14.dirname(path14.resolve(filePath));
42529
+ const tmp = path14.join(dir, `.${path14.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
42530
+ fs14.writeFileSync(tmp, bytes);
42399
42531
  try {
42400
- fs13.renameSync(tmp, filePath);
42532
+ fs14.renameSync(tmp, filePath);
42401
42533
  } catch (error52) {
42402
42534
  try {
42403
- fs13.unlinkSync(tmp);
42535
+ fs14.unlinkSync(tmp);
42404
42536
  } catch {
42405
42537
  }
42406
42538
  throw error52;
@@ -42410,8 +42542,8 @@ function writeFileAtomic(filePath, bytes) {
42410
42542
 
42411
42543
  // src/knowledge.ts
42412
42544
  function readBodyFile(filePath) {
42413
- if (!fs14.existsSync(filePath)) fail(`File not found: ${filePath}`);
42414
- return fs14.readFileSync(filePath, "utf-8");
42545
+ if (!fs15.existsSync(filePath)) fail(`File not found: ${filePath}`);
42546
+ return fs15.readFileSync(filePath, "utf-8");
42415
42547
  }
42416
42548
  function resolveBody(flags, required2) {
42417
42549
  if (flags.from !== void 0 && flags.content !== void 0) {
@@ -42768,8 +42900,8 @@ function prompt(question) {
42768
42900
  });
42769
42901
  });
42770
42902
  }
42771
- async function publicPost(apiUrl, path15, body) {
42772
- const response = await fetch(`${apiUrl}${path15}`, {
42903
+ async function publicPost(apiUrl, path16, body) {
42904
+ const response = await fetch(`${apiUrl}${path16}`, {
42773
42905
  method: "POST",
42774
42906
  headers: { "Content-Type": "application/json" },
42775
42907
  body: JSON.stringify(body)
@@ -43130,13 +43262,13 @@ async function resolveWorkspace(client, ctx) {
43130
43262
  function resolveUploadPaths(rawPaths) {
43131
43263
  const result = [];
43132
43264
  for (const p of rawPaths) {
43133
- const resolved = path14.resolve(p);
43134
- const stat = fs15.statSync(resolved);
43265
+ const resolved = path15.resolve(p);
43266
+ const stat = fs16.statSync(resolved);
43135
43267
  if (stat.isDirectory()) {
43136
- const entries2 = fs15.readdirSync(resolved, { withFileTypes: true });
43268
+ const entries2 = fs16.readdirSync(resolved, { withFileTypes: true });
43137
43269
  for (const entry of entries2) {
43138
43270
  if (entry.isFile()) {
43139
- result.push(path14.join(resolved, entry.name));
43271
+ result.push(path15.join(resolved, entry.name));
43140
43272
  }
43141
43273
  }
43142
43274
  } else {
@@ -43334,11 +43466,11 @@ async function main(argv2) {
43334
43466
  if (input.startsWith("@")) {
43335
43467
  source = "file";
43336
43468
  const file2 = input.slice(1);
43337
- if (!fs15.existsSync(file2)) {
43469
+ if (!fs16.existsSync(file2)) {
43338
43470
  console.error(`No such file: ${file2}`);
43339
43471
  process.exit(1);
43340
43472
  }
43341
- input = fs15.readFileSync(file2, "utf-8");
43473
+ input = fs16.readFileSync(file2, "utf-8");
43342
43474
  } else if (input === "-") {
43343
43475
  source = "stdin";
43344
43476
  input = await readStdin();
@@ -43559,6 +43691,7 @@ async function main(argv2) {
43559
43691
  console.error("Usage:");
43560
43692
  console.error(" lotics app create <name> --custom [path] Create a custom-code app and scaffold it here");
43561
43693
  console.error(" lotics app deploy [-m <message>] Build this directory and upload it as a new version");
43694
+ console.error(" lotics app pull [app_id] [path] The app's live source: this project brought up to date, or a new directory");
43562
43695
  console.error(" Everything else about an app is a tool: lotics tools, then lotics run <tool>.");
43563
43696
  process.exit(1);
43564
43697
  }
@@ -43851,6 +43984,15 @@ Available workspaces:`);
43851
43984
  if (flags.json) emitJson({ workspace_id: ctx.workspaceId, ...created });
43852
43985
  return;
43853
43986
  }
43987
+ if (subcommand === "pull") {
43988
+ if (flags.json) setMachineOutput(true);
43989
+ const pulled = await appPull(client, {
43990
+ ...toolArgs === void 0 ? {} : { appId: toolArgs },
43991
+ ...restArgs[0] === void 0 ? {} : { targetPath: restArgs[0] }
43992
+ });
43993
+ if (flags.json) emitJson({ workspace_id: ctx.workspaceId, ...pulled });
43994
+ return;
43995
+ }
43854
43996
  if (subcommand === "deploy") {
43855
43997
  const message2 = (flags.message ?? toolArgs)?.trim();
43856
43998
  if (flags.json) setMachineOutput(true);
@@ -43946,7 +44088,7 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
43946
44088
  const toolName = subcommand;
43947
44089
  const ingested = await ingestJsonArgs({
43948
44090
  rawArg: toolArgs,
43949
- readFile: (p) => fs15.readFileSync(p, "utf-8"),
44091
+ readFile: (p) => fs16.readFileSync(p, "utf-8"),
43950
44092
  readStdin
43951
44093
  });
43952
44094
  if (ingested.kind === "error") {