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