@lotics/cli 0.280.0 → 0.282.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
  }
@@ -21964,6 +21977,186 @@ function fixtureWriteOrder(entities) {
21964
21977
  return { cycle: entities.filter((entity) => !placed.has(entity.alias)).map((entity) => entity.alias) };
21965
21978
  }
21966
21979
 
21980
+ // ../shared/src/schemas/files.ts
21981
+ var processedFileSchema = zod_default.object({
21982
+ id: zod_default.string().describe("Unique identifier for the processed file"),
21983
+ filename: zod_default.string().describe("Original filename of the uploaded file"),
21984
+ mime_type: zod_default.string().describe("MIME type of the file (e.g., 'image/png', 'application/pdf')"),
21985
+ // Stable proxy URL (`/v1/files/<file_storage_key>`), resolved at serving time
21986
+ // — never stored in the DB.
21987
+ url: zod_default.string().optional().describe("Stable file serving URL, resolved at serving time"),
21988
+ thumbnail_url: zod_default.string().optional().describe("Thumbnail URL for image files, resolved at serving time"),
21989
+ preview_url: zod_default.string().optional().describe("Preview URL for legacy Excel files, resolved at serving time"),
21990
+ provider: zod_default.string().describe("Storage provider name (e.g., 'aws', 'gcs')"),
21991
+ file_storage_key: zod_default.string().describe("Storage key/path where the file is stored"),
21992
+ workspace_id: zod_default.string().describe("ID of the workspace this file belongs to"),
21993
+ size: zod_default.number().nullish().describe("Byte size of the stored object (null until backfilled)"),
21994
+ created_at: zod_default.string().describe("Timestamp when file was uploaded"),
21995
+ metadata: zod_default.unknown().describe("Additional metadata about the file"),
21996
+ data: zod_default.unknown().describe("File-type-specific data (e.g., {width, height, orientation} for images)")
21997
+ });
21998
+ var fileUploadErrorSchema = zod_default.object({
21999
+ filename: zod_default.string().describe("Original filename that failed to upload"),
22000
+ error: zod_default.string().describe("Error message describing the failure reason")
22001
+ });
22002
+ var fileUploadResponseSchema = zod_default.object({
22003
+ files: zod_default.array(processedFileSchema).describe("Successfully uploaded files"),
22004
+ errors: zod_default.array(fileUploadErrorSchema).describe("Files that failed to upload with error details")
22005
+ });
22006
+ var fileFieldValueSchema = zod_default.object({
22007
+ id: zod_default.string().describe("Unique identifier for the file"),
22008
+ /**
22009
+ * Present on a STORED cell and on its way off responses.
22010
+ *
22011
+ * Optional here because this schema types both, and a response is the half
22012
+ * that loses it — no client reads it, and one that required it would reject
22013
+ * the first response without it. Nothing weakens at write time: a file cell
22014
+ * is written from file IDS and `resolveFileFieldIds` mints this from the
22015
+ * `files` row, so a caller never supplies it and a stored cell always has it.
22016
+ * Code that derives a serving URL guards for it (`file_url_resolver.ts`).
22017
+ */
22018
+ file_storage_key: zod_default.string().optional().describe("Storage key for the file"),
22019
+ filename: zod_default.string().describe("Original filename"),
22020
+ mime_type: zod_default.string().describe("MIME type of the file"),
22021
+ url: zod_default.string().optional().describe("Public URL for the file, resolved at serving time"),
22022
+ thumbnail_url: zod_default.string().optional().describe("Thumbnail URL for image files, resolved at serving time"),
22023
+ preview_url: zod_default.string().optional().describe("Preview URL for legacy Excel files, resolved at serving time"),
22024
+ // Immutable file-object metadata, resolved at serving time from the `files`
22025
+ // row (never stored on the denormalized cell) — like `url`. `size` may be
22026
+ // absent for older files not yet backfilled.
22027
+ size: zod_default.number().nullish().describe("Byte size, resolved at serving time from the file object (null/absent until backfilled)"),
22028
+ created_at: zod_default.string().optional().describe("Upload timestamp, resolved at serving time from the file object"),
22029
+ document_template_id: zod_default.string().optional().describe("The document template the file was generated from, resolved at serving time from the file object; absent on a file a person brought")
22030
+ });
22031
+ var servedFileSchema = zod_default.object({
22032
+ id: zod_default.string(),
22033
+ filename: zod_default.string(),
22034
+ mime_type: zod_default.string(),
22035
+ url: zod_default.string().optional().describe("Serving URL, signed for 24 hours."),
22036
+ thumbnail_url: zod_default.string().optional().describe("Signed thumbnail, for an image."),
22037
+ preview_url: zod_default.string().optional(),
22038
+ size: zod_default.number().nullish().describe("Bytes, where the object records it."),
22039
+ created_at: zod_default.string().optional().describe("When the file was uploaded."),
22040
+ document_template_id: zod_default.string().optional().describe("The document template the file was generated from.")
22041
+ });
22042
+ var SERVED_FILE_KEYS = servedFileSchema.keyof().options;
22043
+ var MAX_UPLOAD_FILE_SIZE = 2 * 1024 * 1024 * 1024;
22044
+ var PROFILE_IMAGE_MAX_SIZE = 10 * 1024 * 1024;
22045
+ var UPLOAD_TICKET_TTL_SECONDS = 5 * 60;
22046
+ var MAX_TICKET_UPLOAD_BYTES = 20 * 1024 * 1024;
22047
+ var uploadTicketRequestSchema = zod_default.object({
22048
+ filename: zod_default.string().min(1),
22049
+ mime_type: zod_default.string().optional(),
22050
+ /** Declared up front so an oversize file is refused before its bytes move,
22051
+ * rather than after the transfer is already spent. Optional because a caller
22052
+ * streaming from a pipe may genuinely not know; the PUT enforces the cap
22053
+ * either way. */
22054
+ size_bytes: zod_default.number().int().positive().optional()
22055
+ });
22056
+ var uploadTicketResponseSchema = zod_default.object({
22057
+ upload_url: zod_default.string(),
22058
+ ticket_id: zod_default.string(),
22059
+ method: zod_default.literal("PUT"),
22060
+ expires_at: zod_default.string(),
22061
+ max_bytes: zod_default.number(),
22062
+ required_headers: zod_default.record(zod_default.string(), zod_default.string())
22063
+ });
22064
+ var uploadTicketResultSchema = zod_default.object({
22065
+ file_id: zod_default.string(),
22066
+ file_name: zod_default.string(),
22067
+ mime_type: zod_default.string(),
22068
+ size_bytes: zod_default.number(),
22069
+ sha256: zod_default.string(),
22070
+ /** True when these bytes were already in the workspace and no second copy was
22071
+ * stored. Stated rather than left to inference, because the caller then gets
22072
+ * back a `file_name` it did not send — the stored file's, not the one it
22073
+ * uploaded under — and a silent difference reads as a bug. */
22074
+ deduplicated: zod_default.boolean()
22075
+ });
22076
+ var fileSizeSchema = zod_default.number().int().positive().max(MAX_UPLOAD_FILE_SIZE);
22077
+ var directUploadInitRequestSchema = zod_default.object({
22078
+ file_id: zod_default.string().optional(),
22079
+ filename: zod_default.string(),
22080
+ mime_type: zod_default.string(),
22081
+ file_size: fileSizeSchema
22082
+ });
22083
+ var directUploadInitResponseSchema = zod_default.object({
22084
+ file_id: zod_default.string(),
22085
+ file_storage_key: zod_default.string(),
22086
+ upload_url: zod_default.string()
22087
+ });
22088
+ var directUploadCompleteRequestSchema = zod_default.object({
22089
+ file_id: zod_default.string(),
22090
+ file_storage_key: zod_default.string(),
22091
+ filename: zod_default.string(),
22092
+ mime_type: zod_default.string(),
22093
+ original_file_id: zod_default.string().optional().describe(
22094
+ "When set, this upload becomes a new version of the original file. The server resolves the current latest, links the new row via source_file_id, and bumps the original's latest_version_id pointer."
22095
+ )
22096
+ });
22097
+ var multipartInitRequestSchema = zod_default.object({
22098
+ file_id: zod_default.string().optional(),
22099
+ filename: zod_default.string(),
22100
+ mime_type: zod_default.string(),
22101
+ file_size: fileSizeSchema,
22102
+ network_effective_type: zod_default.enum(["slow-2g", "2g", "3g", "4g"]).optional(),
22103
+ network_rtt_ms: zod_default.number().nonnegative().optional()
22104
+ });
22105
+ var multipartInitResponseSchema = zod_default.object({
22106
+ file_id: zod_default.string(),
22107
+ upload_id: zod_default.string(),
22108
+ file_storage_key: zod_default.string(),
22109
+ part_size: zod_default.number(),
22110
+ parts: zod_default.array(
22111
+ zod_default.object({
22112
+ part_number: zod_default.number(),
22113
+ url: zod_default.string()
22114
+ })
22115
+ )
22116
+ });
22117
+ var multipartCompleteRequestSchema = zod_default.object({
22118
+ file_id: zod_default.string(),
22119
+ upload_id: zod_default.string(),
22120
+ file_storage_key: zod_default.string(),
22121
+ filename: zod_default.string(),
22122
+ mime_type: zod_default.string(),
22123
+ parts: zod_default.array(
22124
+ zod_default.object({
22125
+ part_number: zod_default.number(),
22126
+ etag: zod_default.string()
22127
+ })
22128
+ ),
22129
+ original_file_id: zod_default.string().optional().describe(
22130
+ "When set, this upload becomes a new version of the original file. The server resolves the current latest, links the new row via source_file_id, and bumps the original's latest_version_id pointer."
22131
+ )
22132
+ });
22133
+ var multipartAbortRequestSchema = zod_default.object({
22134
+ upload_id: zod_default.string(),
22135
+ file_storage_key: zod_default.string()
22136
+ });
22137
+ var fileVersionEntrySchema = zod_default.object({
22138
+ id: zod_default.string(),
22139
+ filename: zod_default.string(),
22140
+ /**
22141
+ * Resolved by the server, because deriving it needs the storage key and a
22142
+ * caller that derives is a caller that must be handed one. Every other file
22143
+ * shape already resolves its URL at serving time; this one did not, which is
22144
+ * the whole reason the key still travels here.
22145
+ */
22146
+ url: zod_default.string(),
22147
+ /** @deprecated Unread by any client — resolve bytes through `url` or `id`. */
22148
+ file_storage_key: zod_default.string(),
22149
+ mime_type: zod_default.string(),
22150
+ created_at: zod_default.string(),
22151
+ is_current: zod_default.boolean(),
22152
+ source_file_id: zod_default.string().nullable()
22153
+ });
22154
+ var fileVersionListResponseSchema = zod_default.object({
22155
+ original_file_id: zod_default.string(),
22156
+ current_version_id: zod_default.string(),
22157
+ versions: zod_default.array(fileVersionEntrySchema).describe("Ordered newest \u2192 oldest. Index 0 is the current version.")
22158
+ });
22159
+
21967
22160
  // ../../node_modules/date-fns/constants.js
21968
22161
  var daysInYear = 365.2425;
21969
22162
  var maxTime = Math.pow(10, 8) * 24 * 60 * 60 * 1e3;
@@ -25281,16 +25474,16 @@ function conditionValueIssue(type, operator, value, fieldKey) {
25281
25474
  return null;
25282
25475
  }
25283
25476
  }
25284
- function conditionIssues(node, path15, fieldMap) {
25477
+ function conditionIssues(node, path16, fieldMap) {
25285
25478
  if (!node || typeof node !== "object") {
25286
- return [`${path15}: expected condition object, got ${typeof node}`];
25479
+ return [`${path16}: expected condition object, got ${typeof node}`];
25287
25480
  }
25288
25481
  const obj = node;
25289
25482
  if (obj.type === "locked") {
25290
25483
  const validOps2 = VALID_OPERATORS.locked;
25291
25484
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25292
25485
  return [
25293
- `${path15}: invalid operator '${String(obj.operator)}' for locked filter. Valid: ${validOps2.join(", ")}`
25486
+ `${path16}: invalid operator '${String(obj.operator)}' for locked filter. Valid: ${validOps2.join(", ")}`
25294
25487
  ];
25295
25488
  }
25296
25489
  return [];
@@ -25299,12 +25492,12 @@ function conditionIssues(node, path15, fieldMap) {
25299
25492
  const validOps2 = VALID_OPERATORS.current_member;
25300
25493
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25301
25494
  return [
25302
- `${path15}: invalid operator '${String(obj.operator)}' for current_member filter. Valid: ${validOps2.join(", ")}`
25495
+ `${path16}: invalid operator '${String(obj.operator)}' for current_member filter. Valid: ${validOps2.join(", ")}`
25303
25496
  ];
25304
25497
  }
25305
25498
  if (!Array.isArray(obj.value) || obj.value.length === 0 || !obj.value.every((v) => typeof v === "string" && v.length > 0)) {
25306
25499
  return [
25307
- `${path15}: current_member '${obj.operator}' expects a non-empty string[] of group IDs.`
25500
+ `${path16}: current_member '${obj.operator}' expects a non-empty string[] of group IDs.`
25308
25501
  ];
25309
25502
  }
25310
25503
  return [];
@@ -25313,38 +25506,38 @@ function conditionIssues(node, path15, fieldMap) {
25313
25506
  const validOps2 = VALID_OPERATORS.record_id;
25314
25507
  if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
25315
25508
  return [
25316
- `${path15}: invalid operator '${String(obj.operator)}' for record_id filter. Valid: ${validOps2.join(", ")}`
25509
+ `${path16}: invalid operator '${String(obj.operator)}' for record_id filter. Valid: ${validOps2.join(", ")}`
25317
25510
  ];
25318
25511
  }
25319
25512
  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.`];
25513
+ return [`${path16}: record_id '${obj.operator}' expects a non-empty string[] of record ids.`];
25321
25514
  }
25322
25515
  return [];
25323
25516
  }
25324
25517
  if (typeof obj.field_key !== "string" || !obj.field_key) {
25325
- return [`${path15}: field_key must be a non-empty string`];
25518
+ return [`${path16}: field_key must be a non-empty string`];
25326
25519
  }
25327
25520
  let filterType;
25328
25521
  if (fieldMap) {
25329
25522
  filterType = fieldMap.get(obj.field_key);
25330
25523
  if (filterType === void 0) {
25331
25524
  const available = Array.from(fieldMap.keys()).join(", ");
25332
- return [`${path15}: unknown field_key '${obj.field_key}'. Available: ${available}`];
25525
+ return [`${path16}: unknown field_key '${obj.field_key}'. Available: ${available}`];
25333
25526
  }
25334
25527
  } else if (typeof obj.type === "string") {
25335
25528
  filterType = obj.type;
25336
25529
  }
25337
25530
  if (filterType === "button") {
25338
- return [`${path15}: a button field holds no data and cannot be filtered.`];
25531
+ return [`${path16}: a button field holds no data and cannot be filtered.`];
25339
25532
  }
25340
25533
  if (typeof filterType !== "string" || !VALID_FILTER_TYPES.includes(filterType)) {
25341
25534
  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(", ")}`];
25535
+ return [`${path16}: invalid filter type ${named}. Valid: ${VALID_FILTER_TYPES.join(", ")}`];
25343
25536
  }
25344
25537
  const validOps = VALID_OPERATORS[filterType];
25345
25538
  if (typeof obj.operator !== "string" || !validOps.includes(obj.operator)) {
25346
25539
  return [
25347
- `${path15}: invalid operator '${String(obj.operator)}' for ${filterType} filter. Valid: ${validOps.join(", ")}`
25540
+ `${path16}: invalid operator '${String(obj.operator)}' for ${filterType} filter. Valid: ${validOps.join(", ")}`
25348
25541
  ];
25349
25542
  }
25350
25543
  if (obj.unit_option !== void 0 && (filterType !== "number" || typeof obj.unit_option !== "string")) {
@@ -25355,8 +25548,8 @@ function conditionIssues(node, path15, fieldMap) {
25355
25548
  const valueIssue = conditionValueIssue(filterType, obj.operator, obj.value, obj.field_key);
25356
25549
  return valueIssue ? [valueIssue] : [];
25357
25550
  }
25358
- function validateFilterCondition(condition, path15 = "filter") {
25359
- return conditionIssues(condition, path15, void 0);
25551
+ function validateFilterCondition(condition, path16 = "filter") {
25552
+ return conditionIssues(condition, path16, void 0);
25360
25553
  }
25361
25554
  function inheritedFilterType(type, format2, fallback) {
25362
25555
  switch (type) {
@@ -25546,11 +25739,11 @@ function perRowRefusals(value) {
25546
25739
  ...currencyField.map((message2) => ({ key: "currency_field", message: message2 }))
25547
25740
  ];
25548
25741
  }
25549
- function figureIssue(path15) {
25742
+ function figureIssue(path16) {
25550
25743
  return (value, ctx) => {
25551
25744
  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] });
25745
+ if (refused !== void 0) ctx.addIssue({ code: "custom", message: refused, path: [...path16, "unit"] });
25746
+ for (const { key, message: message2 } of perRowRefusals(value)) ctx.addIssue({ code: "custom", message: message2, path: [...path16, key] });
25554
25747
  };
25555
25748
  }
25556
25749
  var tableNumberFieldSchema = tableFieldBaseSchema.extend({
@@ -28030,9 +28223,9 @@ __export(expression_object_exports, {
28030
28223
  pick: () => pick2,
28031
28224
  values: () => values
28032
28225
  });
28033
- function getNestedValue(obj, path15) {
28226
+ function getNestedValue(obj, path16) {
28034
28227
  if (obj == null) return void 0;
28035
- const parts = path15.split(".");
28228
+ const parts = path16.split(".");
28036
28229
  let value = obj;
28037
28230
  for (const part of parts) {
28038
28231
  if (value == null) return void 0;
@@ -28071,11 +28264,11 @@ function entries(obj) {
28071
28264
  }
28072
28265
  return Object.entries(obj);
28073
28266
  }
28074
- function get(obj, path15, defaultValue) {
28075
- if (typeof path15 !== "string") {
28267
+ function get(obj, path16, defaultValue) {
28268
+ if (typeof path16 !== "string") {
28076
28269
  throw new Error("get: path must be a string");
28077
28270
  }
28078
- const value = getNestedValue(obj, path15);
28271
+ const value = getNestedValue(obj, path16);
28079
28272
  return value !== void 0 ? value : defaultValue;
28080
28273
  }
28081
28274
  function pick2(obj, fields) {
@@ -31453,12 +31646,12 @@ function parseFixtureDateExpression(value) {
31453
31646
  function isFixtureScalarValue(value) {
31454
31647
  return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
31455
31648
  }
31456
- function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors) {
31649
+ function checkTraversalFilter(node, path16, contextEntity, fieldByEntity, errors) {
31457
31650
  if (contextEntity === null) {
31458
31651
  errors.push({
31459
31652
  severity: "error",
31460
31653
  rule: "model.filter",
31461
- path: path15,
31654
+ path: path16,
31462
31655
  message: `traversal filter "${node.path[0]}" resolves only directly over a from_table`
31463
31656
  });
31464
31657
  return;
@@ -31470,7 +31663,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31470
31663
  errors.push({
31471
31664
  severity: "error",
31472
31665
  rule: "model.filter",
31473
- path: path15,
31666
+ path: path16,
31474
31667
  message: `traversal path[${index}] "${hop}" is not an entity.field alias`
31475
31668
  });
31476
31669
  return;
@@ -31479,7 +31672,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31479
31672
  errors.push({
31480
31673
  severity: "error",
31481
31674
  rule: "model.filter",
31482
- path: path15,
31675
+ path: path16,
31483
31676
  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
31677
  });
31485
31678
  return;
@@ -31489,7 +31682,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31489
31682
  errors.push({
31490
31683
  severity: "error",
31491
31684
  rule: "model.filter",
31492
- path: path15,
31685
+ path: path16,
31493
31686
  message: `traversal path[${index}] "${hop}" is not a field on entity "${parsed2.entity}"`
31494
31687
  });
31495
31688
  return;
@@ -31498,7 +31691,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31498
31691
  errors.push({
31499
31692
  severity: "error",
31500
31693
  rule: "model.filter",
31501
- path: path15,
31694
+ path: path16,
31502
31695
  message: `traversal path[${index}] "${hop}" is a ${field.type} field, not select_record_link`
31503
31696
  });
31504
31697
  return;
@@ -31512,7 +31705,7 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31512
31705
  errors.push({
31513
31706
  severity: "error",
31514
31707
  rule: "model.filter",
31515
- path: path15,
31708
+ path: path16,
31516
31709
  message: `traversal condition "${innerField}" must be an entity.field alias on "${current}", the entity its path lands on`
31517
31710
  });
31518
31711
  return;
@@ -31522,12 +31715,12 @@ function checkTraversalFilter(node, path15, contextEntity, fieldByEntity, errors
31522
31715
  errors.push({
31523
31716
  severity: "error",
31524
31717
  rule: "model.filter",
31525
- path: path15,
31718
+ path: path16,
31526
31719
  message: `traversal condition "${innerField}" is not a field on entity "${parsed.entity}"`
31527
31720
  });
31528
31721
  return;
31529
31722
  }
31530
- checkConditionShape(node.condition, parsed.entity, target, path15, fieldByEntity, errors);
31723
+ checkConditionShape(node.condition, parsed.entity, target, path16, fieldByEntity, errors);
31531
31724
  }
31532
31725
  function contractFilterType(entityAlias, field, fieldByEntity, hops = 0) {
31533
31726
  if (field.type === "formula") return formulaResultType(field.formula);
@@ -31550,7 +31743,7 @@ function formulaSelectOptions(entityAlias, field, fieldByEntity, hops = 0) {
31550
31743
  const shown = fieldByEntity.get(source.target_entity)?.get(field.lookup_field_alias);
31551
31744
  return shown === void 0 ? void 0 : formulaSelectOptions(source.target_entity, shown, fieldByEntity, hops + 1);
31552
31745
  }
31553
- function selectWordRefusals(entityAlias, expression, fieldByEntity, path15) {
31746
+ function selectWordRefusals(entityAlias, expression, fieldByEntity, path16) {
31554
31747
  const words = new Set([...expression.matchAll(/(["'])((?:(?!\1).)*)\1/g)].map((match2) => match2[2]));
31555
31748
  if (words.size === 0) return [];
31556
31749
  return parseFieldRefTokens(expression).flatMap((token2) => {
@@ -31564,19 +31757,19 @@ function selectWordRefusals(entityAlias, expression, fieldByEntity, path15) {
31564
31757
  {
31565
31758
  severity: "error",
31566
31759
  rule: "field.formula",
31567
- path: path15,
31760
+ path: path16,
31568
31761
  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
31762
  }
31570
31763
  ];
31571
31764
  });
31572
31765
  }
31573
- function formulaOptionRefusals(field, path15) {
31766
+ function formulaOptionRefusals(field, path16) {
31574
31767
  const { options, output_type: stated } = field.formula;
31575
31768
  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' }] : [];
31769
+ 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
31770
  }
31578
31771
  const refusals = [];
31579
- const refuse = (message2) => refusals.push({ severity: "error", rule: "field.formula", path: path15, message: message2 });
31772
+ const refuse = (message2) => refusals.push({ severity: "error", rule: "field.formula", path: path16, message: message2 });
31580
31773
  if (stated !== void 0 && stated !== "select") refuse(`states output_type "${stated}" beside options \u2014 a formula with options yields one of them`);
31581
31774
  const figure = ["format", "currency", "unit", "unit_field", "currency_field"].filter((key) => field.formula[key] !== void 0);
31582
31775
  if (figure.length > 0) refuse(`states ${figure.join(", ")} beside options \u2014 a formula yielding an option is no figure`);
@@ -31596,12 +31789,12 @@ function formulaOptionRefusals(field, path15) {
31596
31789
  }
31597
31790
  return refusals;
31598
31791
  }
31599
- function checkConditionShape(condition, entityAlias, field, path15, fieldByEntity, errors) {
31792
+ function checkConditionShape(condition, entityAlias, field, path16, fieldByEntity, errors) {
31600
31793
  const type = contractFilterType(entityAlias, field, fieldByEntity);
31601
31794
  if (type === void 0 && condition.type === void 0) return;
31602
31795
  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 });
31796
+ for (const message2 of validateFilterCondition(typed, path16)) {
31797
+ errors.push({ severity: "error", rule: "model.filter", path: path16, message: message2.startsWith(`${path16}: `) ? message2.slice(path16.length + 2) : message2 });
31605
31798
  }
31606
31799
  if (field.type !== "select") return;
31607
31800
  const declared = new Set(field.options.map((option) => option.alias));
@@ -31611,20 +31804,20 @@ function checkConditionShape(condition, entityAlias, field, path15, fieldByEntit
31611
31804
  errors.push({
31612
31805
  severity: "error",
31613
31806
  rule: "model.filter",
31614
- path: path15,
31807
+ path: path16,
31615
31808
  message: `filters field "${field.alias}" on option "${raw}", which that field does not declare (${declared.size > 0 ? [...declared].join(", ") : "it declares none"})`
31616
31809
  });
31617
31810
  }
31618
31811
  }
31619
- function checkFilterFields(filters, path15, contextEntity, fieldByEntity, errors, checkField) {
31812
+ function checkFilterFields(filters, path16, contextEntity, fieldByEntity, errors, checkField) {
31620
31813
  if (filters.node_type === "group") {
31621
31814
  for (const child of filters.children) {
31622
- checkFilterFields(child, path15, contextEntity, fieldByEntity, errors, checkField);
31815
+ checkFilterFields(child, path16, contextEntity, fieldByEntity, errors, checkField);
31623
31816
  }
31624
31817
  return;
31625
31818
  }
31626
31819
  if (filters.node_type === "traversal") {
31627
- checkTraversalFilter(filters, path15, contextEntity, fieldByEntity, errors);
31820
+ checkTraversalFilter(filters, path16, contextEntity, fieldByEntity, errors);
31628
31821
  return;
31629
31822
  }
31630
31823
  if (filters.type === "current_member") return;
@@ -31632,7 +31825,7 @@ function checkFilterFields(filters, path15, contextEntity, fieldByEntity, errors
31632
31825
  checkField(filters.field_key);
31633
31826
  if (contextEntity === null) return;
31634
31827
  const field = fieldByEntity.get(contextEntity)?.get(filters.field_key);
31635
- if (field !== void 0) checkConditionShape(filters, contextEntity, field, path15, fieldByEntity, errors);
31828
+ if (field !== void 0) checkConditionShape(filters, contextEntity, field, path16, fieldByEntity, errors);
31636
31829
  }
31637
31830
  function findDuplicates(aliases) {
31638
31831
  const seen = /* @__PURE__ */ new Set();
@@ -31647,19 +31840,19 @@ function checkUniqueTuples(entity, fields) {
31647
31840
  const errors = [];
31648
31841
  const seen = /* @__PURE__ */ new Set();
31649
31842
  for (const [index, tuple2] of (entity.unique ?? []).entries()) {
31650
- const path15 = `entities.${entity.alias}.unique.${index}`;
31843
+ const path16 = `entities.${entity.alias}.unique.${index}`;
31651
31844
  const key = [...new Set(tuple2)].sort().join("+");
31652
31845
  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` });
31846
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `names a field twice \u2014 a set of fields names each once` });
31654
31847
  }
31655
31848
  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` });
31849
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `states the set ${tuple2.join(" + ")} again \u2014 once is the rule` });
31657
31850
  }
31658
31851
  seen.add(key);
31659
31852
  for (const alias2 of tuple2) {
31660
31853
  const field = fields.get(alias2);
31661
31854
  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}"` });
31855
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `names "${alias2}", which is not a field of "${entity.alias}"` });
31663
31856
  continue;
31664
31857
  }
31665
31858
  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 +31860,14 @@ function checkUniqueTuples(entity, fields) {
31667
31860
  errors.push({
31668
31861
  severity: "error",
31669
31862
  rule: "entity.unique",
31670
- path: path15,
31863
+ path: path16,
31671
31864
  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
31865
  });
31673
31866
  }
31674
31867
  }
31675
31868
  const [only] = tuple2;
31676
31869
  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` });
31870
+ errors.push({ severity: "error", rule: "entity.unique", path: path16, message: `is "${only}" alone \u2014 declare \`unique: true\` on the text field itself` });
31678
31871
  }
31679
31872
  }
31680
31873
  return errors;
@@ -32567,8 +32760,8 @@ var MODEL_RULES = {
32567
32760
  "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
32761
  "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
32762
  };
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 });
32763
+ var error51 = (rule, path16, message2) => ({ severity: "error", path: path16, message: message2, rule });
32764
+ var note = (rule, path16, message2) => ({ severity: "note", path: path16, message: message2, rule });
32572
32765
 
32573
32766
  // ../shared/src/schemas/app_spec_version.ts
32574
32767
  var APP_SPEC_VERSION = 13;
@@ -35073,9 +35266,9 @@ function templateFields(content) {
35073
35266
  }
35074
35267
 
35075
35268
  // ../shared/src/schemas/app_model_validate_parts.ts
35076
- function checkOwnRows(model, entity, field, path15) {
35269
+ function checkOwnRows(model, entity, field, path16) {
35077
35270
  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}" }\``)];
35271
+ 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
35272
  }
35080
35273
  function checkWhen(entities, entity, when, at2, place) {
35081
35274
  const findings = [];
@@ -35118,21 +35311,21 @@ function checkNarrowing(entities, entity, narrowing, at2) {
35118
35311
  return [...mine, ...checkWhen(entities, entity, conditions, at2, "offer")];
35119
35312
  }
35120
35313
  var MADE_FROM = ["html", "excel"];
35121
- function checkTemplate(model, template, path15) {
35314
+ function checkTemplate(model, template, path16) {
35122
35315
  if (model.templates === void 0) return [];
35123
35316
  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`)];
35317
+ if (declared === void 0) return [error51("model.names-declared", path16, `names template "${template}", which this model does not declare`)];
35125
35318
  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`)];
35319
+ 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
35320
  }
35128
35321
  return [];
35129
35322
  }
35130
- function checkExportTemplate(model, entity, tabs, template, path15) {
35131
- const kind = checkTemplate(model, template, path15);
35323
+ function checkExportTemplate(model, entity, tabs, template, path16) {
35324
+ const kind = checkTemplate(model, template, path16);
35132
35325
  if (kind.length > 0 || model.templates === void 0) return kind;
35133
35326
  const reports = APP_EXPORT_REPORT_KEYS;
35134
35327
  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`)];
35328
+ return [error51("register.export-template", path16, `lists its rows under "${entity}", a key the report fills itself (${reports.join(", ")}) \u2014 rename the entity`)];
35136
35329
  }
35137
35330
  const fed = [...reports, entity, ...tabs];
35138
35331
  const content = templateContent(model, template);
@@ -35141,8 +35334,8 @@ function checkExportTemplate(model, entity, tabs, template, path15) {
35141
35334
  (app) => isDashboardApp(app) ? [] : (app.acts ?? []).flatMap((act) => [act.template, ...(act.templates ?? []).map((one) => one.template)].includes(template) ? [`${app.alias}.${act.alias}`] : [])
35142
35335
  );
35143
35336
  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`)]
35337
+ ...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(", ")}`)],
35338
+ ...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
35339
  ];
35147
35340
  }
35148
35341
 
@@ -35192,9 +35385,9 @@ function checkReading(model, reading, at2) {
35192
35385
  const entity = entityOf(model, alias2);
35193
35386
  if (entity === void 0) return [error51("model.names-declared", `${at2}.${key}`, `names entity "${alias2}", which this model does not declare`)];
35194
35387
  const findings = [];
35195
- const own = (name, path15) => {
35388
+ const own = (name, path16) => {
35196
35389
  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}"`));
35390
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
35198
35391
  return found;
35199
35392
  };
35200
35393
  if ("value" in reading && reading.value !== void 0) {
@@ -35231,31 +35424,31 @@ function checkReading(model, reading, at2) {
35231
35424
  if (refused !== void 0) findings.push(error51("reading.over", `${at2}.over`, `"${reading.over}" ${refused}`));
35232
35425
  }
35233
35426
  for (const [name, held] of Object.entries(reading.where ?? {})) {
35234
- const path15 = `${at2}.where.${name}`;
35235
- const named = own(name, path15);
35427
+ const path16 = `${at2}.where.${name}`;
35428
+ const named = own(name, path16);
35236
35429
  if (named === void 0) continue;
35237
35430
  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`));
35431
+ 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
35432
  continue;
35240
35433
  }
35241
35434
  const read = readField(model, entity, named);
35242
35435
  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`));
35436
+ 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
35437
  continue;
35245
35438
  }
35246
35439
  const { field, entity: holder } = read;
35247
35440
  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`));
35441
+ 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
35442
  continue;
35250
35443
  }
35251
35444
  const options = contractSelectOptions(field);
35252
35445
  if (options === void 0) {
35253
- findings.push(error51("reading.where", path15, `"${name}" is a ${field.type} field \u2014 options keep rows by a select`));
35446
+ findings.push(error51("reading.where", path16, `"${name}" is a ${field.type} field \u2014 options keep rows by a select`));
35254
35447
  continue;
35255
35448
  }
35256
35449
  const declared = new Set(options.map((option) => option.alias));
35257
35450
  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}"`));
35451
+ if (!declared.has(option)) findings.push(error51("model.names-declared", `${path16}.${index}`, `names "${option}", which is not an option of "${name}"`));
35259
35452
  }
35260
35453
  }
35261
35454
  if ("list" in reading) {
@@ -35279,18 +35472,18 @@ function checkMilestones(entities, entity, display, at2) {
35279
35472
  const findings = [];
35280
35473
  const seen = /* @__PURE__ */ new Set();
35281
35474
  for (const [index, stage] of stages.entries()) {
35282
- const path15 = `${at2}.status.milestones.${index}`;
35475
+ const path16 = `${at2}.status.milestones.${index}`;
35283
35476
  const date5 = fieldOf(entity, stage.field);
35284
35477
  if (date5 === void 0) {
35285
- findings.push(error51("model.names-declared", path15, `names "${stage.field}", which is not a field of "${entity.alias}"`));
35478
+ findings.push(error51("model.names-declared", path16, `names "${stage.field}", which is not a field of "${entity.alias}"`));
35286
35479
  continue;
35287
35480
  }
35288
35481
  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`));
35482
+ 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
35483
  }
35291
- if (seen.has(stage.field)) findings.push(error51("records.milestones", path15, `"${stage.field}" is already a milestone \u2014 each date marks one stage`));
35484
+ if (seen.has(stage.field)) findings.push(error51("records.milestones", path16, `"${stage.field}" is already a milestone \u2014 each date marks one stage`));
35292
35485
  seen.add(stage.field);
35293
- findings.push(...checkWhen(entities, entity, stage.when, `${path15}.when`, "stage"));
35486
+ findings.push(...checkWhen(entities, entity, stage.when, `${path16}.when`, "stage"));
35294
35487
  }
35295
35488
  for (const [index, closed] of (display.status?.closed ?? []).entries()) {
35296
35489
  if (!seen.has(closed)) findings.push(error51("records.milestones", `${at2}.status.closed.${index}`, `names "${closed}", which is not one of the milestones`));
@@ -35301,11 +35494,11 @@ function checkMilestones(entities, entity, display, at2) {
35301
35494
  }
35302
35495
  return findings;
35303
35496
  }
35304
- function checkCreateMilestones(model, entity, asked, path15) {
35497
+ function checkCreateMilestones(model, entity, asked, path16) {
35305
35498
  const stages = milestonesOf(recordDisplay(model, entity)) ?? [];
35306
35499
  const later = new Set(stages.slice(1).map((stage) => stage.field));
35307
35500
  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`)] : []
35501
+ (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
35502
  );
35310
35503
  }
35311
35504
 
@@ -35316,9 +35509,9 @@ function checkAppRow(model, app, at2) {
35316
35509
  const display = model.records?.[app.entity];
35317
35510
  if (row === void 0 || entity === void 0 || display === void 0) return [];
35318
35511
  const findings = [];
35319
- const field = (name, path15) => {
35512
+ const field = (name, path16) => {
35320
35513
  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}"`));
35514
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
35322
35515
  return found;
35323
35516
  };
35324
35517
  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 +35542,13 @@ function checkAppRow(model, app, at2) {
35349
35542
  }
35350
35543
  function checkApplies(model, entity, display, at2) {
35351
35544
  return Object.entries(display.applies ?? {}).flatMap(([name, conditions]) => {
35352
- const path15 = `${at2}.applies.${name}`;
35545
+ const path16 = `${at2}.applies.${name}`;
35353
35546
  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`)] : [];
35547
+ if (applying === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
35548
+ 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`)];
35549
+ 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
35550
  const held = Object.entries(conditions).flatMap(([condition, value]) => {
35358
- const at3 = `${path15}.${condition}`;
35551
+ const at3 = `${path16}.${condition}`;
35359
35552
  if (condition === name) return [error51("records.applies", at3, `"${name}" applies by itself \u2014 a field applies by another of the row's`)];
35360
35553
  const shape = appliesCondition(model, entity, condition);
35361
35554
  if ("refused" in shape) return [error51(shape.refused.startsWith("names") ? "model.names-declared" : "records.applies", at3, shape.refused)];
@@ -35378,9 +35571,9 @@ function checkRecords(model) {
35378
35571
  findings.push(error51("model.names-declared", at2, `names entity "${alias2}", which this model does not declare`));
35379
35572
  continue;
35380
35573
  }
35381
- const field = (name, path15) => {
35574
+ const field = (name, path16) => {
35382
35575
  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}"`));
35576
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${alias2}"`));
35384
35577
  return found;
35385
35578
  };
35386
35579
  const valueType = (found) => resolvedFieldType(found, entity, model.entities);
@@ -35450,14 +35643,14 @@ function checkRecords(model) {
35450
35643
  }
35451
35644
  }
35452
35645
  for (const [name, start] of Object.entries(display.starts ?? {})) {
35453
- const path15 = `${at2}.starts.${name}`;
35454
- const started = field(name, path15);
35646
+ const path16 = `${at2}.starts.${name}`;
35647
+ const started = field(name, path16);
35455
35648
  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`));
35649
+ if (!isAsked(started)) findings.push(error51("records.starts", path16, `"${name}" is a ${started.type} field nobody writes, so an add never starts it`));
35457
35650
  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`));
35651
+ 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
35652
  } 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`));
35653
+ 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
35654
  }
35462
35655
  }
35463
35656
  for (const [index, name] of (display.due ?? []).entries()) {
@@ -35656,30 +35849,30 @@ function optionsWhereConditions(filter2, target) {
35656
35849
  }
35657
35850
  return out.length === 0 ? void 0 : out;
35658
35851
  }
35659
- function checkBoundRef(entity, field, ref, byAlias, path15) {
35852
+ function checkBoundRef(entity, field, ref, byAlias, path16) {
35660
35853
  const entities = [...byAlias.values()];
35661
35854
  let on = entity;
35662
35855
  if ("link" in ref) {
35663
35856
  const link = entity.fields.find((one) => one.alias === ref.link);
35664
35857
  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`)];
35858
+ 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
35859
  }
35667
35860
  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`)];
35861
+ if (target === void 0) return [error51("model.names-declared", path16, `reads "${ref.link}", whose entity "${link.target_entity}" this model does not declare`)];
35669
35862
  on = target;
35670
35863
  }
35671
35864
  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`)];
35865
+ if (source === void 0) return [error51("model.names-declared", path16, `names "${ref.field}", which is not a field of ${on.label}`)];
35866
+ if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds", path16, `bounds ${field.label} by itself`)];
35674
35867
  const from = resolvedFieldType(source, on, entities);
35675
35868
  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`)];
35869
+ 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
35870
  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)];
35871
+ return apart === void 0 ? [] : [error51("write.bounds", path16, apart)];
35679
35872
  }
35680
- function checkSame(entity, field, same, byAlias, path15) {
35873
+ function checkSame(entity, field, same, byAlias, path16) {
35681
35874
  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`)];
35875
+ return [error51("write.same", path16, `${field.label} is a ${field.type} field \u2014 \`same\` narrows the rows a LINK points at`)];
35683
35876
  }
35684
35877
  const target = byAlias.get(field.target_entity);
35685
35878
  if (target === void 0) return [];
@@ -35687,7 +35880,7 @@ function checkSame(entity, field, same, byAlias, path15) {
35687
35880
  return same.flatMap((alias2, index) => {
35688
35881
  const own = oneLink(entity, alias2);
35689
35882
  const theirs = oneLink(target, alias2);
35690
- const at2 = `${path15}.${index}`;
35883
+ const at2 = `${path16}.${index}`;
35691
35884
  if (own === void 0) return [error51("write.same", at2, `"${alias2}" is not a one-row link of ${entity.label}`)];
35692
35885
  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
35886
  if (own.type !== "select_record_link" || theirs.type !== "select_record_link" || own.target_entity !== theirs.target_entity) {
@@ -35696,14 +35889,14 @@ function checkSame(entity, field, same, byAlias, path15) {
35696
35889
  return [];
35697
35890
  });
35698
35891
  }
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`)];
35892
+ function checkSuggest(entity, field, stated, byAlias, path16) {
35893
+ 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
35894
  const ref = suggestRef(stated);
35702
35895
  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`)];
35896
+ if (catalog === void 0) return [error51("model.names-declared", path16, `names entity "${ref.entity}", which this model does not declare`)];
35704
35897
  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`)];
35898
+ if (source === void 0) return [error51("model.names-declared", path16, `names "${ref.field}", which is not a field of ${catalog.label}`)];
35899
+ 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
35900
  return [];
35708
35901
  }
35709
35902
  function checkNoOverlap(entity, rule, at2) {
@@ -35764,50 +35957,50 @@ function checkWriteRules(entities, rules) {
35764
35957
  }
35765
35958
  }
35766
35959
  for (const [fieldAlias2, rule] of Object.entries(entry.fields ?? {})) {
35767
- const path15 = `${at2}.fields.${fieldAlias2}`;
35960
+ const path16 = `${at2}.fields.${fieldAlias2}`;
35768
35961
  const field = fieldByAlias.get(fieldAlias2);
35769
35962
  if (field === void 0) {
35770
- findings.push(error51("model.names-declared", path15, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
35963
+ findings.push(error51("model.names-declared", path16, `names "${fieldAlias2}", which is not a field of ${entity.label}`));
35771
35964
  continue;
35772
35965
  }
35773
35966
  if (rule.default_from !== void 0) {
35774
- findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path15}.default_from`));
35967
+ findings.push(...checkDefaultFrom(entity, field, rule.default_from, byAlias, `${path16}.default_from`));
35775
35968
  }
35776
35969
  const bounded = resolvedFieldType(field, entity, entities);
35777
35970
  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`));
35971
+ findings.push(error51("write.bounds", path16, `${field.label} is a ${field.type} field \u2014 \`min\`/\`max\` bound a number or a date`));
35779
35972
  }
35780
35973
  for (const side of ["min", "max"]) {
35781
35974
  const bound = rule[side];
35782
35975
  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`));
35976
+ 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
35977
  }
35785
35978
  const perRow = typeof bound === "number" && bound !== 0 ? contractUnitSource(field, entity, entities) : void 0;
35786
35979
  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}`));
35980
+ 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
35981
  }
35789
35982
  const ref = boundRef(bound);
35790
- if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path15}.${side}`));
35983
+ if (ref !== void 0) findings.push(...checkBoundRef(entity, field, ref, byAlias, `${path16}.${side}`));
35791
35984
  }
35792
35985
  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`));
35986
+ findings.push(error51("write.bounds", `${path16}.min`, `is above \`max\` (${rule.min} > ${rule.max}) \u2014 the bound refuses every figure`));
35794
35987
  }
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`));
35988
+ if (rule.same !== void 0) findings.push(...checkSame(entity, field, rule.same, byAlias, `${path16}.same`));
35989
+ if (rule.suggest !== void 0) findings.push(...checkSuggest(entity, field, rule.suggest, byAlias, `${path16}.suggest`));
35797
35990
  if (rule.options_where === void 0) continue;
35798
35991
  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`));
35992
+ 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
35993
  continue;
35801
35994
  }
35802
35995
  const target = byAlias.get(field.target_entity);
35803
35996
  if (target === void 0) continue;
35804
35997
  const conditions = optionsWhereConditions(rule.options_where, target);
35805
35998
  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("; ")}`));
35999
+ 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
36000
  continue;
35808
36001
  }
35809
36002
  for (const condition of conditions) {
35810
- const at3 = `${path15}.options_where`;
36003
+ const at3 = `${path16}.options_where`;
35811
36004
  const perRow = condition.type === "number" ? contractUnitSource(condition.field, target, entities) : void 0;
35812
36005
  if (perRow !== void 0) {
35813
36006
  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 +36024,42 @@ function checkWriteRules(entities, rules) {
35831
36024
  }
35832
36025
  return findings;
35833
36026
  }
35834
- function checkDefaultFrom(entity, field, stated, byAlias, path15) {
36027
+ function checkDefaultFrom(entity, field, stated, byAlias, path16) {
35835
36028
  const entities = [...byAlias.values()];
35836
36029
  const [linkAlias, sourceAlias] = stated.split(".");
35837
36030
  const link = entity.fields.find((one) => one.alias === linkAlias);
35838
36031
  if (link === void 0 || link.type !== "select_record_link") {
35839
36032
  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`)
36033
+ 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
36034
  ];
35842
36035
  }
35843
36036
  if (link.cardinality !== "one") {
35844
36037
  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`)
36038
+ 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
36039
  ];
35847
36040
  }
35848
36041
  const target = byAlias.get(link.target_entity);
35849
36042
  const source = target?.fields.find((one) => one.alias === sourceAlias);
35850
36043
  if (target === void 0 || source === void 0) {
35851
36044
  return [
35852
- error51("model.names-declared", path15, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
36045
+ error51("model.names-declared", path16, `names "${sourceAlias}", which is not a field of entity "${link.target_entity}"`)
35853
36046
  ];
35854
36047
  }
35855
36048
  const from = resolvedFieldType(source, target, entities);
35856
36049
  const into = resolvedFieldType(field, entity, entities);
35857
36050
  if (from !== into) {
35858
36051
  return [
35859
- error51("write.default-from", path15, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
36052
+ error51("write.default-from", path16, `copies ${target.label}'s "${source.label}" (${String(from)}) into a ${String(into)} field`)
35860
36053
  ];
35861
36054
  }
35862
36055
  if (source.type !== "select" || field.type !== "select") return [];
35863
36056
  const findings = [];
35864
36057
  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`));
36058
+ 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
36059
  }
35867
36060
  const missing = source.options.filter((option) => !field.options.some((one) => one.alias === option.alias));
35868
36061
  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`));
36062
+ 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
36063
  }
35871
36064
  return findings;
35872
36065
  }
@@ -36870,28 +37063,28 @@ var RECORDS_INPUT = "record_ids";
36870
37063
  var TEMPLATES_INPUT = "templates";
36871
37064
 
36872
37065
  // ../shared/src/schemas/app_model_validate_acts.ts
36873
- function checkChildCheck(model, check2, children, acts, path15) {
37066
+ function checkChildCheck(model, check2, children, acts, path16) {
36874
37067
  const child = check2.of === void 0 ? void 0 : entityOf(model, check2.of);
36875
37068
  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`)];
37069
+ 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
37070
  }
36878
37071
  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}"`)];
37072
+ if (field === void 0) return [error51("model.names-declared", `${path16}.field`, `names "${check2.field}", which is not a field of "${child.alias}"`)];
36880
37073
  const result = field.type === "formula" ? resolvedFieldType(field, child, model.entities) : void 0;
36881
37074
  const findings = [];
36882
37075
  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`));
37076
+ 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
37077
  }
36885
- findings.push(...checkRestatesMeter(model, child, check2, path15));
37078
+ findings.push(...checkRestatesMeter(model, child, check2, path16));
36886
37079
  if (check2.blocks !== void 0 && check2.blocks !== "all") {
36887
37080
  for (const [index, target] of check2.blocks.entries()) {
36888
37081
  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"`));
37082
+ 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
37083
  }
36891
37084
  }
36892
37085
  return findings;
36893
37086
  }
36894
- function checkRestatesMeter(model, entity, check2, path15) {
37087
+ function checkRestatesMeter(model, entity, check2, path16) {
36895
37088
  const display = model.records?.[entity.alias];
36896
37089
  const figure = display?.figure;
36897
37090
  const bound = figure === void 0 ? void 0 : display?.limits?.[figure];
@@ -36904,7 +37097,7 @@ function checkRestatesMeter(model, entity, check2, path15) {
36904
37097
  return [
36905
37098
  error51(
36906
37099
  "check.restates-meter",
36907
- `${path15}.field`,
37100
+ `${path16}.field`,
36908
37101
  `"${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
37102
  )
36910
37103
  ];
@@ -36929,9 +37122,9 @@ function checkAct(model, entity, act, at2) {
36929
37122
  if (act.on === "view" && (act.requires ?? []).length > 0) {
36930
37123
  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
37124
  }
36932
- const own = (name, path15) => {
37125
+ const own = (name, path16) => {
36933
37126
  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}"`));
37127
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
36935
37128
  return found;
36936
37129
  };
36937
37130
  findings.push(...checkWhen(model.entities, entity, act.when, `${at2}.when`, "offer"));
@@ -36973,17 +37166,17 @@ function checkAct(model, entity, act, at2) {
36973
37166
  }
36974
37167
  function checkTemplates(model, entity, act, at2) {
36975
37168
  const templates = act.templates ?? [];
36976
- const path15 = `${at2}.templates`;
37169
+ const path16 = `${at2}.templates`;
36977
37170
  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`"));
37171
+ 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"));
37172
+ 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`)"));
37173
+ 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"));
37174
+ 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
37175
  if (act.asks !== void 0) {
36983
37176
  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
37177
  }
36985
37178
  for (const [index, one] of templates.entries()) {
36986
- const each = `${path15}.${index}`;
37179
+ const each = `${path16}.${index}`;
36987
37180
  findings.push(...checkTemplate(model, one.template, `${each}.template`), ...checkWhen(model.entities, entity, one.when, `${each}.when`, "offer"));
36988
37181
  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
37182
  }
@@ -37084,53 +37277,53 @@ function checkIntake(model, app, act, at2) {
37084
37277
  if (act.fills === void 0) findings.push(error51("act.intake", at2, "reads papers and fills nothing \u2014 name what they fill in `fills`"));
37085
37278
  findings.push(...checkPapers(model, app, record2, act.intake, `${at2}.intake`));
37086
37279
  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));
37280
+ const path16 = `${at2}.fills.${index}`;
37281
+ if ((act.fills ?? []).indexOf(name) !== index) findings.push(error51("act.fills", path16, `names "${name}" twice \u2014 each is filled once`));
37282
+ findings.push(...checkFill(model, app, record2, name, path16));
37090
37283
  }
37091
37284
  return findings;
37092
37285
  }
37093
- function checkPapers(model, app, record2, name, path15) {
37286
+ function checkPapers(model, app, record2, name, path16) {
37094
37287
  const child = entityOf(model, name);
37095
- if (child === void 0) return [error51("model.names-declared", path15, `names "${name}", which this model does not declare`)];
37288
+ if (child === void 0) return [error51("model.names-declared", path16, `names "${name}", which this model does not declare`)];
37096
37289
  const title = model.records?.[child.alias]?.title;
37097
37290
  const kind = title === void 0 ? void 0 : fieldOf(child, title);
37098
37291
  const block = recordBlocks(app.record).find((one) => isRowsBlock(one) && one.rows === child.alias && one.expect !== void 0 && one.expect === title);
37099
37292
  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>"\``)];
37293
+ 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
37294
  }
37102
37295
  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\``));
37296
+ 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
37297
  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`));
37298
+ 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
37299
  const link = childLink(child, record2.alias, block.via);
37107
37300
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37108
37301
  const filled = ["link" in link ? link.link : "", kind.alias, ...files.map((field) => field.alias), ...starts];
37109
37302
  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`));
37303
+ 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
37304
  }
37112
37305
  return findings;
37113
37306
  }
37114
- function checkFill(model, app, record2, name, path15) {
37307
+ function checkFill(model, app, record2, name, path16) {
37115
37308
  const [head, tail] = name.split(".");
37116
37309
  if (tail !== void 0) {
37117
37310
  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}"`)];
37311
+ if (link2 === void 0) return [error51("model.names-declared", path16, `names "${head}", which is not a field of "${record2.alias}"`)];
37119
37312
  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`)];
37313
+ 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
37314
  }
37122
37315
  const party = entityOf(model, link2.target_entity);
37123
37316
  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);
37317
+ if (!peopleWrite(party)) return [error51("act.fills", path16, `"${party.alias}" rows are written by no person, so no paper fills one`)];
37318
+ return fillable(model, party, tail, path16);
37126
37319
  }
37127
37320
  const listed = recordBlocks(app.record).filter((block2) => isRowsBlock(block2) && block2.rows === name);
37128
- if (listed.length === 0) return fillable(model, record2, name, path15);
37321
+ if (listed.length === 0) return fillable(model, record2, name, path16);
37129
37322
  const child = entityOf(model, name);
37130
37323
  const block = listed.find((one) => one.expect === void 0 && one.under === void 0 && one.where === void 0 && one.create !== false);
37131
37324
  if (child === void 0) return [];
37132
37325
  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`)];
37326
+ 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
37327
  }
37135
37328
  const link = childLink(child, record2.alias, block.via);
37136
37329
  if (!("link" in link)) return [];
@@ -37141,7 +37334,7 @@ function checkFill(model, app, record2, name, path15) {
37141
37334
  });
37142
37335
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37143
37336
  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`)
37337
+ (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
37338
  );
37146
37339
  }
37147
37340
  var RECORD_KEYS = /* @__PURE__ */ new Set(["alias", "label", "when", "requires", "record", "fills"]);
@@ -37151,25 +37344,25 @@ function checkRecord(model, app, act, at2) {
37151
37344
  const record2 = entityOf(model, app.entity);
37152
37345
  if (record2 === void 0) return [];
37153
37346
  const findings = [];
37154
- const path15 = `${at2}.record`;
37347
+ const path16 = `${at2}.record`;
37155
37348
  for (const key of Object.keys(act).filter((one) => !RECORD_KEYS.has(one))) {
37156
37349
  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
37350
  }
37158
37351
  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`)];
37352
+ if (child === void 0) return [...findings, error51("model.names-declared", `${path16}.into`, `names "${recorded.into}", which this model does not declare`)];
37160
37353
  const listing = recordBlocks(app.record).filter((one) => (isRowsBlock(one) ? one.rows : "timeline" in one ? one.timeline : void 0) === child.alias);
37161
37354
  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`)];
37355
+ 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
37356
  }
37164
37357
  const block = listing.find((one) => !isRowsBlock(one) || one.expect === void 0 && one.under === void 0 && one.where === void 0);
37165
37358
  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`)];
37359
+ 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
37360
  }
37168
37361
  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)];
37362
+ if ("refused" in link) return [...findings, error51("act.record", `${path16}.into`, link.refused)];
37170
37363
  const back = fieldOf(child, link.link);
37171
37364
  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`));
37365
+ 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
37366
  }
37174
37367
  const kinds = [
37175
37368
  { key: "audio", types: ["files"], what: "a files field" },
@@ -37184,16 +37377,16 @@ function checkRecord(model, app, act, at2) {
37184
37377
  if (name === void 0) continue;
37185
37378
  const field = fieldOf(child, name);
37186
37379
  if (field === void 0) {
37187
- findings.push(error51("model.names-declared", `${path15}.${key}`, `names "${name}", which is not a field of "${child.alias}"`));
37380
+ findings.push(error51("model.names-declared", `${path16}.${key}`, `names "${name}", which is not a field of "${child.alias}"`));
37188
37381
  continue;
37189
37382
  }
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`));
37383
+ 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}`));
37384
+ 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
37385
  written.push(name);
37193
37386
  }
37194
37387
  const starts = Object.keys(model.records?.[child.alias]?.starts ?? {});
37195
37388
  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`));
37389
+ 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
37390
  }
37198
37391
  for (const [index, name] of (act.fills ?? []).entries()) {
37199
37392
  const filled = `${at2}.fills.${index}`;
@@ -37206,11 +37399,11 @@ function checkRecord(model, app, act, at2) {
37206
37399
  }
37207
37400
  return findings;
37208
37401
  }
37209
- function fillable(model, entity, name, path15) {
37402
+ function fillable(model, entity, name, path16) {
37210
37403
  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}"`)];
37404
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
37212
37405
  const why = unfillable(model, entity, field);
37213
- return why === void 0 ? [] : [error51("act.fills", path15, `"${entity.alias}.${name}" is ${why}`)];
37406
+ return why === void 0 ? [] : [error51("act.fills", path16, `"${entity.alias}.${name}" is ${why}`)];
37214
37407
  }
37215
37408
  function noteIntake(model, app, resolved, at2) {
37216
37409
  const record2 = entityOf(model, app.entity);
@@ -37233,12 +37426,12 @@ function noteIntake(model, app, resolved, at2) {
37233
37426
  if (intake === void 0) return [];
37234
37427
  const children = new Set(intake.rows.map((one) => one.entity));
37235
37428
  return (app.acts?.[index]?.fills ?? []).flatMap((name, order) => {
37236
- const path15 = `${at2}.acts.${index}.fills.${order}`;
37429
+ const path16 = `${at2}.acts.${index}.fills.${order}`;
37237
37430
  if (children.has(name)) {
37238
37431
  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`)];
37432
+ 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
37433
  }
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`)];
37434
+ 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
37435
  });
37243
37436
  });
37244
37437
  }
@@ -37250,7 +37443,7 @@ function offeredTogether2(one, other) {
37250
37443
  return held.some((option) => theirs.includes(option));
37251
37444
  });
37252
37445
  }
37253
- function checkFootNeeds(model, entity, section, drawn, act, path15) {
37446
+ function checkFootNeeds(model, entity, section, drawn, act, path16) {
37254
37447
  const asked = new Set((act.asks ?? []).filter((ask) => typeof ask === "string"));
37255
37448
  const writers = registerApps(model).flatMap((app) => (app.acts ?? []).filter((one) => actEntity(app, one) === entity.alias));
37256
37449
  const writtenBy = (one) => [...Object.keys(one.set ?? {}), ...(one.asks ?? []).filter((ask) => typeof ask === "string"), ...one.into === void 0 ? [] : [one.into]];
@@ -37258,7 +37451,7 @@ function checkFootNeeds(model, entity, section, drawn, act, path15) {
37258
37451
  return (act.requires ?? []).flatMap((name, index) => {
37259
37452
  const field = fieldOf(entity, name);
37260
37453
  if (asked.has(name) || field === void 0 || !isAsked(field) && !owned.has(name)) return [];
37261
- const at2 = `${path15}.requires.${index}`;
37454
+ const at2 = `${path16}.requires.${index}`;
37262
37455
  if (!drawn.has(name)) {
37263
37456
  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
37457
  }
@@ -38414,11 +38607,11 @@ function checkBookEntities(model, app, at2) {
38414
38607
  const seen = /* @__PURE__ */ new Set();
38415
38608
  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
38609
  return [...bookless, ...(app.books ?? []).flatMap((book, index) => {
38417
- const path15 = `${at2}.books.${index}.entity`;
38610
+ const path16 = `${at2}.books.${index}.entity`;
38418
38611
  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`));
38612
+ if (entityOf(model, book.entity) === void 0) findings.push(error51("model.names-declared", path16, `names entity "${book.entity}", which this model does not declare`));
38613
+ 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`));
38614
+ if (seen.has(book.entity)) findings.push(error51("app.books", path16, `names "${book.entity}" twice \u2014 each book is read once`));
38422
38615
  seen.add(book.entity);
38423
38616
  return findings;
38424
38617
  })];
@@ -38428,26 +38621,26 @@ function checkBookFields(model, app, book, reads, at2) {
38428
38621
  const entity = entityOf(model, book.entity);
38429
38622
  const stated = app.books?.[book.index];
38430
38623
  if (main2 === void 0 || entity === void 0 || stated === void 0) return [];
38431
- const path15 = `${at2}.books.${book.index}`;
38624
+ const path16 = `${at2}.books.${book.index}`;
38432
38625
  const read = /* @__PURE__ */ new Set([...reads, ...book.named]);
38433
38626
  const mappable = /* @__PURE__ */ new Set([...read, ...exportsBooks(app) ? exportPrinted(model, main2.alias) : []]);
38434
38627
  const findings = [];
38435
38628
  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}"`));
38629
+ 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}"`));
38630
+ else if (!mappable.has(alias2)) findings.push(error51("app.books", `${path16}.fields.${alias2}`, `maps "${alias2}", which the app does not read`));
38631
+ 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
38632
  }
38440
38633
  for (const alias2 of read) {
38441
38634
  const own = fieldOf(main2, alias2);
38442
38635
  const lined = book.lineUp(alias2);
38443
38636
  const field = lined === void 0 ? void 0 : fieldOf(entity, lined);
38444
38637
  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`));
38638
+ 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
38639
  const options = own === void 0 ? void 0 : contractSelectOptions(own);
38447
38640
  for (const option of field === void 0 ? [] : contractSelectOptions(field) ?? []) {
38448
38641
  const kin = options?.find((one) => one.alias !== option.alias && sameLabel(one.label, option.label));
38449
38642
  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}"`));
38643
+ findings.push(note("app.book-option-kin", path16, `holds "${option.label}" of "${alias2}" as "${option.alias}", which "${main2.alias}" holds as "${kin.alias}"`));
38451
38644
  }
38452
38645
  }
38453
38646
  }
@@ -38456,14 +38649,14 @@ function checkBookFields(model, app, book, reads, at2) {
38456
38649
  function sameLabel(a, b) {
38457
38650
  return a.trim().toLocaleLowerCase() === b.trim().toLocaleLowerCase();
38458
38651
  }
38459
- function checkReportBooks(model, app, report, path15) {
38652
+ function checkReportBooks(model, app, report, path16) {
38460
38653
  const named = report.books;
38461
38654
  const main2 = entityOf(model, app.entity);
38462
38655
  if (named === void 0 || main2 === void 0) return [];
38463
38656
  const findings = [];
38464
38657
  const seen = /* @__PURE__ */ new Set();
38465
38658
  for (const [index, name] of named.entries()) {
38466
- const at2 = `${path15}.books.${index}`;
38659
+ const at2 = `${path16}.books.${index}`;
38467
38660
  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
38661
  if (seen.has(name)) findings.push(error51("app.books", at2, `names "${name}" twice \u2014 each book is read once`));
38469
38662
  seen.add(name);
@@ -38475,7 +38668,7 @@ function checkReportBooks(model, app, report, path15) {
38475
38668
  return stated !== void 0 && entity !== void 0 && bookField(main2, entity, stated, alias2) !== void 0;
38476
38669
  };
38477
38670
  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`));
38671
+ 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
38672
  }
38480
38673
  return findings;
38481
38674
  }
@@ -38570,18 +38763,18 @@ function checkRegisterApp(model, app, at2) {
38570
38763
  function noteTasks(model, resolved, at2) {
38571
38764
  return Object.entries(resolved.tasks).flatMap(([entity, task]) => taskNote(model, entity, task, at2));
38572
38765
  }
38573
- function taskNote(model, entity, task, path15) {
38766
+ function taskNote(model, entity, task, path16) {
38574
38767
  if (task.close === void 0) {
38575
38768
  const status = model.records?.[entity]?.status?.field ?? "";
38576
38769
  return [
38577
38770
  note(
38578
38771
  "app.tasks",
38579
- path15,
38772
+ path16,
38580
38773
  `"${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
38774
  )
38582
38775
  ];
38583
38776
  }
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`)] : [];
38777
+ 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
38778
  }
38586
38779
  function checkReadsRows(model, reads, entities, at2) {
38587
38780
  const findings = [];
@@ -38646,7 +38839,7 @@ function checkCreates(model, resolved, at2) {
38646
38839
  ])
38647
38840
  )
38648
38841
  ];
38649
- return lists.flatMap(({ entity, asked, fills: filed, path: path15 }) => {
38842
+ return lists.flatMap(({ entity, asked, fills: filed, path: path16 }) => {
38650
38843
  const declared = entityOf(model, entity);
38651
38844
  if (declared === void 0) return [];
38652
38845
  const fills = [...filed, ...Object.keys(model.records?.[entity]?.starts ?? {})];
@@ -38656,16 +38849,16 @@ function checkCreates(model, resolved, at2) {
38656
38849
  return by === void 0 ? void 0 : `apps.${by.app}.acts.${by.act}.set.${name}`;
38657
38850
  };
38658
38851
  return [
38659
- ...checkCreateMilestones(model, entity, asked, path15),
38852
+ ...checkCreateMilestones(model, entity, asked, path16),
38660
38853
  // An act's outcome is the act's to write: an add stating it would decide what only the act decides. What the
38661
38854
  // add fills itself (the record, a document's lines) is no reader's statement.
38662
38855
  ...asked.filter((name) => !fills.includes(name)).flatMap((name) => {
38663
38856
  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`)];
38857
+ 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
38858
  }),
38666
38859
  ...requiredOnCreate(model, declared, fills).filter((name) => !asked.includes(name)).map((name) => {
38667
38860
  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`);
38861
+ 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
38862
  })
38670
38863
  ];
38671
38864
  });
@@ -38678,9 +38871,9 @@ function checkApp(model, app, at2) {
38678
38871
  if (display === void 0) {
38679
38872
  return [error51("app.records-entry", `${at2}.entity`, `lists "${entity.alias}", which has no \`records\` entry \u2014 say how its row is recognised first`)];
38680
38873
  }
38681
- const own = (name, path15) => {
38874
+ const own = (name, path16) => {
38682
38875
  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}"`));
38876
+ if (found === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
38684
38877
  return found;
38685
38878
  };
38686
38879
  const stages = new Set((milestonesOf(display) ?? []).map((stage) => stage.field));
@@ -38693,41 +38886,41 @@ function checkApp(model, app, at2) {
38693
38886
  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
38887
  }
38695
38888
  }
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`));
38889
+ const scoped = (name, path16, where) => {
38890
+ 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
38891
  };
38699
38892
  const rowParts = partsOf(display);
38700
38893
  const registerBounds = meterBounds(display, app.register?.columns ?? []);
38701
38894
  const columns = /* @__PURE__ */ new Map();
38702
38895
  for (const [index, name] of (app.register?.columns ?? []).entries()) {
38703
- const path15 = `${at2}.register.columns.${index}`;
38704
- const column = own(name, path15);
38896
+ const path16 = `${at2}.register.columns.${index}`;
38897
+ const column = own(name, path16);
38705
38898
  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`));
38899
+ 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
38900
  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");
38901
+ 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`));
38902
+ scoped(name, path16, "it lists");
38710
38903
  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`));
38904
+ if (first2 !== void 0) findings.push(error51("register.column-drawn", path16, `"${name}" is already column ${first2} \u2014 a field is one column`));
38712
38905
  else columns.set(name, index);
38713
38906
  }
38714
38907
  const splits = new Set((app.register?.readings ?? []).flatMap((reading) => "metric" in reading ? [] : pressFields(modelPressed(reading))));
38715
38908
  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");
38909
+ const path16 = `${at2}.register.filters.${index}`;
38910
+ findings.push(...checkFilter(model, entity, stated, path16, { owner: "a register", chips: display.status?.field, splits }));
38911
+ scoped(filterField(stated), path16, "it lists");
38719
38912
  }
38720
38913
  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");
38914
+ const path16 = `${at2}.register.search.${index}`;
38915
+ findings.push(...checkSearch(model, entity, name, path16));
38916
+ scoped(name, path16, "it lists");
38724
38917
  }
38725
38918
  findings.push(...checkOrder(entity, app.register?.sort, `${at2}.register.sort`));
38726
38919
  const tabs = app.register?.tabs ?? [];
38727
- const dated = (name, path15) => {
38728
- const field = own(name, path15);
38920
+ const dated = (name, path16) => {
38921
+ const field = own(name, path16);
38729
38922
  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`));
38923
+ 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
38924
  };
38732
38925
  if (tabs.length > 0 && !["table", "cards"].includes(app.register?.layout ?? "table")) {
38733
38926
  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 +38931,24 @@ function checkApp(model, app, at2) {
38738
38931
  const named = /* @__PURE__ */ new Set();
38739
38932
  const reportKeys = APP_EXPORT_REPORT_KEYS;
38740
38933
  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`));
38934
+ const path16 = `${at2}.register.tabs.${index}`;
38935
+ if (named.has(tab.tab)) findings.push(error51("register.tabs", `${path16}.tab`, `"${tab.tab}" names another tab already`));
38743
38936
  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`));
38937
+ findings.push(error51("register.tabs", `${path16}.tab`, `"${tab.tab}" is a key a report already holds \u2014 name the tab otherwise`));
38745
38938
  }
38746
38939
  named.add(tab.tab);
38747
- if (tab.in !== void 0) dated(tab.in, `${path15}.in`);
38940
+ if (tab.in !== void 0) dated(tab.in, `${path16}.in`);
38748
38941
  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`));
38942
+ dated(tab.open.from, `${path16}.open.from`);
38943
+ dated(tab.open.to, `${path16}.open.to`);
38944
+ 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
38945
  }
38753
- findings.push(...checkNarrowing(model.entities, entity, tab.where, `${path15}.where`));
38946
+ findings.push(...checkNarrowing(model.entities, entity, tab.where, `${path16}.where`));
38754
38947
  const status = display?.status?.field;
38755
38948
  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`));
38949
+ 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
38950
  }
38758
- findings.push(...checkOrder(entity, tab.sort, `${path15}.sort`));
38951
+ findings.push(...checkOrder(entity, tab.sort, `${path16}.sort`));
38759
38952
  }
38760
38953
  const group = app.register?.group;
38761
38954
  if (group !== void 0 && group === app.scope) {
@@ -38775,37 +38968,37 @@ function checkApp(model, app, at2) {
38775
38968
  }
38776
38969
  if (!peopleWrite(entity)) findings.push(error51("app.create-asks", `${at2}.register.create`, `adds rows to "${entity.alias}", which no person writes`));
38777
38970
  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`));
38971
+ const path16 = `${at2}.register.create.${index}`;
38972
+ const field = own(name, path16);
38973
+ if (field !== void 0 && !isAsked(field)) findings.push(error51("app.asked-written", path16, `asks "${name}", a ${field.type} field nobody writes`));
38974
+ if (field !== void 0) findings.push(...checkOwnRows(model, entity, field, path16));
38975
+ 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
38976
  }
38784
38977
  }
38785
38978
  findings.push(...checkReports(model, entity, app, display.status?.field, at2));
38786
38979
  for (const [index, reading] of (app.register?.readings ?? []).entries()) {
38787
- const path15 = `${at2}.register.readings.${index}`;
38980
+ const path16 = `${at2}.register.readings.${index}`;
38788
38981
  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`));
38982
+ 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
38983
  continue;
38791
38984
  }
38792
- findings.push(...checkReading(model, reading, path15));
38985
+ findings.push(...checkReading(model, reading, path16));
38793
38986
  const tab = "tab" in reading ? reading.tab : void 0;
38794
38987
  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(", ")}`));
38988
+ 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
38989
  }
38797
38990
  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`));
38991
+ 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
38992
  }
38800
38993
  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"));
38994
+ 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
38995
  }
38803
38996
  }
38804
38997
  const placed = /* @__PURE__ */ new Map();
38805
- const place = (name, where, path15) => {
38998
+ const place = (name, where, path16) => {
38806
38999
  const first2 = placed.get(name);
38807
39000
  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})`));
39001
+ 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
39002
  else placed.set(name, where);
38810
39003
  };
38811
39004
  place(display.title, "the header's title", `${at2}.record`);
@@ -38824,21 +39017,21 @@ function checkApp(model, app, at2) {
38824
39017
  }
38825
39018
  const inFacts = /* @__PURE__ */ new Map();
38826
39019
  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"));
39020
+ const path16 = `${at2}.record.sections.${index}`;
39021
+ findings.push(...checkWhen(model.entities, entity, section.when, `${path16}.when`, "offer"));
38829
39022
  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`));
39023
+ 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
39024
  }
38832
- findings.push(...checkSectionAt(entity, display, section.at, `${path15}.at`));
39025
+ findings.push(...checkSectionAt(entity, display, section.at, `${path16}.at`));
38833
39026
  for (const [order, name] of (section.fields ?? []).entries()) {
38834
- const fieldPath = `${path15}.fields.${order}`;
39027
+ const fieldPath = `${path16}.fields.${order}`;
38835
39028
  const fact = own(name, fieldPath);
38836
39029
  if (fact !== void 0) place(name, `the section "${section.title}"`, fieldPath);
38837
39030
  if (fact !== void 0) inFacts.set(name, fieldPath);
38838
39031
  if (fact !== void 0) findings.push(...checkOwnRows(model, entity, fact, fieldPath));
38839
39032
  }
38840
39033
  }
38841
- for (const [name, path15] of inFacts) {
39034
+ for (const [name, path16] of inFacts) {
38842
39035
  const fact = fieldOf(entity, name);
38843
39036
  if (fact?.type !== "lookup" || !(display.subtitle ?? []).includes(fact.source_field_alias)) continue;
38844
39037
  const link = fieldOf(entity, fact.source_field_alias);
@@ -38846,9 +39039,9 @@ function checkApp(model, app, at2) {
38846
39039
  const figure = target?.figure;
38847
39040
  const bound = figure === void 0 ? void 0 : target?.limits?.[figure];
38848
39041
  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`));
39042
+ 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
39043
  }
38851
- for (const [name, path15] of inFacts) {
39044
+ for (const [name, path16] of inFacts) {
38852
39045
  const fact = fieldOf(entity, name);
38853
39046
  if (fact?.type !== "lookup" || !inFacts.has(fact.source_field_alias)) continue;
38854
39047
  const link = fieldOf(entity, fact.source_field_alias);
@@ -38856,22 +39049,22 @@ function checkApp(model, app, at2) {
38856
39049
  if (target === void 0) continue;
38857
39050
  const part = fact.lookup_field_alias === target.title ? "title" : (target.subtitle ?? []).includes(fact.lookup_field_alias) ? "line under the title" : void 0;
38858
39051
  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`));
39052
+ 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
39053
  }
38861
39054
  const blocksPlaced = /* @__PURE__ */ new Map();
38862
39055
  for (const [index, section] of sections.entries()) {
38863
39056
  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"));
39057
+ const path16 = `${at2}.record.sections.${index}.blocks.${order}`;
39058
+ findings.push(...checkWhen(model.entities, entity, block.when, `${path16}.when`, "offer"));
38866
39059
  const listed = "timeline" in block ? block.timeline : isRowsBlock(block) ? block.rows : isAgendaBlock(block) ? block.agenda : void 0;
38867
39060
  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`));
39061
+ 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
39062
  }
38870
39063
  const identity = blockIdentity(block);
38871
39064
  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`));
39065
+ 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
39066
  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 ?? []));
39067
+ findings.push(...checkBlock(model, entity, block, path16, place, app.reads !== "shared", app.checks ?? []));
38875
39068
  if (!isRowsBlock(block)) continue;
38876
39069
  const child = entityOf(model, block.rows);
38877
39070
  const link = child === void 0 ? void 0 : childLink(child, entity.alias, block.via);
@@ -38894,34 +39087,34 @@ function checkApp(model, app, at2) {
38894
39087
  const children = new Set(recordChildren(model, entity.alias, recordBlocks(app.record)));
38895
39088
  const acts = /* @__PURE__ */ new Map();
38896
39089
  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`));
39090
+ const path16 = `${at2}.acts.${index}`;
39091
+ 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
39092
  acts.set(act.alias, act.of);
38900
39093
  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`));
39094
+ 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
39095
  continue;
38903
39096
  }
38904
39097
  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`));
39098
+ 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
39099
  }
38907
39100
  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\``));
39101
+ 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
39102
  }
38910
39103
  const on = entityOf(model, actEntity(app, act));
38911
39104
  if (on === void 0) continue;
38912
- findings.push(...checkAct(model, on, act, path15), ...checkIntake(model, app, act, path15), ...checkRecord(model, app, act, path15));
39105
+ findings.push(...checkAct(model, on, act, path16), ...checkIntake(model, app, act, path16), ...checkRecord(model, app, act, path16));
38913
39106
  for (const expected of expectedKeys(model, on.alias)) {
38914
39107
  const writes = [...Object.keys(act.set ?? {}), ...(act.asks ?? []).filter((ask) => typeof ask === "string")];
38915
39108
  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`));
39109
+ 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
39110
  }
38918
39111
  }
38919
- findings.push(...checkHistoryCopies(model, on, act, path15));
39112
+ findings.push(...checkHistoryCopies(model, on, act, path16));
38920
39113
  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"));
39114
+ 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
39115
  }
38923
39116
  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`"));
39117
+ 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
39118
  }
38926
39119
  }
38927
39120
  const actsPlaced = /* @__PURE__ */ new Map();
@@ -38939,20 +39132,20 @@ function checkApp(model, app, at2) {
38939
39132
  for (const [index, section] of sections.entries()) {
38940
39133
  const drawn = /* @__PURE__ */ new Set([...drawnBy(section), ...section.at === void 0 ? unstaged : []]);
38941
39134
  for (const [order, name] of (section.acts ?? []).entries()) {
38942
- const path15 = `${at2}.record.sections.${index}.acts.${order}`;
39135
+ const path16 = `${at2}.record.sections.${index}.acts.${order}`;
38943
39136
  const act = (app.acts ?? []).find((one) => one.alias === name);
38944
39137
  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));
39138
+ if (act === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not an act of this app`));
39139
+ 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`));
39140
+ 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`));
39141
+ else if (home !== void 0 && home.section !== index) findings.push(error51("record.move-home", path16, misplacedMove(act, sections[home.section]?.title ?? "", home.entry)));
39142
+ else if (home !== void 0 || isCorrectionAct(model, app, act)) findings.push(...checkFootNeeds(model, entity, section, drawn, act, path16));
39143
+ else findings.push(...checkStagedAct(display, section, act, path16), ...checkFootNeeds(model, entity, section, drawn, act, path16));
38951
39144
  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`));
39145
+ 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
39146
  }
38954
39147
  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})`));
39148
+ 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
39149
  else actsPlaced.set(name, `the section "${section.title}"`);
38957
39150
  }
38958
39151
  }
@@ -38973,27 +39166,27 @@ function checkApp(model, app, at2) {
38973
39166
  );
38974
39167
  }
38975
39168
  for (const [index, check2] of (app.checks ?? []).entries()) {
38976
- const path15 = `${at2}.checks.${index}`;
38977
- findings.push(...checkResolve(model, app, check2, `${path15}.resolve`));
39169
+ const path16 = `${at2}.checks.${index}`;
39170
+ findings.push(...checkResolve(model, app, check2, `${path16}.resolve`));
38978
39171
  if (check2.of !== void 0) {
38979
- findings.push(...checkChildCheck(model, check2, children, acts, path15));
39172
+ findings.push(...checkChildCheck(model, check2, children, acts, path16));
38980
39173
  continue;
38981
39174
  }
38982
- const field = own(check2.field, `${path15}.field`);
39175
+ const field = own(check2.field, `${path16}.field`);
38983
39176
  const result = field?.type === "formula" ? resolvedFieldType(field, entity, model.entities) : void 0;
38984
39177
  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`));
39178
+ 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
39179
  }
38987
39180
  const drawn = placed.get(check2.field);
38988
39181
  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`));
39182
+ 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
39183
  }
38991
- findings.push(...checkRestatesMeter(model, entity, check2, path15));
39184
+ findings.push(...checkRestatesMeter(model, entity, check2, path16));
38992
39185
  if (check2.blocks === void 0 || check2.blocks === "all") continue;
38993
39186
  for (const [at22, target] of check2.blocks.entries()) {
38994
39187
  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`));
39188
+ 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"`));
39189
+ 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
39190
  }
38998
39191
  }
38999
39192
  return findings;
@@ -39002,14 +39195,14 @@ function noteChildLists(model, app, at2) {
39002
39195
  const placed = (app.record?.sections ?? []).flatMap(
39003
39196
  (section, index) => (section.blocks ?? []).flatMap((block, order) => isRowsBlock(block) ? [{ block, path: `${at2}.record.sections.${index}.blocks.${order}` }] : [])
39004
39197
  );
39005
- const planned = placed.flatMap(({ block, path: path15 }) => {
39198
+ const planned = placed.flatMap(({ block, path: path16 }) => {
39006
39199
  const child = entityOf(model, block.rows);
39007
39200
  const axis = child === void 0 ? void 0 : agendaAxis(model, child);
39008
39201
  if (axis === void 0 || "refused" in axis || axis.time === void 0 && axis.part === void 0) return [];
39009
39202
  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`)];
39203
+ 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
39204
  });
39012
- const covering = placed.flatMap(({ block, path: path15 }) => {
39205
+ const covering = placed.flatMap(({ block, path: path16 }) => {
39013
39206
  if (!(entityOf(model, block.rows)?.fields.some((field) => field.type === "files") ?? false)) return [];
39014
39207
  if (Array.isArray(block.create)) return [];
39015
39208
  const covers = placed.find((other) => {
@@ -39022,7 +39215,7 @@ function noteChildLists(model, app, at2) {
39022
39215
  return !("refused" in shape) && shape.entity === block.rows;
39023
39216
  });
39024
39217
  });
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`)];
39218
+ 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
39219
  });
39027
39220
  return [...planned, ...covering];
39028
39221
  }
@@ -39036,10 +39229,10 @@ function checkDocumentsListed(model, entity, sections, at2) {
39036
39229
  return shape === void 0 || "refused" in shape ? [] : [[shape.entity, block.title ?? child?.label ?? block.rows]];
39037
39230
  })
39038
39231
  );
39039
- return placed.flatMap(({ block, path: path15 }) => {
39232
+ return placed.flatMap(({ block, path: path16 }) => {
39040
39233
  if (!isRowsBlock(block)) return [];
39041
39234
  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`)];
39235
+ 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
39236
  });
39044
39237
  }
39045
39238
  function hiddenWhenOffered(shown, offered) {
@@ -39050,20 +39243,20 @@ function hiddenWhenOffered(shown, offered) {
39050
39243
  return !held.some((option) => options.includes(option));
39051
39244
  });
39052
39245
  }
39053
- function checkFilter(model, entity, stated, path15, around) {
39246
+ function checkFilter(model, entity, stated, path16, around) {
39054
39247
  const name = filterField(stated);
39055
39248
  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}"`)];
39249
+ 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
39250
  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`)];
39251
+ if (name === around.chips) return [error51("register.filter", path16, `"${name}" is the status, already the register's chips \u2014 drop it`)];
39059
39252
  if (!FILTER_TYPES.has(type)) {
39060
39253
  const read = filter2.type === "lookup" ? readField(model, entity, filter2) : void 0;
39061
39254
  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`)];
39255
+ 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
39256
  }
39064
- if (typeof stated !== "string") return checkTiers(model, entity, filter2, type, `${path15}.tiers`);
39257
+ if (typeof stated !== "string") return checkTiers(model, entity, filter2, type, `${path16}.tiers`);
39065
39258
  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`)];
39259
+ 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
39260
  }
39068
39261
  return [];
39069
39262
  }
@@ -39071,36 +39264,36 @@ function checkOrder(entity, sort, base) {
39071
39264
  const findings = [];
39072
39265
  const sorted = /* @__PURE__ */ new Set();
39073
39266
  for (const [index, key] of registerSortKeys(sort).entries()) {
39074
- const path15 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
39267
+ const path16 = `${base}${Array.isArray(sort) ? `.${index}` : ""}.field`;
39075
39268
  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`));
39269
+ if (field === void 0) findings.push(error51("model.names-declared", path16, `names "${key.field}", which is not a field of "${entity.alias}"`));
39270
+ else if (!isSortableFieldType(field.type)) findings.push(error51("register.sort", path16, `"${key.field}" is a ${field.type} field, which holds no order`));
39271
+ 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
39272
  sorted.add(key.field);
39080
39273
  }
39081
39274
  return findings;
39082
39275
  }
39083
- function checkSearch(model, entity, name, path15) {
39276
+ function checkSearch(model, entity, name, path16) {
39084
39277
  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}"`)];
39278
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39086
39279
  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`)];
39280
+ 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
39281
  }
39089
- function checkGroup(model, entity, display, name, path15, around) {
39282
+ function checkGroup(model, entity, display, name, path16, around) {
39090
39283
  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}"`)];
39284
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39092
39285
  const type = resolvedFieldType(field, entity, model.entities) ?? field.type;
39093
39286
  const read = readField(model, entity, field);
39094
39287
  const held = read?.field ?? field;
39095
39288
  const several = read?.several === true || (held.type === "select" || held.type === "select_member") && held.multi === true;
39096
39289
  const many = held.type === "select_record_link" && held.cardinality !== "one";
39097
39290
  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`)];
39291
+ 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
39292
  }
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`)];
39293
+ if (name === around.chips) return [error51("register.group", path16, `"${name}" is the status, whose chips already split the rows \u2014 group by another field`)];
39294
+ 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
39295
  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)`)];
39296
+ 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
39297
  }
39105
39298
  return [];
39106
39299
  }
@@ -39122,16 +39315,16 @@ function likelySection(model, app, act, drawn) {
39122
39315
  const best = Math.max(0, ...scores);
39123
39316
  return best === 0 ? void 0 : scores.indexOf(best);
39124
39317
  }
39125
- function checkResolve(model, app, check2, path15) {
39318
+ function checkResolve(model, app, check2, path16) {
39126
39319
  if (check2.resolve === void 0) return [];
39127
39320
  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`)];
39321
+ if (act === void 0) return [error51("model.names-declared", path16, `names "${check2.resolve}", which is not an act of this app`)];
39129
39322
  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`)];
39323
+ 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
39324
  }
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`)];
39325
+ 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`)];
39326
+ 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`)];
39327
+ 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
39328
  return [];
39136
39329
  }
39137
39330
  function checkPages(door, sections, at2) {
@@ -39139,16 +39332,16 @@ function checkPages(door, sections, at2) {
39139
39332
  const addressed = /* @__PURE__ */ new Map();
39140
39333
  for (const [index, section] of sections.entries()) {
39141
39334
  if (section.page !== true) continue;
39142
- const path15 = `${at2}.sections.${index}.page`;
39335
+ const path16 = `${at2}.sections.${index}.page`;
39143
39336
  if (door === "drawer" || door === "beside") {
39144
39337
  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`));
39338
+ 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
39339
  }
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\``));
39340
+ 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
39341
  const segment = pageSegment(section.title);
39149
39342
  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`));
39343
+ if (segment === "") findings.push(error51("record.page", path16, `"${section.title}" holds no letter or digit to name its page's address \u2014 retitle it`));
39344
+ else if (first2 !== void 0) findings.push(error51("record.page", path16, `"${section.title}" and "${first2}" make one page address, "${segment}" \u2014 retitle one`));
39152
39345
  else addressed.set(segment, section.title);
39153
39346
  }
39154
39347
  if (sections.length > 0 && sections.every((section) => section.page === true)) {
@@ -39175,20 +39368,20 @@ function checkRecordLayout(app, at2) {
39175
39368
  }
39176
39369
  return findings;
39177
39370
  }
39178
- function checkSectionAt(entity, display, at2, path15) {
39371
+ function checkSectionAt(entity, display, at2, path16) {
39179
39372
  if (at2 === void 0) return [];
39180
39373
  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\``)];
39374
+ 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
39375
  const stages = milestonesOf(display);
39183
39376
  if (stages !== void 0) {
39184
39377
  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(", ")})`)]);
39378
+ 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
39379
  }
39187
39380
  const field = status.field === void 0 ? void 0 : fieldOf(entity, status.field);
39188
39381
  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 ?? ""}"`)]);
39382
+ return at2.flatMap((name) => options.has(name) ? [] : [error51("record.at", path16, `names "${name}", which is not an option of the status "${status.field ?? ""}"`)]);
39190
39383
  }
39191
- function checkStagedAct(display, section, act, path15) {
39384
+ function checkStagedAct(display, section, act, path16) {
39192
39385
  const at2 = section.at;
39193
39386
  const status = display.status;
39194
39387
  if (at2 === void 0 || status === void 0) return [];
@@ -39201,16 +39394,16 @@ function checkStagedAct(display, section, act, path15) {
39201
39394
  return before !== void 0 && at2.includes(before);
39202
39395
  });
39203
39396
  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}`)];
39397
+ 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
39398
  }
39206
39399
  const field = status.field;
39207
39400
  if (field === void 0) return [];
39208
39401
  const held = heldOptions(act.when, field);
39209
39402
  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}`)];
39403
+ 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
39404
  }
39212
39405
  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}`)];
39406
+ 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
39407
  }
39215
39408
  function blockIdentity(block) {
39216
39409
  if (isRowsBlock(block)) {
@@ -39224,20 +39417,20 @@ function blockIdentity(block) {
39224
39417
  }
39225
39418
  function checkWhere(child, block, at2) {
39226
39419
  return Object.entries(block.where ?? {}).flatMap(([name, options]) => {
39227
- const path15 = `${at2}.where.${name}`;
39420
+ const path16 = `${at2}.where.${name}`;
39228
39421
  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}"`)];
39422
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`)];
39230
39423
  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`)];
39424
+ 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
39425
  }
39233
39426
  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`)];
39427
+ 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
39428
  }
39236
39429
  const held = new Set((field.options ?? []).map((option) => option.alias));
39237
39430
  const stray = options.filter((option) => !held.has(option));
39238
39431
  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`)] : []
39432
+ ...stray.length === 0 ? [] : [error51("block.where", path16, `names ${stray.map((one) => `"${one}"`).join(", ")}, which "${name}" has no option of`)],
39433
+ ...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
39434
  ];
39242
39435
  });
39243
39436
  }
@@ -39248,19 +39441,19 @@ function checkChildColumns(model, child, names, back, expecting, at2) {
39248
39441
  const bounds = meterBounds(display, names);
39249
39442
  const columns = /* @__PURE__ */ new Map();
39250
39443
  for (const [index, name] of names.entries()) {
39251
- const path15 = `${at2}.columns.${index}`;
39444
+ const path16 = `${at2}.columns.${index}`;
39252
39445
  if (fieldOf(child, name) === void 0) {
39253
- findings.push(error51("model.names-declared", path15, `names "${name}", which is not a field of "${child.alias}"`));
39446
+ findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`));
39254
39447
  continue;
39255
39448
  }
39256
39449
  const part = rowParts.get(name);
39257
39450
  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`));
39451
+ 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
39452
  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`));
39453
+ 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`));
39454
+ 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
39455
  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`));
39456
+ if (first2 !== void 0) findings.push(error51("block.column-drawn", path16, `"${name}" is already column ${first2} \u2014 a field is one column`));
39264
39457
  else columns.set(name, index);
39265
39458
  }
39266
39459
  return findings;
@@ -39278,9 +39471,9 @@ function checkChildCreate(model, child, create, via, at2) {
39278
39471
  }
39279
39472
  return findings;
39280
39473
  }
39281
- function checkUnder(model, entity, child, block, path15) {
39474
+ function checkUnder(model, entity, child, block, path16) {
39282
39475
  if (block.under === void 0) return [];
39283
- const at2 = `${path15}.under`;
39476
+ const at2 = `${path16}.under`;
39284
39477
  const shape = documentUnder(model, entity.alias, child, block.under);
39285
39478
  if ("refused" in shape) return [error51("block.under", at2, shape.refused)];
39286
39479
  const findings = [];
@@ -39292,11 +39485,11 @@ function checkUnder(model, entity, child, block, path15) {
39292
39485
  for (const [index, name] of (block.columns ?? []).entries()) {
39293
39486
  const column = fieldOf(child, name);
39294
39487
  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\``));
39488
+ 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
39489
  continue;
39297
39490
  }
39298
39491
  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`));
39492
+ 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
39493
  }
39301
39494
  return findings;
39302
39495
  }
@@ -39314,22 +39507,22 @@ function checkRoster(model, entity, app, at2) {
39314
39507
  const layout = app.register?.layout;
39315
39508
  const roster = app.register?.roster;
39316
39509
  const expect = app.register?.expect;
39317
- const path15 = `${at2}.register.roster`;
39510
+ const path16 = `${at2}.register.roster`;
39318
39511
  if (roster === void 0) {
39319
39512
  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)`)] : [],
39513
+ ...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
39514
  ...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
39515
  ...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
39516
  ];
39324
39517
  }
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`)];
39518
+ 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
39519
  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
39520
  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
39521
  const shape = rosterShape(model, entity.alias, roster, expect === void 0);
39329
- if ("refused" in shape) return [error51("register.roster", path15, shape.refused)];
39522
+ if ("refused" in shape) return [error51("register.roster", path16, shape.refused)];
39330
39523
  const child = entityOf(model, shape.entity);
39331
39524
  if (child === void 0) return [];
39332
- return [...checkRosterColumns(entity, child, app, at2), ...app.reads === "shared" ? [] : unruledChild(model, entity, child, shape.via, path15)];
39525
+ return [...checkRosterColumns(entity, child, app, at2), ...app.reads === "shared" ? [] : unruledChild(model, entity, child, shape.via, path16)];
39333
39526
  }
39334
39527
  function checkRosterColumns(entity, child, app, at2) {
39335
39528
  const expect = app.register?.expect;
@@ -39354,16 +39547,16 @@ function checkLanes(model, entity, app, at2) {
39354
39547
  const layout = app.register?.layout;
39355
39548
  const lanes = app.register?.lanes;
39356
39549
  const loads = app.register?.loads;
39357
- const path15 = `${at2}.register.lanes`;
39550
+ const path16 = `${at2}.register.lanes`;
39358
39551
  if (lanes === void 0) {
39359
39552
  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)")] : [],
39553
+ ...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
39554
  ...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
39555
  ];
39363
39556
  }
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`)];
39557
+ 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
39558
  const shape = lanesShape(model, entity.alias, lanes);
39366
- if ("refused" in shape) return [error51("register.lanes", path15, shape.refused)];
39559
+ if ("refused" in shape) return [error51("register.lanes", path16, shape.refused)];
39367
39560
  if (loads === void 0) return [];
39368
39561
  const count2 = Object.keys(loads).length;
39369
39562
  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 +39565,22 @@ function checkLanes(model, entity, app, at2) {
39372
39565
  function checkGantt(model, entity, app, at2) {
39373
39566
  const layout = app.register?.layout;
39374
39567
  const gantt = app.register?.gantt;
39375
- const path15 = `${at2}.register.gantt`;
39568
+ const path16 = `${at2}.register.gantt`;
39376
39569
  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`)")] : [];
39570
+ 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
39571
  }
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`)];
39572
+ 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
39573
  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)) : [];
39574
+ return "refused" in shape ? shape.refused.map((one) => error51("register.gantt", one.at === "" ? path16 : `${path16}.${one.at}`, one.reason)) : [];
39382
39575
  }
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`)];
39576
+ function checkTiers(model, entity, field, type, path16) {
39577
+ if (type !== "number") return [error51("register.tiers", path16, `"${field.alias}" is a ${field.type} field \u2014 tiers band a number; name it bare`)];
39385
39578
  const select = contractFigureSelect(field, entity, model.entities);
39386
39579
  if (select === void 0) return [];
39387
39580
  return [
39388
39581
  error51(
39389
39582
  "register.tiers",
39390
- path15,
39583
+ path16,
39391
39584
  `"${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
39585
  )
39393
39586
  ];
@@ -39397,29 +39590,29 @@ function checkReports(model, entity, app, status, at2) {
39397
39590
  if (reports === void 0 || reports === true) return [];
39398
39591
  const named = /* @__PURE__ */ new Map();
39399
39592
  return reports.flatMap((report, index) => {
39400
- const path15 = `${at2}.register.export.${index}`;
39593
+ const path16 = `${at2}.register.export.${index}`;
39401
39594
  const first2 = named.get(report.label);
39402
39595
  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`)] : [];
39596
+ 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
39597
  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`),
39598
+ ...first2 === void 0 ? [] : [error51("register.export-report", `${path16}.label`, `names "${report.label}", as report ${first2} does \u2014 a menu entry is one report`)],
39599
+ ...report.template === void 0 ? [] : checkExportTemplate(model, entity.alias, (app.register?.tabs ?? []).map((tab) => tab.tab), report.template, `${path16}.template`),
39600
+ ...checkNarrowing(model.entities, entity, report.where, `${path16}.where`),
39408
39601
  ...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)
39602
+ ...report.per === void 0 ? [] : checkPer(model, entity, report.per, { status, scope: app.scope, fixed: report.where?.[report.per] !== void 0 }, `${path16}.per`),
39603
+ ...checkReportBooks(model, app, report, path16)
39411
39604
  ];
39412
39605
  });
39413
39606
  }
39414
- function checkPer(model, entity, name, around, path15) {
39607
+ function checkPer(model, entity, name, around, path16) {
39415
39608
  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}"`)];
39609
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`)];
39417
39610
  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`)] : [];
39611
+ 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\``)];
39612
+ 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`)];
39613
+ 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`)];
39614
+ 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`)];
39615
+ return around.fixed ? [error51("register.export-report", path16, `picks a value of "${name}", which its \`where\` already fixes \u2014 drop one`)] : [];
39423
39616
  }
39424
39617
  function checkRegisterNarrowing(model, entity, app, at2) {
39425
39618
  const where = app.register?.where;
@@ -39427,9 +39620,9 @@ function checkRegisterNarrowing(model, entity, app, at2) {
39427
39620
  const filters = (app.register?.filters ?? []).map(filterField);
39428
39621
  const findings = [...checkWhen(model.entities, entity, where, `${at2}.register.where`, "offer"), ...checkNarrowing(model.entities, entity, opens, `${at2}.register.opens`)];
39429
39622
  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`));
39623
+ const path16 = `${at2}.register.where.${name}`;
39624
+ 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\``));
39625
+ 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
39626
  const opened = opens?.[name];
39434
39627
  if (opened === void 0) continue;
39435
39628
  const stray = Array.isArray(opened) && Array.isArray(kept) ? opened.filter((option) => !kept.includes(option)) : void 0;
@@ -39457,10 +39650,10 @@ function checkFiltersShown(model, entity, app, at2) {
39457
39650
  ...(register.filters ?? []).map((stated, index) => ({ name: filterField(stated), path: `${at2}.register.filters.${index}`, says: unshown })),
39458
39651
  ...Object.entries(register.opens ?? {}).flatMap(([name, held]) => held === NARROW_ME ? [] : [{ name, path: `${at2}.register.opens.${name}`, says: unshown }]),
39459
39652
  ...(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 [];
39653
+ const path16 = `${at2}.register.readings.${index}`;
39654
+ if (readingEntity(reading) !== entity.alias || checkReading(model, reading, path16).length > 0) return [];
39462
39655
  const part = "metric" in reading ? "figure" : "pivot" in reading ? "cell" : "part";
39463
- return pressFields(modelPressed(reading)).map((name) => ({ name, path: path15, says: pressed(part) }));
39656
+ return pressFields(modelPressed(reading)).map((name) => ({ name, path: path16, says: pressed(part) }));
39464
39657
  })
39465
39658
  ];
39466
39659
  return unshownFilters(model, entity, narrowed, shown, fixed);
@@ -39480,11 +39673,11 @@ function rowShows(model, entity, display, columns, more) {
39480
39673
  ]);
39481
39674
  }
39482
39675
  function unshownFilters(model, entity, narrowed, shown, fixed) {
39483
- return narrowed.flatMap(({ name, path: path15, says }) => {
39676
+ return narrowed.flatMap(({ name, path: path16, says }) => {
39484
39677
  const field = fieldOf(entity, name);
39485
39678
  const type = field === void 0 ? void 0 : resolvedFieldType(field, entity, model.entities) ?? field.type;
39486
39679
  if (shown.has(name) || fixed.has(name) || type === void 0 || !FILTER_TYPES.has(type)) return [];
39487
- return [error51("register.filter-unshown", path15, says(name))];
39680
+ return [error51("register.filter-unshown", path16, says(name))];
39488
39681
  });
39489
39682
  }
39490
39683
  function checkBlock(model, entity, block, at2, place, scoped, checks) {
@@ -39555,11 +39748,11 @@ function checkBlock(model, entity, block, at2, place, scoped, checks) {
39555
39748
  }
39556
39749
  if ("files" in block) {
39557
39750
  for (const [index, name] of block.files.entries()) {
39558
- const path15 = `${at2}.files.${index}`;
39751
+ const path16 = `${at2}.files.${index}`;
39559
39752
  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);
39753
+ if (field === void 0) findings.push(error51("model.names-declared", path16, `names "${name}", which is not a field of "${entity.alias}"`));
39754
+ 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`));
39755
+ else place(name, FILES_BLOCK, path16);
39563
39756
  }
39564
39757
  return findings;
39565
39758
  }
@@ -39581,19 +39774,19 @@ function checkBlockNarrowing(model, child, block, back, checks, at2) {
39581
39774
  }
39582
39775
  const chips = isTaskBlock(model, block) ? display.status?.field : void 0;
39583
39776
  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`)] : [];
39777
+ 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
39778
  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));
39779
+ const path16 = `${at2}.filters.${index}`;
39780
+ findings.push(...checkFilter(model, child, stated, path16, { owner: "a rows block", chips, splits: /* @__PURE__ */ new Set() }), ...same(filterField(stated), path16));
39588
39781
  }
39589
39782
  for (const [index, name] of (block.search ?? []).entries()) findings.push(...checkSearch(model, child, name, `${at2}.search.${index}`));
39590
39783
  findings.push(...checkOrder(child, block.sort, `${at2}.sort`));
39591
39784
  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\``));
39785
+ const path16 = `${at2}.group`;
39786
+ const fixedGroup = same(block.group, path16);
39787
+ 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
39788
  else if (fixedGroup.length > 0) findings.push(...fixedGroup);
39596
- else findings.push(...checkGroup(model, child, display, block.group, path15, { chips, layout: "table" }));
39789
+ else findings.push(...checkGroup(model, child, display, block.group, path16, { chips, layout: "table" }));
39597
39790
  }
39598
39791
  const shown = rowShows(model, child, display, block.columns ?? [], [
39599
39792
  // A child row's line says the warnings standing on it.
@@ -39611,15 +39804,15 @@ function checkBlockEdits(model, child, block, at2) {
39611
39804
  const figure = recordDisplay(model, child.alias)?.figure;
39612
39805
  const shown = /* @__PURE__ */ new Set([...block.columns ?? [], ...figure === void 0 ? [] : [figure]]);
39613
39806
  return edits.flatMap((name, index) => {
39614
- const path15 = `${at2}.edits.${index}`;
39807
+ const path16 = `${at2}.edits.${index}`;
39615
39808
  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`)];
39809
+ if (field === void 0) return [error51("model.names-declared", path16, `names "${name}", which is not a field of "${child.alias}"`)];
39810
+ 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`)];
39811
+ 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
39812
  const long = field.type === "text" && field.format === "markdown";
39620
39813
  const several = field.type === "select" && field.multi === true;
39621
39814
  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`)];
39815
+ 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
39816
  }
39624
39817
  return [];
39625
39818
  });
@@ -39698,14 +39891,14 @@ var workspaceModelFileSchema = workspaceModelContractSchema.extend({
39698
39891
  }).strict();
39699
39892
  function modelIssueFindings(issues) {
39700
39893
  const unfold = (issue2, at2) => {
39701
- const path15 = [...at2, ...issue2.path];
39702
- if (issue2.code !== "invalid_union") return [{ path: path15, message: issue2.message }];
39894
+ const path16 = [...at2, ...issue2.path];
39895
+ if (issue2.code !== "invalid_union") return [{ path: path16, message: issue2.message }];
39703
39896
  const meant = issue2.errors.filter((arm) => !arm.some((one) => one.path.length === 0 && one.code === "invalid_type"));
39704
39897
  const fewest = Math.min(...meant.map((arm) => arm.length));
39705
39898
  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 }];
39899
+ return closest.length === 1 ? closest[0].flatMap((one) => unfold(one, path16)) : [{ path: path16, message: issue2.message }];
39707
39900
  };
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));
39901
+ 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
39902
  }
39710
39903
  var RETIRED_MODEL_KEYS = {
39711
39904
  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 +39968,13 @@ function validateModelRows(model, rows) {
39775
39968
  }
39776
39969
  seenRefs.add(row.ref);
39777
39970
  for (const [fieldAlias2, value] of Object.entries(row.fields)) {
39778
- const path15 = `rows.${alias2}.${row.ref}.${fieldAlias2}`;
39971
+ const path16 = `rows.${alias2}.${row.ref}.${fieldAlias2}`;
39779
39972
  const field = fieldByAlias.get(fieldAlias2);
39780
39973
  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"})`));
39974
+ 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
39975
  continue;
39783
39976
  }
39784
- checkValue(field, value, path15, refs, errors);
39977
+ checkValue(field, value, path16, refs, errors);
39785
39978
  }
39786
39979
  for (const field of entity.fields) {
39787
39980
  if (field.required !== true || COMPUTED_FIELD_TYPES2.has(field.type)) continue;
@@ -39793,13 +39986,13 @@ function validateModelRows(model, rows) {
39793
39986
  return errors;
39794
39987
  }
39795
39988
  var COMPUTED_FIELD_TYPES2 = /* @__PURE__ */ new Set(["formula", "rollup", "lookup", "autonumber"]);
39796
- function checkValue(field, value, path15, refs, errors) {
39989
+ function checkValue(field, value, path16, refs, errors) {
39797
39990
  switch (field.type) {
39798
39991
  case "select": {
39799
39992
  const declared = new Set(field.options.map((option) => option.alias));
39800
39993
  for (const raw of asList(value)) {
39801
39994
  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"})`));
39995
+ 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
39996
  }
39804
39997
  }
39805
39998
  return;
@@ -39807,30 +40000,30 @@ function checkValue(field, value, path15, refs, errors) {
39807
40000
  case "select_record_link": {
39808
40001
  for (const raw of asList(value)) {
39809
40002
  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>"`));
40003
+ errors.push(error51("rows.value", path16, `${quote(raw)} names no row in this request \u2014 use "<entity-alias>:<ref>"`));
39811
40004
  continue;
39812
40005
  }
39813
40006
  const named = raw.slice(0, raw.indexOf(":"));
39814
40007
  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}"`));
40008
+ errors.push(error51("rows.value", path16, `names a row of entity "${named}", but this field links to "${field.target_entity}"`));
39816
40009
  }
39817
40010
  return;
39818
40011
  }
39819
40012
  case "select_member": {
39820
40013
  if (value !== "self") {
39821
- errors.push(error51("rows.value", path15, `${quote(value)} is not a member this model can name \u2014 use "self"`));
40014
+ errors.push(error51("rows.value", path16, `${quote(value)} is not a member this model can name \u2014 use "self"`));
39822
40015
  }
39823
40016
  return;
39824
40017
  }
39825
40018
  case "files": {
39826
40019
  for (const raw of asList(value)) {
39827
40020
  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`));
40021
+ 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
40022
  continue;
39830
40023
  }
39831
40024
  if (isStoredFileId(raw)) continue;
39832
40025
  if (raw.startsWith("/") || raw.split(/[\\/]/).includes("..")) {
39833
- errors.push(error51("rows.value", path15, `"${raw}" must be a relative path beside this file, with no ".."`));
40026
+ errors.push(error51("rows.value", path16, `"${raw}" must be a relative path beside this file, with no ".."`));
39834
40027
  }
39835
40028
  }
39836
40029
  return;
@@ -39839,23 +40032,23 @@ function checkValue(field, value, path15, refs, errors) {
39839
40032
  case "rollup":
39840
40033
  case "lookup":
39841
40034
  case "autonumber": {
39842
- errors.push(error51("rows.value", path15, `is a ${field.type} field, which the platform computes \u2014 drop the value`));
40035
+ errors.push(error51("rows.value", path16, `is a ${field.type} field, which the platform computes \u2014 drop the value`));
39843
40036
  return;
39844
40037
  }
39845
40038
  default: {
39846
40039
  if (!isFixtureScalarValue(value)) {
39847
- errors.push(error51("rows.value", path15, `must be a string, number, boolean or null on a ${field.type} field`));
40040
+ errors.push(error51("rows.value", path16, `must be a string, number, boolean or null on a ${field.type} field`));
39848
40041
  return;
39849
40042
  }
39850
40043
  const expression = field.type === "date" ? parseFixtureDateExpression(value) : void 0;
39851
40044
  if (expression?.kind === "invalid") {
39852
- errors.push(error51("rows.value", path15, `${quote(value)} is not a date \u2014 use ${FIXTURE_DATE_GRAMMAR}`));
40045
+ errors.push(error51("rows.value", path16, `${quote(value)} is not a date \u2014 use ${FIXTURE_DATE_GRAMMAR}`));
39853
40046
  } else if (expression?.kind === "relative" && field.type === "date") {
39854
40047
  const clock = field.format === "datetime";
39855
40048
  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"`));
40049
+ 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
40050
  } 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`));
40051
+ errors.push(error51("rows.value", path16, `${quote(value)} states an hour on a date that holds none \u2014 drop the hour`));
39859
40052
  }
39860
40053
  }
39861
40054
  }
@@ -40128,17 +40321,17 @@ function validateWorkspaceModel(model, rows) {
40128
40321
  errors.push(...checkModelSingulars(contract.entities, apps ?? []));
40129
40322
  return [...contractErrors, ...errors];
40130
40323
  }
40131
- function unknownModelKeys(raw, kept, path15 = "") {
40324
+ function unknownModelKeys(raw, kept, path16 = "") {
40132
40325
  if (Array.isArray(raw)) {
40133
40326
  if (!Array.isArray(kept)) return [];
40134
- return raw.flatMap((item, i) => unknownModelKeys(item, kept[i], path15 === "" ? String(i) : `${path15}.${i}`));
40327
+ return raw.flatMap((item, i) => unknownModelKeys(item, kept[i], path16 === "" ? String(i) : `${path16}.${i}`));
40135
40328
  }
40136
40329
  if (!isPlainObject2(raw) || !isPlainObject2(kept)) return [];
40137
40330
  if (isBareFilterNode(raw) && kept.node_type === "group" && Array.isArray(kept.children) && kept.children.length === 1) {
40138
- return unknownModelKeys(raw, kept.children[0], path15);
40331
+ return unknownModelKeys(raw, kept.children[0], path16);
40139
40332
  }
40140
40333
  return Object.keys(raw).flatMap((key) => {
40141
- const here = path15 === "" ? key : `${path15}.${key}`;
40334
+ const here = path16 === "" ? key : `${path16}.${key}`;
40142
40335
  if (!(key in kept)) return [error51("model.schema", here, "unknown key \u2014 not part of the model")];
40143
40336
  return unknownModelKeys(raw[key], kept[key], here);
40144
40337
  });
@@ -40251,8 +40444,8 @@ function resultSideEffects(result) {
40251
40444
  }
40252
40445
 
40253
40446
  // src/version.ts
40254
- var VERSION = "0.280.0";
40255
- var APP_SDK_VERSION = "0.108.0";
40447
+ var VERSION = "0.282.0";
40448
+ var APP_SDK_VERSION = "0.109.0";
40256
40449
 
40257
40450
  // src/timezone.ts
40258
40451
  function machineTimezone() {
@@ -40277,7 +40470,7 @@ var AGENTS_default = "# @lotics/cli \u2014 agent index\n\nThe model an agent nee
40277
40470
  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
40471
 
40279
40472
  // 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";
40473
+ 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
40474
 
40282
40475
  // docs/data_model.md
40283
40476
  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 +40495,8 @@ var migration_default = "# @lotics/cli \u2014 migration notes\n\nWhat to DO when
40302
40495
 
40303
40496
  // ../shared/src/doc_pages.ts
40304
40497
  var DOC_LINKS = {
40305
- cli: (path15) => `lotics docs ${path15}`,
40306
- tool: (path15) => `docs path="${path15}"`
40498
+ cli: (path16) => `lotics docs ${path16}`,
40499
+ tool: (path16) => `docs path="${path16}"`
40307
40500
  };
40308
40501
  var SOURCES = [
40309
40502
  { area: "model", file: "src/model_reference.md", marker: "a workspace starts here", readers: ["cli", "tool"] },
@@ -40505,8 +40698,8 @@ function renderDocIndex(areas, usage) {
40505
40698
  const lines = areas.map((a, index) => ` ${names[index].padEnd(width)}${titleOf(a.text)}`);
40506
40699
  return [...lines, "", ...usage].join("\n");
40507
40700
  }
40508
- function readDocs(areas, path15, surface) {
40509
- const asked = (path15 ?? "").trim();
40701
+ function readDocs(areas, path16, surface) {
40702
+ const asked = (path16 ?? "").trim();
40510
40703
  if (asked === "") return { text: oneNewline(renderDocIndex(areas, surface.usage)) };
40511
40704
  let hits = matchDocArea(areas, asked);
40512
40705
  let section;
@@ -40517,11 +40710,11 @@ function readDocs(areas, path15, surface) {
40517
40710
  section = asked.slice(cut + 1);
40518
40711
  }
40519
40712
  }
40520
- if (hits.length === 0) return { error: `No reference area matching "${path15}".
40713
+ if (hits.length === 0) return { error: `No reference area matching "${path16}".
40521
40714
  ${surface.listHint}` };
40522
40715
  if (hits.length > 1) {
40523
40716
  return {
40524
- error: `"${path15}" matches more than one reference:
40717
+ error: `"${path16}" matches more than one reference:
40525
40718
  ${hits.map((a) => ` \u2022 ${surface.link(a.area)}`).join("\n")}`
40526
40719
  };
40527
40720
  }
@@ -40702,7 +40895,11 @@ var COMMANDS = [
40702
40895
  " @lotics/app-sdk, installed, and typed by the app's",
40703
40896
  " bindings in .lotics/. An app stated in a model is made",
40704
40897
  " by lotics model apply",
40705
- " lotics app deploy [-m <msg>] Rewrite .lotics/ from the app's live bindings, then",
40898
+ " lotics app pull [app_id] [path] The live version's source, installed and typed, ready to",
40899
+ " deploy on it: this app's project is brought up to date in",
40900
+ " place (refused, naming the files, when it has edits since",
40901
+ " its version); an empty or new [path] receives it whole",
40902
+ " lotics app deploy [-m <msg>] Rewrite .lotics/ from the app's live bindings, then",
40706
40903
  " typecheck + build this directory and upload it as a new",
40707
40904
  " version of the live app. Its queries, workflows, agents",
40708
40905
  " and capabilities carry forward unchanged \u2014 each is set",
@@ -40768,8 +40965,8 @@ function helpEntries(help) {
40768
40965
  }
40769
40966
  return entries2;
40770
40967
  }
40771
- function commandHelp(path15) {
40772
- const words = path15.filter((word) => word !== "");
40968
+ function commandHelp(path16) {
40969
+ const words = path16.filter((word) => word !== "");
40773
40970
  if (words.length === 0) return null;
40774
40971
  const group = COMMANDS.find(
40775
40972
  (g) => g.verbs.includes(words[0]) || (g.aliases ?? []).includes(words[0])
@@ -40982,6 +41179,9 @@ function readAppWorkspaceId(projectDir) {
40982
41179
  return null;
40983
41180
  }
40984
41181
  }
41182
+ function readProjectAppId(projectDir) {
41183
+ return stringOrNull(readLotics(projectDir)?.app_id);
41184
+ }
40985
41185
  function readAppMeta(projectDir) {
40986
41186
  const lotics = readLotics(projectDir);
40987
41187
  const appId = stringOrNull(lotics?.app_id);
@@ -41000,6 +41200,15 @@ function readAppMeta(projectDir) {
41000
41200
  version_number: typeof versionNumber === "number" ? versionNumber : null
41001
41201
  };
41002
41202
  }
41203
+ function writePulledManifest(projectDir, manifest) {
41204
+ const pkgPath = path7.join(projectDir, "package.json");
41205
+ if (!fs7.existsSync(pkgPath)) fail(`The pulled source has no package.json at ${pkgPath}.`);
41206
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
41207
+ if (typeof pkg !== "object" || pkg === null) fail(`${pkgPath} is not a JSON object.`);
41208
+ const lotics = { ...manifest, version_number: null };
41209
+ fs7.writeFileSync(pkgPath, `${JSON.stringify({ ...pkg, lotics }, null, 2)}
41210
+ `);
41211
+ }
41003
41212
  function writeDeployedVersion(projectDir, version2) {
41004
41213
  const pkgPath = path7.join(projectDir, "package.json");
41005
41214
  const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
@@ -41395,6 +41604,10 @@ function flushPendingWarnings() {
41395
41604
  }
41396
41605
 
41397
41606
  // src/app_toolchain.ts
41607
+ var ARCHIVE_EXCLUDES = ["node_modules", "dist", "*.tsbuildinfo", ".git"];
41608
+ function packSource(projectDir, archivePath) {
41609
+ return runTar(["-czf", archivePath, ...ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`), "."], projectDir);
41610
+ }
41398
41611
  function runTar(args, cwd) {
41399
41612
  return new Promise((resolve, reject) => {
41400
41613
  const proc = spawn("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
@@ -41580,10 +41793,110 @@ Ready. Next steps:`);
41580
41793
  return { app_id: app.id, path: targetPath };
41581
41794
  }
41582
41795
 
41583
- // src/app_deploy.ts
41796
+ // src/app_pull.ts
41584
41797
  import fs11 from "node:fs";
41585
41798
  import path11 from "node:path";
41586
41799
  import { tmpdir } from "node:os";
41800
+ var NAMED_CHANGES = 20;
41801
+ async function appPull(client, args) {
41802
+ const cwdAppId = readProjectAppId(process.cwd());
41803
+ const appId = args.appId ?? cwdAppId;
41804
+ if (appId === null) fail("This directory is not an app project. Name the app: lotics app pull <app_id> [path]");
41805
+ const app = await client.getApp(appId);
41806
+ const live = app.current_version_id;
41807
+ if (live === null) fail(`App ${app.id} has no version yet, so there is nothing to pull.`);
41808
+ const targetPath = path11.resolve(
41809
+ args.targetPath ?? (args.appId === void 0 || cwdAppId === app.id ? process.cwd() : appDirName(app.name))
41810
+ );
41811
+ const targetAppId = readProjectAppId(targetPath);
41812
+ if (targetAppId !== null && targetAppId !== app.id) {
41813
+ fail(`${targetPath} is the project of ${targetAppId}, not ${app.id}.`);
41814
+ }
41815
+ if (targetAppId === null && fs11.existsSync(targetPath) && fs11.readdirSync(targetPath).length > 0) {
41816
+ fail(`${targetPath} is not empty and is not this app's project. Pull into a new directory.`);
41817
+ }
41818
+ const scratch = fs11.mkdtempSync(path11.join(tmpdir(), "lotics-pull-"));
41819
+ try {
41820
+ if (targetAppId !== null) {
41821
+ const base = readAppMeta(targetPath).current_version_id;
41822
+ if (base === live) {
41823
+ note2(`${targetPath} is already at the live version (${live}).`);
41824
+ return { app_id: app.id, version_id: live, path: targetPath, outcome: "unchanged" };
41825
+ }
41826
+ if (base === null) {
41827
+ fail(`${targetPath} was never deployed, so its edits cannot be told apart from ${live}. Pull into a new directory.`);
41828
+ }
41829
+ const local = await unpack(scratch, "local", (archive2) => packSource(targetPath, archive2));
41830
+ const baseTree = await unpack(
41831
+ scratch,
41832
+ "base",
41833
+ async (archive2) => fs11.writeFileSync(archive2, await client.downloadAppVersionSource(app.id, base))
41834
+ );
41835
+ const changed = changedSourcePaths(baseTree, local);
41836
+ if (changed.length > 0) {
41837
+ const named = changed.slice(0, NAMED_CHANGES).map((file2) => ` ${file2}`);
41838
+ const rest = changed.length > NAMED_CHANGES ? [` \u2026and ${changed.length - NAMED_CHANGES} more`] : [];
41839
+ fail(
41840
+ `${targetPath} has changes since ${base}, the version it was based on:
41841
+ ${[...named, ...rest].join("\n")}
41842
+ 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.`
41843
+ );
41844
+ }
41845
+ for (const file2 of listFiles(local)) fs11.rmSync(path11.join(targetPath, file2));
41846
+ removeEmptyDirs(targetPath, listDirs(local));
41847
+ }
41848
+ const archive = path11.join(scratch, "live.tar.gz");
41849
+ fs11.writeFileSync(archive, await client.downloadAppVersionSource(app.id, live));
41850
+ fs11.mkdirSync(targetPath, { recursive: true });
41851
+ await runTar(["-xzf", archive], targetPath);
41852
+ } finally {
41853
+ fs11.rmSync(scratch, { recursive: true, force: true });
41854
+ }
41855
+ writePulledManifest(targetPath, { app_id: app.id, workspace_id: app.workspace_id, current_version_id: live });
41856
+ await writeAppTypes(client, targetPath, app.id);
41857
+ note2(`Pulled ${app.name} (${live}) into ${targetPath}`);
41858
+ await runNpmInstall(targetPath);
41859
+ return { app_id: app.id, version_id: live, path: targetPath, outcome: "pulled" };
41860
+ }
41861
+ async function unpack(scratch, name, write) {
41862
+ const archive = path11.join(scratch, `${name}.tar.gz`);
41863
+ const dir = path11.join(scratch, name);
41864
+ await write(archive);
41865
+ fs11.mkdirSync(dir);
41866
+ await runTar(["-xzf", archive], dir);
41867
+ return dir;
41868
+ }
41869
+ function changedSourcePaths(baseDir, localDir) {
41870
+ const files = /* @__PURE__ */ new Set([...listFiles(baseDir), ...listFiles(localDir)]);
41871
+ return [...files].filter((file2) => !file2.startsWith(`.lotics${path11.sep}`)).filter((file2) => !sameSource(path11.join(baseDir, file2), path11.join(localDir, file2), file2 === "package.json")).sort();
41872
+ }
41873
+ function sameSource(a, b, isManifest) {
41874
+ if (!fs11.existsSync(a) || !fs11.existsSync(b)) return false;
41875
+ if (!isManifest) return fs11.readFileSync(a).equals(fs11.readFileSync(b));
41876
+ return JSON.stringify(withoutOwnRecord(a)) === JSON.stringify(withoutOwnRecord(b));
41877
+ }
41878
+ function withoutOwnRecord(pkgPath) {
41879
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
41880
+ if (typeof pkg !== "object" || pkg === null) return pkg;
41881
+ return Object.fromEntries(Object.entries(pkg).filter(([key]) => key !== "lotics"));
41882
+ }
41883
+ function listFiles(dir) {
41884
+ return fs11.readdirSync(dir, { recursive: true, withFileTypes: true }).filter((entry) => !entry.isDirectory()).map((entry) => path11.relative(dir, path11.join(entry.parentPath, entry.name)));
41885
+ }
41886
+ function listDirs(dir) {
41887
+ return fs11.readdirSync(dir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path11.relative(dir, path11.join(entry.parentPath, entry.name)));
41888
+ }
41889
+ function removeEmptyDirs(root, dirs) {
41890
+ for (const dir of [...dirs].sort((a, b) => b.length - a.length)) {
41891
+ const full = path11.join(root, dir);
41892
+ if (fs11.existsSync(full) && fs11.readdirSync(full).length === 0) fs11.rmdirSync(full);
41893
+ }
41894
+ }
41895
+
41896
+ // src/app_deploy.ts
41897
+ import fs12 from "node:fs";
41898
+ import path12 from "node:path";
41899
+ import { tmpdir as tmpdir2 } from "node:os";
41587
41900
  var BINDING_KEY_OWNERS = {
41588
41901
  queries: "set_app_query",
41589
41902
  workflows: "set_app_workflow",
@@ -41595,19 +41908,18 @@ function declaredBindingKeys(pkg) {
41595
41908
  if (typeof lotics !== "object" || lotics === null) return [];
41596
41909
  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
41910
  }
41598
- var ARCHIVE_EXCLUDES = ["node_modules", "dist", "*.tsbuildinfo", ".git"];
41599
41911
  async function appDeploy(client, args) {
41600
- const projectDir = path11.resolve(args.projectDir ?? process.cwd());
41912
+ const projectDir = path12.resolve(args.projectDir ?? process.cwd());
41601
41913
  const meta3 = readAppMeta(projectDir);
41602
- const pkg = JSON.parse(fs11.readFileSync(path11.join(projectDir, "package.json"), "utf-8"));
41914
+ const pkg = JSON.parse(fs12.readFileSync(path12.join(projectDir, "package.json"), "utf-8"));
41603
41915
  const scripts = typeof pkg === "object" && pkg !== null && "scripts" in pkg ? pkg.scripts : void 0;
41604
41916
  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.`);
41917
+ fail(`${path12.join(projectDir, "package.json")} declares no \`build\` script, so there is no bundle to deploy.`);
41606
41918
  }
41607
41919
  const declared = declaredBindingKeys(pkg);
41608
41920
  if (declared.length > 0) {
41609
41921
  fail(
41610
- `${path11.join(projectDir, "package.json")} still declares bindings a deploy does not ship:
41922
+ `${path12.join(projectDir, "package.json")} still declares bindings a deploy does not ship:
41611
41923
  ${declared.join("\n ")}
41612
41924
  Delete these keys from package.json#lotics, then deploy again.`
41613
41925
  );
@@ -41616,26 +41928,26 @@ Delete these keys from package.json#lotics, then deploy again.`
41616
41928
  await runAppTypecheck(projectDir);
41617
41929
  note2("Building...");
41618
41930
  await runNpm(["run", "build"], projectDir);
41619
- const distDir = path11.join(projectDir, "dist");
41620
- if (!fs11.existsSync(distDir)) {
41931
+ const distDir = path12.join(projectDir, "dist");
41932
+ if (!fs12.existsSync(distDir)) {
41621
41933
  fail(`\`npm run build\` produced no dist/ in ${projectDir} \u2014 Vite writes one by default; check its outDir.`);
41622
41934
  }
41623
41935
  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`);
41936
+ const tmpSource = path12.join(tmpdir2(), `lotics-source-${stamp}.tar.gz`);
41937
+ const tmpDist = path12.join(tmpdir2(), `lotics-dist-${stamp}.tar.gz`);
41626
41938
  try {
41627
41939
  note2("Packaging...");
41628
- await runTar(["-czf", tmpSource, ...ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`), "."], projectDir);
41940
+ await packSource(projectDir, tmpSource);
41629
41941
  await runTar(["-czf", tmpDist, "-C", distDir, "."], projectDir);
41630
41942
  note2("Uploading...");
41631
41943
  const result = await client.deployAppVersion({
41632
41944
  app_id: meta3.app_id,
41633
- source_archive: fs11.readFileSync(tmpSource),
41634
- dist_archive: fs11.readFileSync(tmpDist),
41945
+ source_archive: fs12.readFileSync(tmpSource),
41946
+ dist_archive: fs12.readFileSync(tmpDist),
41635
41947
  prev_version_id: meta3.current_version_id,
41636
41948
  ...args.message === void 0 ? {} : { message: args.message }
41637
41949
  }).catch((error52) => {
41638
- throw staleRefusal(error52, meta3.current_version_id);
41950
+ throw staleRefusal(error52, meta3.current_version_id, meta3.app_id);
41639
41951
  });
41640
41952
  writeDeployedVersion(projectDir, result);
41641
41953
  note2(`Deployed v${result.version_number} (${result.version_id})`);
@@ -41643,18 +41955,18 @@ Delete these keys from package.json#lotics, then deploy again.`
41643
41955
  if (result.origin !== void 0) note2(`Address: ${result.origin}`);
41644
41956
  return result;
41645
41957
  } finally {
41646
- fs11.rmSync(tmpSource, { force: true });
41647
- fs11.rmSync(tmpDist, { force: true });
41958
+ fs12.rmSync(tmpSource, { force: true });
41959
+ fs12.rmSync(tmpDist, { force: true });
41648
41960
  }
41649
41961
  }
41650
- function staleRefusal(error52, deployedFrom) {
41962
+ function staleRefusal(error52, deployedFrom, appId) {
41651
41963
  if (!(error52 instanceof LoticsRequestError) || error52.status !== 409) return error52;
41652
41964
  const served = error52.body.current_version_id;
41653
41965
  if (typeof served !== "string" || served === deployedFrom) return error52;
41654
41966
  const said = typeof error52.body.message === "string" ? error52.body.message : error52.message;
41655
41967
  return new CliError(
41656
41968
  `${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.`
41969
+ 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
41970
  );
41659
41971
  }
41660
41972
 
@@ -41910,8 +42222,8 @@ function parseArgs(argv2) {
41910
42222
  }
41911
42223
 
41912
42224
  // src/model_commands.ts
41913
- import fs12 from "node:fs";
41914
- import path12 from "node:path";
42225
+ import fs13 from "node:fs";
42226
+ import path13 from "node:path";
41915
42227
 
41916
42228
  // ../shared/src/app_compile/history_rows.ts
41917
42229
  var LANDED = "@today 00:00";
@@ -42011,8 +42323,8 @@ function count(n, noun) {
42011
42323
  return `${n} ${noun}${n === 1 ? "" : "s"}`;
42012
42324
  }
42013
42325
  function besideFile(file2) {
42014
- const dir = path12.dirname(path12.resolve(file2));
42015
- return { documents: { dir, exists: (relative) => fs12.existsSync(path12.resolve(dir, relative)) } };
42326
+ const dir = path13.dirname(path13.resolve(file2));
42327
+ return { documents: { dir, exists: (relative) => fs13.existsSync(path13.resolve(dir, relative)) } };
42016
42328
  }
42017
42329
  function oneFinding(pathName, message2) {
42018
42330
  return [{ path: pathName, message: message2, severity: "error" }];
@@ -42020,7 +42332,7 @@ function oneFinding(pathName, message2) {
42020
42332
  function readModelFile(file2) {
42021
42333
  let raw;
42022
42334
  try {
42023
- raw = JSON.parse(fs12.readFileSync(file2, "utf-8"));
42335
+ raw = JSON.parse(fs13.readFileSync(file2, "utf-8"));
42024
42336
  } catch (error52) {
42025
42337
  return { kind: "error", findings: oneFinding(file2, `cannot read: ${error52 instanceof Error ? error52.message : String(error52)}`) };
42026
42338
  }
@@ -42074,9 +42386,9 @@ async function uploadDocuments(client, read, file2) {
42074
42386
  const idByPath = new Map(Object.entries(known).filter(([relative]) => paths.includes(relative)));
42075
42387
  const fresh = paths.filter((relative) => !idByPath.has(relative));
42076
42388
  if (fresh.length > 0) note2(`Uploading ${count(fresh.length, "document")} the rows attach\u2026`);
42077
- const baseDir = path12.dirname(path12.resolve(file2));
42389
+ const baseDir = path13.dirname(path13.resolve(file2));
42078
42390
  for (const relative of fresh) {
42079
- const uploaded = await client.uploadFiles([path12.resolve(baseDir, relative)]);
42391
+ const uploaded = await client.uploadFiles([path13.resolve(baseDir, relative)]);
42080
42392
  const refused = uploaded.errors[0];
42081
42393
  if (refused !== void 0) fail(`"${relative}" could not be uploaded (${refused.error}), so nothing was applied.`);
42082
42394
  const id = uploaded.files[0]?.id;
@@ -42201,8 +42513,8 @@ async function modelPull(client, output) {
42201
42513
  process.stdout.write(text);
42202
42514
  return;
42203
42515
  }
42204
- fs12.writeFileSync(output, text);
42205
- note2(`Wrote ${path12.resolve(output)}.`);
42516
+ fs13.writeFileSync(output, text);
42517
+ note2(`Wrote ${path13.resolve(output)}.`);
42206
42518
  }
42207
42519
 
42208
42520
  // src/signin_link.ts
@@ -42382,7 +42694,7 @@ async function validateOrgWorkspacePin(client, orgId, profile) {
42382
42694
  }
42383
42695
 
42384
42696
  // src/knowledge.ts
42385
- import fs14 from "node:fs";
42697
+ import fs15 from "node:fs";
42386
42698
 
42387
42699
  // ../shared/src/knowledge_tags.ts
42388
42700
  function isHiddenDoc(doc) {
@@ -42390,17 +42702,17 @@ function isHiddenDoc(doc) {
42390
42702
  }
42391
42703
 
42392
42704
  // src/cli_io.ts
42393
- import fs13 from "node:fs";
42394
- import path13 from "node:path";
42705
+ import fs14 from "node:fs";
42706
+ import path14 from "node:path";
42395
42707
  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);
42708
+ const dir = path14.dirname(path14.resolve(filePath));
42709
+ const tmp = path14.join(dir, `.${path14.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
42710
+ fs14.writeFileSync(tmp, bytes);
42399
42711
  try {
42400
- fs13.renameSync(tmp, filePath);
42712
+ fs14.renameSync(tmp, filePath);
42401
42713
  } catch (error52) {
42402
42714
  try {
42403
- fs13.unlinkSync(tmp);
42715
+ fs14.unlinkSync(tmp);
42404
42716
  } catch {
42405
42717
  }
42406
42718
  throw error52;
@@ -42410,8 +42722,8 @@ function writeFileAtomic(filePath, bytes) {
42410
42722
 
42411
42723
  // src/knowledge.ts
42412
42724
  function readBodyFile(filePath) {
42413
- if (!fs14.existsSync(filePath)) fail(`File not found: ${filePath}`);
42414
- return fs14.readFileSync(filePath, "utf-8");
42725
+ if (!fs15.existsSync(filePath)) fail(`File not found: ${filePath}`);
42726
+ return fs15.readFileSync(filePath, "utf-8");
42415
42727
  }
42416
42728
  function resolveBody(flags, required2) {
42417
42729
  if (flags.from !== void 0 && flags.content !== void 0) {
@@ -42768,8 +43080,8 @@ function prompt(question) {
42768
43080
  });
42769
43081
  });
42770
43082
  }
42771
- async function publicPost(apiUrl, path15, body) {
42772
- const response = await fetch(`${apiUrl}${path15}`, {
43083
+ async function publicPost(apiUrl, path16, body) {
43084
+ const response = await fetch(`${apiUrl}${path16}`, {
42773
43085
  method: "POST",
42774
43086
  headers: { "Content-Type": "application/json" },
42775
43087
  body: JSON.stringify(body)
@@ -43130,13 +43442,13 @@ async function resolveWorkspace(client, ctx) {
43130
43442
  function resolveUploadPaths(rawPaths) {
43131
43443
  const result = [];
43132
43444
  for (const p of rawPaths) {
43133
- const resolved = path14.resolve(p);
43134
- const stat = fs15.statSync(resolved);
43445
+ const resolved = path15.resolve(p);
43446
+ const stat = fs16.statSync(resolved);
43135
43447
  if (stat.isDirectory()) {
43136
- const entries2 = fs15.readdirSync(resolved, { withFileTypes: true });
43448
+ const entries2 = fs16.readdirSync(resolved, { withFileTypes: true });
43137
43449
  for (const entry of entries2) {
43138
43450
  if (entry.isFile()) {
43139
- result.push(path14.join(resolved, entry.name));
43451
+ result.push(path15.join(resolved, entry.name));
43140
43452
  }
43141
43453
  }
43142
43454
  } else {
@@ -43334,11 +43646,11 @@ async function main(argv2) {
43334
43646
  if (input.startsWith("@")) {
43335
43647
  source = "file";
43336
43648
  const file2 = input.slice(1);
43337
- if (!fs15.existsSync(file2)) {
43649
+ if (!fs16.existsSync(file2)) {
43338
43650
  console.error(`No such file: ${file2}`);
43339
43651
  process.exit(1);
43340
43652
  }
43341
- input = fs15.readFileSync(file2, "utf-8");
43653
+ input = fs16.readFileSync(file2, "utf-8");
43342
43654
  } else if (input === "-") {
43343
43655
  source = "stdin";
43344
43656
  input = await readStdin();
@@ -43559,6 +43871,7 @@ async function main(argv2) {
43559
43871
  console.error("Usage:");
43560
43872
  console.error(" lotics app create <name> --custom [path] Create a custom-code app and scaffold it here");
43561
43873
  console.error(" lotics app deploy [-m <message>] Build this directory and upload it as a new version");
43874
+ console.error(" lotics app pull [app_id] [path] The app's live source: this project brought up to date, or a new directory");
43562
43875
  console.error(" Everything else about an app is a tool: lotics tools, then lotics run <tool>.");
43563
43876
  process.exit(1);
43564
43877
  }
@@ -43851,6 +44164,15 @@ Available workspaces:`);
43851
44164
  if (flags.json) emitJson({ workspace_id: ctx.workspaceId, ...created });
43852
44165
  return;
43853
44166
  }
44167
+ if (subcommand === "pull") {
44168
+ if (flags.json) setMachineOutput(true);
44169
+ const pulled = await appPull(client, {
44170
+ ...toolArgs === void 0 ? {} : { appId: toolArgs },
44171
+ ...restArgs[0] === void 0 ? {} : { targetPath: restArgs[0] }
44172
+ });
44173
+ if (flags.json) emitJson({ workspace_id: ctx.workspaceId, ...pulled });
44174
+ return;
44175
+ }
43854
44176
  if (subcommand === "deploy") {
43855
44177
  const message2 = (flags.message ?? toolArgs)?.trim();
43856
44178
  if (flags.json) setMachineOutput(true);
@@ -43946,7 +44268,7 @@ ${JSON.stringify(info.input_schema, null, 2)}`);
43946
44268
  const toolName = subcommand;
43947
44269
  const ingested = await ingestJsonArgs({
43948
44270
  rawArg: toolArgs,
43949
- readFile: (p) => fs15.readFileSync(p, "utf-8"),
44271
+ readFile: (p) => fs16.readFileSync(p, "utf-8"),
43950
44272
  readStdin
43951
44273
  });
43952
44274
  if (ingested.kind === "error") {