@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 +865 -543
- package/dist/src/client.d.ts +8 -0
- package/dist/src/client.js +13 -0
- package/docs/cli_reference.md +3 -2
- package/package.json +1 -1
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(
|
|
1201
|
-
return /^\.|this\b/.test(
|
|
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(
|
|
1206
|
-
return
|
|
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(
|
|
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:
|
|
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),
|
|
2599
|
+
var params = this.setupFullMustacheParams(decorator, program, void 0), path16 = decorator.path;
|
|
2600
2600
|
this.useDecorators = true;
|
|
2601
|
-
this.opcode("registerDecorator", params.length,
|
|
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
|
|
2666
|
-
this.opcode("getContext",
|
|
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
|
-
|
|
2670
|
-
this.accept(
|
|
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
|
|
2675
|
-
|
|
2676
|
-
this.accept(
|
|
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),
|
|
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
|
-
|
|
2687
|
-
|
|
2688
|
-
this.accept(
|
|
2689
|
-
this.opcode("invokeHelper", params.length,
|
|
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(
|
|
2693
|
-
this.addDepth(
|
|
2694
|
-
this.opcode("getContext",
|
|
2695
|
-
var 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,
|
|
2697
|
+
this.opcode("lookupBlockParam", blockParamId, path16.parts);
|
|
2698
2698
|
} else if (!name) {
|
|
2699
2699
|
this.opcode("pushContext");
|
|
2700
|
-
} else if (
|
|
2700
|
+
} else if (path16.data) {
|
|
2701
2701
|
this.options.data = true;
|
|
2702
|
-
this.opcode("lookupData",
|
|
2702
|
+
this.opcode("lookupData", path16.depth, path16.parts, path16.strict);
|
|
2703
2703
|
} else {
|
|
2704
|
-
this.opcode("lookupOnContext",
|
|
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
|
|
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
|
-
|
|
3060
|
+
path16 = url2.path;
|
|
3061
3061
|
}
|
|
3062
|
-
var isAbsolute = exports.isAbsolute(
|
|
3063
|
-
var parts =
|
|
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
|
-
|
|
3081
|
-
if (
|
|
3082
|
-
|
|
3080
|
+
path16 = parts.join("/");
|
|
3081
|
+
if (path16 === "") {
|
|
3082
|
+
path16 = isAbsolute ? "/" : ".";
|
|
3083
3083
|
}
|
|
3084
3084
|
if (url2) {
|
|
3085
|
-
url2.path =
|
|
3085
|
+
url2.path = path16;
|
|
3086
3086
|
return urlGenerate(url2);
|
|
3087
3087
|
}
|
|
3088
|
-
return
|
|
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
|
|
5872
|
-
return (id.data ? "@" : "") + "PATH:" +
|
|
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
|
|
5912
|
-
var templateString =
|
|
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
|
|
5928
|
-
import
|
|
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,
|
|
6241
|
-
const url2 = `${this.baseUrl}${
|
|
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,
|
|
8181
|
-
if (!
|
|
8193
|
+
function getElementAtPath(obj, path16) {
|
|
8194
|
+
if (!path16)
|
|
8182
8195
|
return obj;
|
|
8183
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 }, [...
|
|
8759
|
+
issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
|
|
8747
8760
|
} else if (issue2.code === "invalid_key") {
|
|
8748
|
-
processError({ issues: issue2.issues }, [...
|
|
8761
|
+
processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
|
|
8749
8762
|
} else if (issue2.code === "invalid_element") {
|
|
8750
|
-
processError({ issues: issue2.issues }, [...
|
|
8763
|
+
processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
|
|
8751
8764
|
} else {
|
|
8752
|
-
const fullpath = [...
|
|
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,
|
|
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 }, [...
|
|
8796
|
+
issue2.errors.map((issues) => processError({ issues }, [...path16, ...issue2.path]));
|
|
8784
8797
|
} else if (issue2.code === "invalid_key") {
|
|
8785
|
-
processError({ issues: issue2.issues }, [...
|
|
8798
|
+
processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
|
|
8786
8799
|
} else if (issue2.code === "invalid_element") {
|
|
8787
|
-
processError({ issues: issue2.issues }, [...
|
|
8800
|
+
processError({ issues: issue2.issues }, [...path16, ...issue2.path]);
|
|
8788
8801
|
} else {
|
|
8789
|
-
const fullpath = [...
|
|
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
|
|
8822
|
-
for (const seg of
|
|
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
|
|
21515
|
-
if (
|
|
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 (
|
|
21520
|
-
const key =
|
|
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,
|
|
25477
|
+
function conditionIssues(node, path16, fieldMap) {
|
|
25285
25478
|
if (!node || typeof node !== "object") {
|
|
25286
|
-
return [`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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
|
-
`${
|
|
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 [`${
|
|
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 [`${
|
|
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 [`${
|
|
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 [`${
|
|
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 [`${
|
|
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
|
-
`${
|
|
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,
|
|
25359
|
-
return conditionIssues(condition,
|
|
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(
|
|
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: [...
|
|
25553
|
-
for (const { key, message: message2 } of perRowRefusals(value)) ctx.addIssue({ code: "custom", message: message2, path: [...
|
|
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,
|
|
28226
|
+
function getNestedValue(obj, path16) {
|
|
28034
28227
|
if (obj == null) return void 0;
|
|
28035
|
-
const parts =
|
|
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,
|
|
28075
|
-
if (typeof
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
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,
|
|
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:
|
|
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:
|
|
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,
|
|
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,
|
|
31604
|
-
errors.push({ severity: "error", rule: "model.filter", path:
|
|
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:
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
32571
|
-
var note = (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,
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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,
|
|
35131
|
-
const kind = checkTemplate(model, template,
|
|
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",
|
|
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",
|
|
35145
|
-
...acts.length === 0 ? [] : [error51("register.export-template",
|
|
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,
|
|
35388
|
+
const own = (name, path16) => {
|
|
35196
35389
|
const found = fieldOf(entity, name);
|
|
35197
|
-
if (found === void 0) findings.push(error51("model.names-declared",
|
|
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
|
|
35235
|
-
const named = own(name,
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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", `${
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
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, `${
|
|
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,
|
|
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", `${
|
|
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,
|
|
35512
|
+
const field = (name, path16) => {
|
|
35320
35513
|
const found = fieldOf(entity, name);
|
|
35321
|
-
if (found === void 0) findings.push(error51("model.names-declared",
|
|
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
|
|
35545
|
+
const path16 = `${at2}.applies.${name}`;
|
|
35353
35546
|
const applying = fieldOf(entity, name);
|
|
35354
|
-
if (applying === void 0) return [error51("model.names-declared",
|
|
35355
|
-
if (name === display.title) return [error51("records.applies",
|
|
35356
|
-
const own = applying.required === true && !hasDefault(applying) ? [error51("records.applies",
|
|
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 = `${
|
|
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,
|
|
35574
|
+
const field = (name, path16) => {
|
|
35382
35575
|
const found = fieldOf(entity, name);
|
|
35383
|
-
if (found === void 0) findings.push(error51("model.names-declared",
|
|
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
|
|
35454
|
-
const started = field(name,
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
35673
|
-
if (!("link" in ref) && source.alias === field.alias) return [error51("write.bounds",
|
|
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",
|
|
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",
|
|
35871
|
+
return apart === void 0 ? [] : [error51("write.bounds", path16, apart)];
|
|
35679
35872
|
}
|
|
35680
|
-
function checkSame(entity, field, same, byAlias,
|
|
35873
|
+
function checkSame(entity, field, same, byAlias, path16) {
|
|
35681
35874
|
if (field.type !== "select_record_link") {
|
|
35682
|
-
return [error51("write.same",
|
|
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 = `${
|
|
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,
|
|
35700
|
-
if (field.type !== "text") return [error51("write.suggest",
|
|
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",
|
|
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",
|
|
35706
|
-
if (source.type !== "text") return [error51("write.suggest",
|
|
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
|
|
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",
|
|
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, `${
|
|
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",
|
|
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", `${
|
|
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", `${
|
|
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, `${
|
|
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", `${
|
|
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, `${
|
|
35796
|
-
if (rule.suggest !== void 0) findings.push(...checkSuggest(entity, field, rule.suggest, byAlias, `${
|
|
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", `${
|
|
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", `${
|
|
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 = `${
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
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,
|
|
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", `${
|
|
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,
|
|
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
|
-
`${
|
|
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,
|
|
37125
|
+
const own = (name, path16) => {
|
|
36933
37126
|
const found = fieldOf(entity, name);
|
|
36934
|
-
if (found === void 0) findings.push(error51("model.names-declared",
|
|
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
|
|
37169
|
+
const path16 = `${at2}.templates`;
|
|
36977
37170
|
const findings = [];
|
|
36978
|
-
if (act.template !== void 0) findings.push(error51("act.templates",
|
|
36979
|
-
if (act.into === void 0) findings.push(error51("act.templates",
|
|
36980
|
-
if (act.workflow !== void 0) findings.push(error51("act.makes-or-runs",
|
|
36981
|
-
if (act.of !== void 0 || (act.on ?? "record") !== "record") findings.push(error51("act.templates",
|
|
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 = `${
|
|
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
|
|
37088
|
-
if ((act.fills ?? []).indexOf(name) !== index) findings.push(error51("act.fills",
|
|
37089
|
-
findings.push(...checkFill(model, app, record2, name,
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
37125
|
-
return fillable(model, party, tail,
|
|
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,
|
|
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",
|
|
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",
|
|
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
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
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", `${
|
|
37191
|
-
if (written.includes(name)) findings.push(error51("act.record", `${
|
|
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", `${
|
|
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,
|
|
37402
|
+
function fillable(model, entity, name, path16) {
|
|
37210
37403
|
const field = fieldOf(entity, name);
|
|
37211
|
-
if (field === void 0) return [error51("model.names-declared",
|
|
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",
|
|
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
|
|
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",
|
|
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",
|
|
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,
|
|
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 = `${
|
|
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
|
|
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",
|
|
38420
|
-
if (book.entity === app.entity) findings.push(error51("app.books",
|
|
38421
|
-
if (seen.has(book.entity)) findings.push(error51("app.books",
|
|
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
|
|
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", `${
|
|
38437
|
-
else if (!mappable.has(alias2)) findings.push(error51("app.books", `${
|
|
38438
|
-
if (fieldOf(entity, held) === void 0) findings.push(error51("model.names-declared", `${
|
|
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",
|
|
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",
|
|
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,
|
|
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 = `${
|
|
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",
|
|
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,
|
|
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
|
-
|
|
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",
|
|
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:
|
|
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,
|
|
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",
|
|
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",
|
|
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,
|
|
38874
|
+
const own = (name, path16) => {
|
|
38682
38875
|
const found = fieldOf(entity, name);
|
|
38683
|
-
if (found === void 0) findings.push(error51("model.names-declared",
|
|
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,
|
|
38697
|
-
if (name === scope) findings.push(error51("app.scope",
|
|
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
|
|
38704
|
-
const column = own(name,
|
|
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",
|
|
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",
|
|
38709
|
-
scoped(name,
|
|
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",
|
|
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
|
|
38717
|
-
findings.push(...checkFilter(model, entity, stated,
|
|
38718
|
-
scoped(filterField(stated),
|
|
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
|
|
38722
|
-
findings.push(...checkSearch(model, entity, name,
|
|
38723
|
-
scoped(name,
|
|
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,
|
|
38728
|
-
const field = own(name,
|
|
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",
|
|
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
|
|
38742
|
-
if (named.has(tab.tab)) findings.push(error51("register.tabs", `${
|
|
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", `${
|
|
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, `${
|
|
38940
|
+
if (tab.in !== void 0) dated(tab.in, `${path16}.in`);
|
|
38748
38941
|
if (tab.open !== void 0) {
|
|
38749
|
-
dated(tab.open.from, `${
|
|
38750
|
-
dated(tab.open.to, `${
|
|
38751
|
-
if (tab.open.from === tab.open.to) findings.push(error51("register.tabs", `${
|
|
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, `${
|
|
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", `${
|
|
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, `${
|
|
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
|
|
38779
|
-
const field = own(name,
|
|
38780
|
-
if (field !== void 0 && !isAsked(field)) findings.push(error51("app.asked-written",
|
|
38781
|
-
if (field !== void 0) findings.push(...checkOwnRows(model, entity, field,
|
|
38782
|
-
if (name === scope) findings.push(error51("app.scope",
|
|
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
|
|
38980
|
+
const path16 = `${at2}.register.readings.${index}`;
|
|
38788
38981
|
if (readingEntity(reading) !== entity.alias) {
|
|
38789
|
-
findings.push(error51("register.readings-own",
|
|
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,
|
|
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", `${
|
|
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",
|
|
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",
|
|
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,
|
|
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",
|
|
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
|
|
38828
|
-
findings.push(...checkWhen(model.entities, entity, section.when, `${
|
|
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",
|
|
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, `${
|
|
39025
|
+
findings.push(...checkSectionAt(entity, display, section.at, `${path16}.at`));
|
|
38833
39026
|
for (const [order, name] of (section.fields ?? []).entries()) {
|
|
38834
|
-
const fieldPath = `${
|
|
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,
|
|
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",
|
|
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,
|
|
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",
|
|
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
|
|
38865
|
-
findings.push(...checkWhen(model.entities, entity, block.when, `${
|
|
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",
|
|
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",
|
|
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,
|
|
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
|
|
38898
|
-
if (acts.has(act.alias)) findings.push(error51("app.alias-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", `${
|
|
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", `${
|
|
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", `${
|
|
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,
|
|
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",
|
|
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,
|
|
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", `${
|
|
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", `${
|
|
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
|
|
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",
|
|
38946
|
-
else if (act.of !== void 0) findings.push(error51("record.section-acts",
|
|
38947
|
-
else if ((act.on ?? "record") !== "record") findings.push(error51("record.section-acts",
|
|
38948
|
-
else if (home !== void 0 && home.section !== index) findings.push(error51("record.move-home",
|
|
38949
|
-
else if (home !== void 0 || isCorrectionAct(model, app, act)) findings.push(...checkFootNeeds(model, entity, section, drawn, act,
|
|
38950
|
-
else findings.push(...checkStagedAct(display, section, act,
|
|
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",
|
|
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",
|
|
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
|
|
38977
|
-
findings.push(...checkResolve(model, app, check2, `${
|
|
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,
|
|
39172
|
+
findings.push(...checkChildCheck(model, check2, children, acts, path16));
|
|
38980
39173
|
continue;
|
|
38981
39174
|
}
|
|
38982
|
-
const field = own(check2.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", `${
|
|
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", `${
|
|
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,
|
|
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", `${
|
|
38996
|
-
else if (acts.get(target) !== void 0) findings.push(error51("check.blocks", `${
|
|
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:
|
|
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",
|
|
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:
|
|
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",
|
|
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:
|
|
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",
|
|
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,
|
|
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" ?
|
|
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",
|
|
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",
|
|
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, `${
|
|
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",
|
|
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
|
|
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",
|
|
39077
|
-
else if (!isSortableFieldType(field.type)) findings.push(error51("register.sort",
|
|
39078
|
-
if (sorted.has(key.field)) findings.push(error51("register.sort",
|
|
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,
|
|
39276
|
+
function checkSearch(model, entity, name, path16) {
|
|
39084
39277
|
const field = fieldOf(entity, name);
|
|
39085
|
-
if (field === void 0) return [error51("model.names-declared",
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
39101
|
-
if (around.layout !== "table" && around.layout !== "gantt") return [error51("register.group",
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
39133
|
-
if (checkRefuses(check2, act.alias)) return [error51("check.resolve",
|
|
39134
|
-
if (isCorrectionAct(model, app, act)) return [error51("check.resolve",
|
|
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
|
|
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",
|
|
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",
|
|
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",
|
|
39151
|
-
else if (first2 !== void 0) findings.push(error51("record.page",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
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
|
|
39420
|
+
const path16 = `${at2}.where.${name}`;
|
|
39228
39421
|
const field = fieldOf(child, name);
|
|
39229
|
-
if (field === void 0) return [error51("model.names-declared",
|
|
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",
|
|
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",
|
|
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",
|
|
39240
|
-
...options.length === 1 && (block.columns ?? []).includes(name) ? [error51("block.where",
|
|
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
|
|
39444
|
+
const path16 = `${at2}.columns.${index}`;
|
|
39252
39445
|
if (fieldOf(child, name) === void 0) {
|
|
39253
|
-
findings.push(error51("model.names-declared",
|
|
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",
|
|
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",
|
|
39261
|
-
if (name === back) findings.push(error51("block.column-drawn",
|
|
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",
|
|
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,
|
|
39474
|
+
function checkUnder(model, entity, child, block, path16) {
|
|
39282
39475
|
if (block.under === void 0) return [];
|
|
39283
|
-
const at2 = `${
|
|
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", `${
|
|
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", `${
|
|
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
|
|
39510
|
+
const path16 = `${at2}.register.roster`;
|
|
39318
39511
|
if (roster === void 0) {
|
|
39319
39512
|
return [
|
|
39320
|
-
...layout === "roster" ? [error51("register.roster",
|
|
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",
|
|
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",
|
|
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,
|
|
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
|
|
39550
|
+
const path16 = `${at2}.register.lanes`;
|
|
39358
39551
|
if (lanes === void 0) {
|
|
39359
39552
|
return [
|
|
39360
|
-
...layout === "lanes" ? [error51("register.lanes",
|
|
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",
|
|
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",
|
|
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
|
|
39568
|
+
const path16 = `${at2}.register.gantt`;
|
|
39376
39569
|
if (gantt === void 0) {
|
|
39377
|
-
return layout === "gantt" ? [error51("register.gantt",
|
|
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",
|
|
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 === "" ?
|
|
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,
|
|
39384
|
-
if (type !== "number") return [error51("register.tiers",
|
|
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
|
-
|
|
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
|
|
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", `${
|
|
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", `${
|
|
39406
|
-
...report.template === void 0 ? [] : checkExportTemplate(model, entity.alias, (app.register?.tabs ?? []).map((tab) => tab.tab), report.template, `${
|
|
39407
|
-
...checkNarrowing(model.entities, entity, report.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 }, `${
|
|
39410
|
-
...checkReportBooks(model, app, report,
|
|
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,
|
|
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",
|
|
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",
|
|
39419
|
-
if (name === around.scope) return [error51("app.scope",
|
|
39420
|
-
if (type !== "select" && type !== "select_member" && type !== "select_record_link") return [error51("register.export-report",
|
|
39421
|
-
if (field.type === "select_record_link" && field.cardinality === "many") return [error51("register.export-report",
|
|
39422
|
-
return around.fixed ? [error51("register.export-report",
|
|
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
|
|
39431
|
-
if (filters.includes(name)) findings.push(error51("register.where-opens",
|
|
39432
|
-
if (name === app.scope) findings.push(error51("app.scope",
|
|
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
|
|
39461
|
-
if (readingEntity(reading) !== entity.alias || checkReading(model, reading,
|
|
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:
|
|
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:
|
|
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",
|
|
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
|
|
39751
|
+
const path16 = `${at2}.files.${index}`;
|
|
39559
39752
|
const field = fieldOf(entity, name);
|
|
39560
|
-
if (field === void 0) findings.push(error51("model.names-declared",
|
|
39561
|
-
else if (field.type !== "files") findings.push(error51("block.field-kind",
|
|
39562
|
-
else place(name, FILES_BLOCK,
|
|
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,
|
|
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
|
|
39587
|
-
findings.push(...checkFilter(model, child, stated,
|
|
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
|
|
39593
|
-
const fixedGroup = same(block.group,
|
|
39594
|
-
if (block.under !== void 0) findings.push(error51("block.narrow",
|
|
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,
|
|
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
|
|
39807
|
+
const path16 = `${at2}.edits.${index}`;
|
|
39615
39808
|
const field = fieldOf(child, name);
|
|
39616
|
-
if (field === void 0) return [error51("model.names-declared",
|
|
39617
|
-
if (!shown.has(name)) return [error51("block.edits",
|
|
39618
|
-
if (!isAsked(field)) return [error51("block.edits",
|
|
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",
|
|
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
|
|
39702
|
-
if (issue2.code !== "invalid_union") return [{ path:
|
|
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,
|
|
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:
|
|
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
|
|
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",
|
|
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,
|
|
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,
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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,
|
|
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],
|
|
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],
|
|
40331
|
+
return unknownModelKeys(raw, kept.children[0], path16);
|
|
40139
40332
|
}
|
|
40140
40333
|
return Object.keys(raw).flatMap((key) => {
|
|
40141
|
-
const here =
|
|
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.
|
|
40255
|
-
var APP_SDK_VERSION = "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: (
|
|
40306
|
-
tool: (
|
|
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,
|
|
40509
|
-
const asked = (
|
|
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 "${
|
|
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: `"${
|
|
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
|
|
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(
|
|
40772
|
-
const words =
|
|
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/
|
|
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 =
|
|
41912
|
+
const projectDir = path12.resolve(args.projectDir ?? process.cwd());
|
|
41601
41913
|
const meta3 = readAppMeta(projectDir);
|
|
41602
|
-
const pkg = JSON.parse(
|
|
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(`${
|
|
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
|
-
`${
|
|
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 =
|
|
41620
|
-
if (!
|
|
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 =
|
|
41625
|
-
const tmpDist =
|
|
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
|
|
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:
|
|
41634
|
-
dist_archive:
|
|
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
|
-
|
|
41647
|
-
|
|
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.
|
|
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
|
|
41914
|
-
import
|
|
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 =
|
|
42015
|
-
return { documents: { dir, exists: (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(
|
|
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 =
|
|
42389
|
+
const baseDir = path13.dirname(path13.resolve(file2));
|
|
42078
42390
|
for (const relative of fresh) {
|
|
42079
|
-
const uploaded = await client.uploadFiles([
|
|
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
|
-
|
|
42205
|
-
note2(`Wrote ${
|
|
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
|
|
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
|
|
42394
|
-
import
|
|
42705
|
+
import fs14 from "node:fs";
|
|
42706
|
+
import path14 from "node:path";
|
|
42395
42707
|
function writeFileAtomic(filePath, bytes) {
|
|
42396
|
-
const dir =
|
|
42397
|
-
const tmp =
|
|
42398
|
-
|
|
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
|
-
|
|
42712
|
+
fs14.renameSync(tmp, filePath);
|
|
42401
42713
|
} catch (error52) {
|
|
42402
42714
|
try {
|
|
42403
|
-
|
|
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 (!
|
|
42414
|
-
return
|
|
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,
|
|
42772
|
-
const response = await fetch(`${apiUrl}${
|
|
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 =
|
|
43134
|
-
const stat =
|
|
43445
|
+
const resolved = path15.resolve(p);
|
|
43446
|
+
const stat = fs16.statSync(resolved);
|
|
43135
43447
|
if (stat.isDirectory()) {
|
|
43136
|
-
const entries2 =
|
|
43448
|
+
const entries2 = fs16.readdirSync(resolved, { withFileTypes: true });
|
|
43137
43449
|
for (const entry of entries2) {
|
|
43138
43450
|
if (entry.isFile()) {
|
|
43139
|
-
result.push(
|
|
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 (!
|
|
43649
|
+
if (!fs16.existsSync(file2)) {
|
|
43338
43650
|
console.error(`No such file: ${file2}`);
|
|
43339
43651
|
process.exit(1);
|
|
43340
43652
|
}
|
|
43341
|
-
input =
|
|
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) =>
|
|
44271
|
+
readFile: (p) => fs16.readFileSync(p, "utf-8"),
|
|
43950
44272
|
readStdin
|
|
43951
44273
|
});
|
|
43952
44274
|
if (ingested.kind === "error") {
|