@lotics/cli 0.190.0 → 0.191.1
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/AGENTS.md +1 -1
- package/README.md +2 -0
- package/dist/probe_page.js +421 -0
- package/dist/src/cli.js +1700 -650
- package/docs/building_an_app.md +14 -5
- package/docs/cli_reference.md +9 -9
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -26822,12 +26822,12 @@ var require_tokens = __commonJS({
|
|
|
26822
26822
|
});
|
|
26823
26823
|
}
|
|
26824
26824
|
exports2.assignCategoriesMapProp = assignCategoriesMapProp;
|
|
26825
|
-
function singleAssignCategoriesToksMap(
|
|
26826
|
-
utils_1.forEach(
|
|
26825
|
+
function singleAssignCategoriesToksMap(path14, nextNode) {
|
|
26826
|
+
utils_1.forEach(path14, function(pathNode) {
|
|
26827
26827
|
nextNode.categoryMatchesMap[pathNode.tokenTypeIdx] = true;
|
|
26828
26828
|
});
|
|
26829
26829
|
utils_1.forEach(nextNode.CATEGORIES, function(nextCategory) {
|
|
26830
|
-
var newPath =
|
|
26830
|
+
var newPath = path14.concat(nextNode);
|
|
26831
26831
|
if (!utils_1.contains(newPath, nextCategory)) {
|
|
26832
26832
|
singleAssignCategoriesToksMap(newPath, nextCategory);
|
|
26833
26833
|
}
|
|
@@ -28590,10 +28590,10 @@ var require_interpreter = __commonJS({
|
|
|
28590
28590
|
/** @class */
|
|
28591
28591
|
(function(_super) {
|
|
28592
28592
|
__extends(AbstractNextPossibleTokensWalker2, _super);
|
|
28593
|
-
function AbstractNextPossibleTokensWalker2(topProd,
|
|
28593
|
+
function AbstractNextPossibleTokensWalker2(topProd, path14) {
|
|
28594
28594
|
var _this = _super.call(this) || this;
|
|
28595
28595
|
_this.topProd = topProd;
|
|
28596
|
-
_this.path =
|
|
28596
|
+
_this.path = path14;
|
|
28597
28597
|
_this.possibleTokTypes = [];
|
|
28598
28598
|
_this.nextProductionName = "";
|
|
28599
28599
|
_this.nextProductionOccurrence = 0;
|
|
@@ -28647,9 +28647,9 @@ var require_interpreter = __commonJS({
|
|
|
28647
28647
|
/** @class */
|
|
28648
28648
|
(function(_super) {
|
|
28649
28649
|
__extends(NextAfterTokenWalker2, _super);
|
|
28650
|
-
function NextAfterTokenWalker2(topProd,
|
|
28651
|
-
var _this = _super.call(this, topProd,
|
|
28652
|
-
_this.path =
|
|
28650
|
+
function NextAfterTokenWalker2(topProd, path14) {
|
|
28651
|
+
var _this = _super.call(this, topProd, path14) || this;
|
|
28652
|
+
_this.path = path14;
|
|
28653
28653
|
_this.nextTerminalName = "";
|
|
28654
28654
|
_this.nextTerminalOccurrence = 0;
|
|
28655
28655
|
_this.nextTerminalName = _this.path.lastTok.name;
|
|
@@ -29346,10 +29346,10 @@ var require_lookahead = __commonJS({
|
|
|
29346
29346
|
}
|
|
29347
29347
|
return result;
|
|
29348
29348
|
}
|
|
29349
|
-
function pathToHashKeys(
|
|
29349
|
+
function pathToHashKeys(path14) {
|
|
29350
29350
|
var keys3 = [""];
|
|
29351
|
-
for (var i2 = 0; i2 <
|
|
29352
|
-
var tokType =
|
|
29351
|
+
for (var i2 = 0; i2 < path14.length; i2++) {
|
|
29352
|
+
var tokType = path14[i2];
|
|
29353
29353
|
var longerKeys = [];
|
|
29354
29354
|
for (var j = 0; j < keys3.length; j++) {
|
|
29355
29355
|
var currShorterKey = keys3[j];
|
|
@@ -29661,9 +29661,9 @@ var require_checks = __commonJS({
|
|
|
29661
29661
|
return errors2;
|
|
29662
29662
|
}
|
|
29663
29663
|
exports2.validateRuleIsOverridden = validateRuleIsOverridden;
|
|
29664
|
-
function validateNoLeftRecursion(topRule, currRule, errMsgProvider,
|
|
29665
|
-
if (
|
|
29666
|
-
|
|
29664
|
+
function validateNoLeftRecursion(topRule, currRule, errMsgProvider, path14) {
|
|
29665
|
+
if (path14 === void 0) {
|
|
29666
|
+
path14 = [];
|
|
29667
29667
|
}
|
|
29668
29668
|
var errors2 = [];
|
|
29669
29669
|
var nextNonTerminals = getFirstNoneTerminal(currRule.definition);
|
|
@@ -29676,15 +29676,15 @@ var require_checks = __commonJS({
|
|
|
29676
29676
|
errors2.push({
|
|
29677
29677
|
message: errMsgProvider.buildLeftRecursionError({
|
|
29678
29678
|
topLevelRule: topRule,
|
|
29679
|
-
leftRecursionPath:
|
|
29679
|
+
leftRecursionPath: path14
|
|
29680
29680
|
}),
|
|
29681
29681
|
type: parser_1.ParserDefinitionErrorType.LEFT_RECURSION,
|
|
29682
29682
|
ruleName
|
|
29683
29683
|
});
|
|
29684
29684
|
}
|
|
29685
|
-
var validNextSteps = utils2.difference(nextNonTerminals,
|
|
29685
|
+
var validNextSteps = utils2.difference(nextNonTerminals, path14.concat([topRule]));
|
|
29686
29686
|
var errorsFromNextSteps = utils2.map(validNextSteps, function(currRefRule) {
|
|
29687
|
-
var newPath = utils2.cloneArr(
|
|
29687
|
+
var newPath = utils2.cloneArr(path14);
|
|
29688
29688
|
newPath.push(currRefRule);
|
|
29689
29689
|
return validateNoLeftRecursion(topRule, currRefRule, errMsgProvider, newPath);
|
|
29690
29690
|
});
|
|
@@ -34706,7 +34706,7 @@ var require_BufferList = __commonJS({
|
|
|
34706
34706
|
this.head = this.tail = null;
|
|
34707
34707
|
this.length = 0;
|
|
34708
34708
|
};
|
|
34709
|
-
BufferList.prototype.join = function
|
|
34709
|
+
BufferList.prototype.join = function join5(s) {
|
|
34710
34710
|
if (this.length === 0) return "";
|
|
34711
34711
|
var p = this.head;
|
|
34712
34712
|
var ret = "" + p.data;
|
|
@@ -37293,8 +37293,8 @@ var require_utils4 = __commonJS({
|
|
|
37293
37293
|
var result = transform2[inputType][outputType](input);
|
|
37294
37294
|
return result;
|
|
37295
37295
|
};
|
|
37296
|
-
exports2.resolve = function(
|
|
37297
|
-
var parts =
|
|
37296
|
+
exports2.resolve = function(path14) {
|
|
37297
|
+
var parts = path14.split("/");
|
|
37298
37298
|
var result = [];
|
|
37299
37299
|
for (var index = 0; index < parts.length; index++) {
|
|
37300
37300
|
var part = parts[index];
|
|
@@ -39864,15 +39864,15 @@ var require_deflate = __commonJS({
|
|
|
39864
39864
|
if (!strm) {
|
|
39865
39865
|
return Z_STREAM_ERROR;
|
|
39866
39866
|
}
|
|
39867
|
-
var
|
|
39867
|
+
var wrap2 = 1;
|
|
39868
39868
|
if (level === Z_DEFAULT_COMPRESSION) {
|
|
39869
39869
|
level = 6;
|
|
39870
39870
|
}
|
|
39871
39871
|
if (windowBits < 0) {
|
|
39872
|
-
|
|
39872
|
+
wrap2 = 0;
|
|
39873
39873
|
windowBits = -windowBits;
|
|
39874
39874
|
} else if (windowBits > 15) {
|
|
39875
|
-
|
|
39875
|
+
wrap2 = 2;
|
|
39876
39876
|
windowBits -= 16;
|
|
39877
39877
|
}
|
|
39878
39878
|
if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED || windowBits < 8 || windowBits > 15 || level < 0 || level > 9 || strategy < 0 || strategy > Z_FIXED) {
|
|
@@ -39884,7 +39884,7 @@ var require_deflate = __commonJS({
|
|
|
39884
39884
|
var s = new DeflateState();
|
|
39885
39885
|
strm.state = s;
|
|
39886
39886
|
s.strm = strm;
|
|
39887
|
-
s.wrap =
|
|
39887
|
+
s.wrap = wrap2;
|
|
39888
39888
|
s.gzhead = null;
|
|
39889
39889
|
s.w_bits = windowBits;
|
|
39890
39890
|
s.w_size = 1 << s.w_bits;
|
|
@@ -40177,7 +40177,7 @@ var require_deflate = __commonJS({
|
|
|
40177
40177
|
var dictLength = dictionary.length;
|
|
40178
40178
|
var s;
|
|
40179
40179
|
var str, n;
|
|
40180
|
-
var
|
|
40180
|
+
var wrap2;
|
|
40181
40181
|
var avail;
|
|
40182
40182
|
var next;
|
|
40183
40183
|
var input;
|
|
@@ -40186,16 +40186,16 @@ var require_deflate = __commonJS({
|
|
|
40186
40186
|
return Z_STREAM_ERROR;
|
|
40187
40187
|
}
|
|
40188
40188
|
s = strm.state;
|
|
40189
|
-
|
|
40190
|
-
if (
|
|
40189
|
+
wrap2 = s.wrap;
|
|
40190
|
+
if (wrap2 === 2 || wrap2 === 1 && s.status !== INIT_STATE || s.lookahead) {
|
|
40191
40191
|
return Z_STREAM_ERROR;
|
|
40192
40192
|
}
|
|
40193
|
-
if (
|
|
40193
|
+
if (wrap2 === 1) {
|
|
40194
40194
|
strm.adler = adler32(strm.adler, dictionary, dictLength, 0);
|
|
40195
40195
|
}
|
|
40196
40196
|
s.wrap = 0;
|
|
40197
40197
|
if (dictLength >= s.w_size) {
|
|
40198
|
-
if (
|
|
40198
|
+
if (wrap2 === 0) {
|
|
40199
40199
|
zero(s.head);
|
|
40200
40200
|
s.strstart = 0;
|
|
40201
40201
|
s.block_start = 0;
|
|
@@ -40235,7 +40235,7 @@ var require_deflate = __commonJS({
|
|
|
40235
40235
|
strm.next_in = next;
|
|
40236
40236
|
strm.input = input;
|
|
40237
40237
|
strm.avail_in = avail;
|
|
40238
|
-
s.wrap =
|
|
40238
|
+
s.wrap = wrap2;
|
|
40239
40239
|
return Z_OK;
|
|
40240
40240
|
}
|
|
40241
40241
|
exports2.deflateInit = deflateInit;
|
|
@@ -41255,17 +41255,17 @@ var require_inflate = __commonJS({
|
|
|
41255
41255
|
return inflateResetKeep(strm);
|
|
41256
41256
|
}
|
|
41257
41257
|
function inflateReset2(strm, windowBits) {
|
|
41258
|
-
var
|
|
41258
|
+
var wrap2;
|
|
41259
41259
|
var state;
|
|
41260
41260
|
if (!strm || !strm.state) {
|
|
41261
41261
|
return Z_STREAM_ERROR;
|
|
41262
41262
|
}
|
|
41263
41263
|
state = strm.state;
|
|
41264
41264
|
if (windowBits < 0) {
|
|
41265
|
-
|
|
41265
|
+
wrap2 = 0;
|
|
41266
41266
|
windowBits = -windowBits;
|
|
41267
41267
|
} else {
|
|
41268
|
-
|
|
41268
|
+
wrap2 = (windowBits >> 4) + 1;
|
|
41269
41269
|
if (windowBits < 48) {
|
|
41270
41270
|
windowBits &= 15;
|
|
41271
41271
|
}
|
|
@@ -41276,7 +41276,7 @@ var require_inflate = __commonJS({
|
|
|
41276
41276
|
if (state.window !== null && state.wbits !== windowBits) {
|
|
41277
41277
|
state.window = null;
|
|
41278
41278
|
}
|
|
41279
|
-
state.wrap =
|
|
41279
|
+
state.wrap = wrap2;
|
|
41280
41280
|
state.wbits = windowBits;
|
|
41281
41281
|
return inflateReset(strm);
|
|
41282
41282
|
}
|
|
@@ -43147,18 +43147,18 @@ var require_object = __commonJS({
|
|
|
43147
43147
|
var object2 = new ZipObject(name2, zipObjectContent, o);
|
|
43148
43148
|
this.files[name2] = object2;
|
|
43149
43149
|
};
|
|
43150
|
-
var parentFolder = function(
|
|
43151
|
-
if (
|
|
43152
|
-
|
|
43150
|
+
var parentFolder = function(path14) {
|
|
43151
|
+
if (path14.slice(-1) === "/") {
|
|
43152
|
+
path14 = path14.substring(0, path14.length - 1);
|
|
43153
43153
|
}
|
|
43154
|
-
var lastSlash =
|
|
43155
|
-
return lastSlash > 0 ?
|
|
43154
|
+
var lastSlash = path14.lastIndexOf("/");
|
|
43155
|
+
return lastSlash > 0 ? path14.substring(0, lastSlash) : "";
|
|
43156
43156
|
};
|
|
43157
|
-
var forceTrailingSlash = function(
|
|
43158
|
-
if (
|
|
43159
|
-
|
|
43157
|
+
var forceTrailingSlash = function(path14) {
|
|
43158
|
+
if (path14.slice(-1) !== "/") {
|
|
43159
|
+
path14 += "/";
|
|
43160
43160
|
}
|
|
43161
|
-
return
|
|
43161
|
+
return path14;
|
|
43162
43162
|
};
|
|
43163
43163
|
var folderAdd = function(name2, createFolders) {
|
|
43164
43164
|
createFolders = typeof createFolders !== "undefined" ? createFolders : defaults2.createFolders;
|
|
@@ -44162,7 +44162,7 @@ import dns from "node:dns";
|
|
|
44162
44162
|
import net2 from "node:net";
|
|
44163
44163
|
import fs15 from "node:fs";
|
|
44164
44164
|
import os4 from "node:os";
|
|
44165
|
-
import
|
|
44165
|
+
import path13 from "node:path";
|
|
44166
44166
|
import readline from "node:readline";
|
|
44167
44167
|
|
|
44168
44168
|
// ../shared/src/transport_error.ts
|
|
@@ -44531,8 +44531,8 @@ var LoticsClient = class {
|
|
|
44531
44531
|
headers["x-request-id"] = newRequestId();
|
|
44532
44532
|
return headers;
|
|
44533
44533
|
}
|
|
44534
|
-
async request(method,
|
|
44535
|
-
const url2 = `${this.baseUrl}${
|
|
44534
|
+
async request(method, path14, body) {
|
|
44535
|
+
const url2 = `${this.baseUrl}${path14}`;
|
|
44536
44536
|
const headers = this.buildHeaders();
|
|
44537
44537
|
const init = { method, headers };
|
|
44538
44538
|
if (body !== void 0) {
|
|
@@ -46001,7 +46001,7 @@ function resultSideEffects(result) {
|
|
|
46001
46001
|
}
|
|
46002
46002
|
|
|
46003
46003
|
// src/version.ts
|
|
46004
|
-
var VERSION = "0.
|
|
46004
|
+
var VERSION = "0.191.1";
|
|
46005
46005
|
|
|
46006
46006
|
// src/timezone.ts
|
|
46007
46007
|
function machineTimezone() {
|
|
@@ -46110,6 +46110,167 @@ function matchDocArea(areas, query) {
|
|
|
46110
46110
|
if (exact.length > 0) return exact;
|
|
46111
46111
|
return areas.filter((a) => a.area.toLowerCase().includes(q));
|
|
46112
46112
|
}
|
|
46113
|
+
var PAGE_BYTES = 16e3;
|
|
46114
|
+
var NAV_BYTES = PAGE_BYTES / 2;
|
|
46115
|
+
var CUT_MARK = "\n[cut \u2014 the whole document is at the path above]";
|
|
46116
|
+
var slugOf = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
46117
|
+
function docSections(text) {
|
|
46118
|
+
const lines = text.split("\n");
|
|
46119
|
+
const found = [];
|
|
46120
|
+
let fenced = false;
|
|
46121
|
+
lines.forEach((line, index) => {
|
|
46122
|
+
if (line.startsWith("```")) {
|
|
46123
|
+
fenced = !fenced;
|
|
46124
|
+
return;
|
|
46125
|
+
}
|
|
46126
|
+
if (fenced) return;
|
|
46127
|
+
const m = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
|
|
46128
|
+
if (m === null) return;
|
|
46129
|
+
const level = m[1].length;
|
|
46130
|
+
for (const open of found) {
|
|
46131
|
+
if (open.end === lines.length && open.level >= level) open.end = index;
|
|
46132
|
+
}
|
|
46133
|
+
found.push({ level, title: m[2], slug: slugOf(m[2]), line: index, end: lines.length });
|
|
46134
|
+
});
|
|
46135
|
+
return found;
|
|
46136
|
+
}
|
|
46137
|
+
function docRows(text) {
|
|
46138
|
+
const found = [];
|
|
46139
|
+
let fenced = false;
|
|
46140
|
+
let header = true;
|
|
46141
|
+
text.split("\n").forEach((line, index) => {
|
|
46142
|
+
if (line.startsWith("```")) {
|
|
46143
|
+
fenced = !fenced;
|
|
46144
|
+
return;
|
|
46145
|
+
}
|
|
46146
|
+
if (fenced) return;
|
|
46147
|
+
if (!line.startsWith("|")) {
|
|
46148
|
+
header = true;
|
|
46149
|
+
return;
|
|
46150
|
+
}
|
|
46151
|
+
const cells = line.split("|");
|
|
46152
|
+
if (cells.length < 3) return;
|
|
46153
|
+
const first3 = cells[1].trim();
|
|
46154
|
+
if (/^:?-{2,}:?$/.test(first3)) return;
|
|
46155
|
+
if (header) {
|
|
46156
|
+
header = false;
|
|
46157
|
+
return;
|
|
46158
|
+
}
|
|
46159
|
+
const key = first3.replace(/[`*[\]]/g, "").trim();
|
|
46160
|
+
if (key === "") return;
|
|
46161
|
+
found.push({ key, slug: slugOf(key), line: index });
|
|
46162
|
+
});
|
|
46163
|
+
return found;
|
|
46164
|
+
}
|
|
46165
|
+
var byteLength = (s) => Buffer.byteLength(s, "utf-8");
|
|
46166
|
+
var oneNewline = (s) => `${s.replace(/\n+$/, "")}
|
|
46167
|
+
`;
|
|
46168
|
+
var wrap = (names, width = 92) => names.reduce((out, name2) => {
|
|
46169
|
+
const last3 = out[out.length - 1];
|
|
46170
|
+
if (last3 !== void 0 && last3.length + name2.length + 1 <= width) {
|
|
46171
|
+
out[out.length - 1] = `${last3} ${name2}`;
|
|
46172
|
+
return out;
|
|
46173
|
+
}
|
|
46174
|
+
return [...out, ` ${name2}`];
|
|
46175
|
+
}, []);
|
|
46176
|
+
var fit = (lines, budget) => {
|
|
46177
|
+
const kept = [];
|
|
46178
|
+
let used = 0;
|
|
46179
|
+
for (const line of lines) {
|
|
46180
|
+
const cost = byteLength(line) + 1;
|
|
46181
|
+
if (used + cost > budget) break;
|
|
46182
|
+
kept.push(line);
|
|
46183
|
+
used += cost;
|
|
46184
|
+
}
|
|
46185
|
+
return { kept, dropped: lines.length - kept.length };
|
|
46186
|
+
};
|
|
46187
|
+
var childrenOf = (sections, parent) => {
|
|
46188
|
+
const within = parent === void 0 ? sections : sections.filter((s) => s.line > parent.line && s.line < parent.end);
|
|
46189
|
+
if (within.length === 0) return [];
|
|
46190
|
+
const top = Math.min(...within.map((s) => s.level));
|
|
46191
|
+
return within.filter((s) => s.level === top);
|
|
46192
|
+
};
|
|
46193
|
+
var linesOf = (lines, section) => section === void 0 ? lines : lines.slice(section.line, section.end);
|
|
46194
|
+
var tableTop = (lines, rowLine) => {
|
|
46195
|
+
for (let i2 = rowLine; i2 > 0; i2--) {
|
|
46196
|
+
if (!lines[i2].startsWith("|")) return i2 + 1;
|
|
46197
|
+
}
|
|
46198
|
+
return 0;
|
|
46199
|
+
};
|
|
46200
|
+
var tableHeader = (lines, rowLine) => {
|
|
46201
|
+
for (let i2 = rowLine; i2 > 0; i2--) {
|
|
46202
|
+
if (/^\|\s*:?-{2,}/.test(lines[i2])) return i2 > 0 ? [lines[i2 - 1], lines[i2]] : [];
|
|
46203
|
+
if (!lines[i2].startsWith("|")) return [];
|
|
46204
|
+
}
|
|
46205
|
+
return [];
|
|
46206
|
+
};
|
|
46207
|
+
function renderDocPage(text, args) {
|
|
46208
|
+
const lines = text.split("\n");
|
|
46209
|
+
const sections = docSections(text);
|
|
46210
|
+
const rows = docRows(text);
|
|
46211
|
+
const address = (slug) => ` lotics docs ${args.address}/${slug}`;
|
|
46212
|
+
const navFor = (parent2) => {
|
|
46213
|
+
const children2 = childrenOf(sections, parent2);
|
|
46214
|
+
if (children2.length > 0) {
|
|
46215
|
+
return children2.map(
|
|
46216
|
+
(s) => `${address(s.slug)} (${byteLength(linesOf(lines, s).join("\n"))} bytes)`
|
|
46217
|
+
);
|
|
46218
|
+
}
|
|
46219
|
+
const within = parent2 === void 0 ? rows : rows.filter((r) => r.line > parent2.line && r.line < parent2.end);
|
|
46220
|
+
if (within.length === 0) return [];
|
|
46221
|
+
return [` lotics docs ${args.address}/<name>, where <name> is one of:`, "", ...wrap(within.map((r) => r.slug))];
|
|
46222
|
+
};
|
|
46223
|
+
const titled = sections.length > 0 && sections[0].level === 1 ? sections[0] : void 0;
|
|
46224
|
+
const listed = (nav2) => {
|
|
46225
|
+
const shown = fit(nav2, NAV_BYTES);
|
|
46226
|
+
return [...shown.kept, ...shown.dropped > 0 ? [` \u2026 and ${shown.dropped} more`] : []].join(
|
|
46227
|
+
"\n"
|
|
46228
|
+
);
|
|
46229
|
+
};
|
|
46230
|
+
let current;
|
|
46231
|
+
if (args.section !== void 0 && args.section !== "") {
|
|
46232
|
+
const q = slugOf(args.section);
|
|
46233
|
+
const pick4 = (all) => {
|
|
46234
|
+
const exact = all.filter((s) => s.slug === q);
|
|
46235
|
+
return exact.length > 0 ? exact : all.filter((s) => s.slug.startsWith(q));
|
|
46236
|
+
};
|
|
46237
|
+
const hitSections = pick4(sections.filter((s) => s.level > 1));
|
|
46238
|
+
const hitRows = hitSections.length > 0 ? [] : pick4(rows);
|
|
46239
|
+
if (hitSections.length + hitRows.length !== 1) {
|
|
46240
|
+
const ambiguous = [...hitSections.map((s) => s.slug), ...hitRows.map((r) => r.slug)];
|
|
46241
|
+
const list2 = listed(ambiguous.length > 1 ? ambiguous.map(address) : navFor(titled));
|
|
46242
|
+
return {
|
|
46243
|
+
error: ambiguous.length > 1 ? `"${args.section}" names more than one part of ${args.address}:
|
|
46244
|
+
${list2}` : `No "${args.section}" in ${args.address}. It holds:
|
|
46245
|
+
${list2}`
|
|
46246
|
+
};
|
|
46247
|
+
}
|
|
46248
|
+
const row = hitRows[0];
|
|
46249
|
+
if (row !== void 0) {
|
|
46250
|
+
return { text: oneNewline([...tableHeader(lines, row.line), lines[row.line]].join("\n")) };
|
|
46251
|
+
}
|
|
46252
|
+
current = hitSections[0];
|
|
46253
|
+
}
|
|
46254
|
+
const whole = linesOf(lines, current).join("\n");
|
|
46255
|
+
if (byteLength(whole) <= PAGE_BYTES) return { text: oneNewline(whole) };
|
|
46256
|
+
const parent = current ?? titled;
|
|
46257
|
+
const nav = navFor(parent);
|
|
46258
|
+
const navText = nav.length === 0 ? "" : `
|
|
46259
|
+
|
|
46260
|
+
This is ${byteLength(whole)} bytes; one page is ${PAGE_BYTES}. It holds:
|
|
46261
|
+
|
|
46262
|
+
${listed(nav)}`;
|
|
46263
|
+
const children = childrenOf(sections, parent);
|
|
46264
|
+
const rowsWithin = parent === void 0 ? rows : rows.filter((r) => r.line > parent.line && r.line < parent.end);
|
|
46265
|
+
const openingTo = children.length > 0 ? children[0].line : rowsWithin.length > 0 ? tableTop(lines, rowsWithin[0].line) : current?.end ?? lines.length;
|
|
46266
|
+
const head = fit(
|
|
46267
|
+
lines.slice(current?.line ?? 0, openingTo),
|
|
46268
|
+
PAGE_BYTES - byteLength(navText) - byteLength(CUT_MARK) - 1
|
|
46269
|
+
);
|
|
46270
|
+
return {
|
|
46271
|
+
text: oneNewline(`${head.kept.join("\n")}${head.dropped > 0 ? CUT_MARK : ""}${navText}`)
|
|
46272
|
+
};
|
|
46273
|
+
}
|
|
46113
46274
|
function onlySelfReference(areas) {
|
|
46114
46275
|
return areas.length > 0 && areas.every((a) => a.pkg === "@lotics/cli");
|
|
46115
46276
|
}
|
|
@@ -46129,6 +46290,7 @@ ${a.pkg} ${a.version}`);
|
|
|
46129
46290
|
lines.push(` ${name2.length >= 24 ? `${name2} ` : name2.padEnd(24)}${a.title}`);
|
|
46130
46291
|
}
|
|
46131
46292
|
lines.push("\n lotics docs <area> print one, e.g. `lotics docs ai`");
|
|
46293
|
+
lines.push(" lotics docs <area>/<section> one section \u2014 a long doc prints its section list");
|
|
46132
46294
|
lines.push(" lotics docs <pkg>/<area> when two packages share a name");
|
|
46133
46295
|
lines.push("\n Versions are the INSTALLED ones; a doc describes the version beside it.");
|
|
46134
46296
|
if (onlySelfReference(areas)) {
|
|
@@ -46145,7 +46307,17 @@ function docsCommand(args) {
|
|
|
46145
46307
|
console.log(renderDocIndex(areas));
|
|
46146
46308
|
return;
|
|
46147
46309
|
}
|
|
46148
|
-
const
|
|
46310
|
+
const asked = args.area.trim();
|
|
46311
|
+
let hits = matchDocArea(areas, asked);
|
|
46312
|
+
let section;
|
|
46313
|
+
if (hits.length === 0 && asked.includes("/")) {
|
|
46314
|
+
const cut = asked.lastIndexOf("/");
|
|
46315
|
+
const head = matchDocArea(areas, asked.slice(0, cut));
|
|
46316
|
+
if (head.length > 0) {
|
|
46317
|
+
hits = head;
|
|
46318
|
+
section = asked.slice(cut + 1);
|
|
46319
|
+
}
|
|
46320
|
+
}
|
|
46149
46321
|
if (hits.length === 0) {
|
|
46150
46322
|
console.error(
|
|
46151
46323
|
`No reference area matching "${args.area}".
|
|
@@ -46156,14 +46328,22 @@ function docsCommand(args) {
|
|
|
46156
46328
|
if (hits.length > 1) {
|
|
46157
46329
|
console.error(
|
|
46158
46330
|
`"${args.area}" matches more than one reference \u2014 name the package:
|
|
46159
|
-
` + hits.map(
|
|
46331
|
+
` + hits.map(
|
|
46332
|
+
(a) => ` \u2022 lotics docs ${qualified(a)}${section === void 0 ? "" : `/${section}`} (${a.pkg} ${a.version})`
|
|
46333
|
+
).join("\n")
|
|
46160
46334
|
);
|
|
46161
46335
|
process.exit(1);
|
|
46162
46336
|
}
|
|
46163
46337
|
const hit = hits[0];
|
|
46338
|
+
const address = areas.filter((a) => a.area === hit.area).length > 1 ? qualified(hit) : hit.area;
|
|
46339
|
+
const page = renderDocPage(fs5.readFileSync(hit.absPath, "utf-8"), { address, section });
|
|
46340
|
+
if ("error" in page) {
|
|
46341
|
+
console.error(page.error);
|
|
46342
|
+
process.exit(1);
|
|
46343
|
+
}
|
|
46164
46344
|
console.error(`${hit.pkg} ${hit.version} \xB7 ${path5.relative(projectDir, hit.absPath)}
|
|
46165
46345
|
`);
|
|
46166
|
-
process.stdout.write(
|
|
46346
|
+
process.stdout.write(page.text);
|
|
46167
46347
|
}
|
|
46168
46348
|
|
|
46169
46349
|
// src/commands.ts
|
|
@@ -46201,9 +46381,10 @@ var COMMANDS = [
|
|
|
46201
46381
|
" and a worked example. Offline, no account",
|
|
46202
46382
|
" lotics scaffold check <file> Prove a model before anyone sees it \u2014 offline, no",
|
|
46203
46383
|
" account, no network. Reports EVERY problem in one",
|
|
46204
|
-
" run; exits 1 if there is one, else
|
|
46205
|
-
"
|
|
46206
|
-
"
|
|
46384
|
+
" run; exits 1 if there is one, else what it would",
|
|
46385
|
+
" create, every planned screen with the field in each",
|
|
46386
|
+
" slot, and what the first rows would show. --json",
|
|
46387
|
+
" prints the counts and the plan, or the findings",
|
|
46207
46388
|
" lotics scaffold apply <file> Create the model's tables, fields, options, views,",
|
|
46208
46389
|
" roles and first rows in this workspace, then copy in",
|
|
46209
46390
|
" every package its apply list names. Additive:",
|
|
@@ -46294,7 +46475,8 @@ var COMMANDS = [
|
|
|
46294
46475
|
verbs: ["docs"],
|
|
46295
46476
|
help: [
|
|
46296
46477
|
" lotics docs List the reference docs the npm packages in node_modules ship",
|
|
46297
|
-
" lotics docs <area> Print one (e.g. 'lotics docs ai')"
|
|
46478
|
+
" lotics docs <area> Print one, capped at a page (e.g. 'lotics docs ai')",
|
|
46479
|
+
" lotics docs <area>/<section> One section, or one row of a table doc"
|
|
46298
46480
|
]
|
|
46299
46481
|
},
|
|
46300
46482
|
{
|
|
@@ -46350,7 +46532,7 @@ var COMMANDS = [
|
|
|
46350
46532
|
" offer one",
|
|
46351
46533
|
" lotics app codegen [path] Regenerate .lotics/* (types + field/option ids)",
|
|
46352
46534
|
" from the manifest + workspace schema \u2014 no deploy",
|
|
46353
|
-
" lotics app check
|
|
46535
|
+
" lotics app check [--screens] Run every deploy pre-flight WITHOUT building or",
|
|
46354
46536
|
" shipping: the app's own typecheck over regenerated",
|
|
46355
46537
|
" .lotics types, agent schemas vs the live app, workflow",
|
|
46356
46538
|
" declarations and BODIES vs what is live, aliases",
|
|
@@ -46360,6 +46542,9 @@ var COMMANDS = [
|
|
|
46360
46542
|
" the pulled types, and a concrete id or an",
|
|
46361
46543
|
" id-shaped placeholder in source a starter",
|
|
46362
46544
|
" would ship.",
|
|
46545
|
+
" --screens also renders the app headless (needs",
|
|
46546
|
+
" Chrome) at 1280 and 375 and measures every screen",
|
|
46547
|
+
" against the review probes (lotics docs cli_reference).",
|
|
46363
46548
|
" Exits 1 on any of it, so CI can gate on it",
|
|
46364
46549
|
" lotics app workflow set <alias> Push the edited src/workflows/<alias>.ts body",
|
|
46365
46550
|
" through set_app_workflow (server verifies)",
|
|
@@ -46437,7 +46622,9 @@ var COMMANDS = [
|
|
|
46437
46622
|
" lotics file preview <file|fil_id> [-o png]",
|
|
46438
46623
|
" Render a .docx/.xlsx to a PNG \u2014 a local path OR a",
|
|
46439
46624
|
" stored file id (downloaded first). Frontend engines;",
|
|
46440
|
-
" needs a Chrome/Chromium on the machine"
|
|
46625
|
+
" needs a Chrome/Chromium on the machine. A .html",
|
|
46626
|
+
" renders as the page it is, sized to its content \u2014",
|
|
46627
|
+
" the way a demo's paper props are made"
|
|
46441
46628
|
]
|
|
46442
46629
|
}
|
|
46443
46630
|
];
|
|
@@ -46628,12 +46815,11 @@ async function reportCommand(client, options) {
|
|
|
46628
46815
|
// src/app_commands.ts
|
|
46629
46816
|
import fs8 from "node:fs";
|
|
46630
46817
|
import path9 from "node:path";
|
|
46631
|
-
import { spawn as
|
|
46818
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
46632
46819
|
import { createHash as createHash3 } from "node:crypto";
|
|
46633
|
-
import { tmpdir } from "node:os";
|
|
46820
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
46634
46821
|
|
|
46635
46822
|
// src/starter_template.ts
|
|
46636
|
-
var STARTER_REACT_NATIVE_VERSION = "0.85.3";
|
|
46637
46823
|
var VITEST_SETUP_FILENAME = "vitest.setup.ts";
|
|
46638
46824
|
var VITEST_SETUP_CONTENT = `// Runs before every test file (wired by vite.config.ts \`test.setupFiles\`).
|
|
46639
46825
|
// Empty by default \u2014 put global test setup here: custom matchers, a fetch stub,
|
|
@@ -46669,7 +46855,7 @@ function buildStarterTemplate(args) {
|
|
|
46669
46855
|
// or a broken vitest config, indefinitely. A detector that cannot
|
|
46670
46856
|
// fire when the thing is ABSENT rather than excessive is not one.
|
|
46671
46857
|
//
|
|
46672
|
-
// What the scaffold seeds instead is `src/harness.test.
|
|
46858
|
+
// What the scaffold seeds instead is `src/harness.test.tsx`, which
|
|
46673
46859
|
// asserts the HARNESS and nothing else. The file it replaces
|
|
46674
46860
|
// (`src/App.test.tsx`) asserted the scaffold's placeholder copy
|
|
46675
46861
|
// ("First item"), so it failed the moment the author did the very
|
|
@@ -46687,15 +46873,14 @@ function buildStarterTemplate(args) {
|
|
|
46687
46873
|
// that IMPORTS @lotics/docx or @lotics/xlsx directly (its own doc/excel
|
|
46688
46874
|
// builders) declares its own dependency then.
|
|
46689
46875
|
"@lotics/ui": uiVersion,
|
|
46690
|
-
|
|
46691
|
-
|
|
46876
|
+
// @base-ui/react is deliberately absent: @lotics/ui declares it as an
|
|
46877
|
+
// EXACT peer, npm installs a peer, and a second declaration here is a
|
|
46878
|
+
// version that drifts from the kit's — which Base UI punishes
|
|
46879
|
+
// silently, since it passes state through React context and a nested
|
|
46880
|
+
// copy renders parts that never open.
|
|
46692
46881
|
"lucide-react": "^0.562.0",
|
|
46693
|
-
"lucide-react-native": "^0.562.0",
|
|
46694
46882
|
react: "^19.0.0",
|
|
46695
46883
|
"react-dom": "^19.0.0",
|
|
46696
|
-
"react-native": STARTER_REACT_NATIVE_VERSION,
|
|
46697
|
-
"react-native-svg": "^15.0.0",
|
|
46698
|
-
"react-native-web": "^0.21.0",
|
|
46699
46884
|
// In-app routing for the scaffolded list→detail example. The app owns
|
|
46700
46885
|
// its routing; the SDK's `isEmbedded()` picks memory vs browser history. `react-router` is the
|
|
46701
46886
|
// canonical package (react-router-dom is a shim over it).
|
|
@@ -46744,9 +46929,10 @@ function buildStarterTemplate(args) {
|
|
|
46744
46929
|
strict: true,
|
|
46745
46930
|
noFallthroughCasesInSwitch: true
|
|
46746
46931
|
},
|
|
46747
|
-
// Restrict tsc to the project's own sources
|
|
46748
|
-
//
|
|
46749
|
-
//
|
|
46932
|
+
// Restrict tsc to the project's own sources — a `noEmit` program has
|
|
46933
|
+
// no reason to walk anything else, and under the `LOTICS_UI_SRC`
|
|
46934
|
+
// dev-link `@lotics/ui` resolves to a working copy of the kit's own
|
|
46935
|
+
// .tsx, which strict rules (noUnusedLocals etc.) would then fire on.
|
|
46750
46936
|
// `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
|
|
46751
46937
|
// SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
|
|
46752
46938
|
// generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
|
|
@@ -46774,87 +46960,25 @@ function buildStarterTemplate(args) {
|
|
|
46774
46960
|
path: "vite.config.ts",
|
|
46775
46961
|
content: `/// <reference types="vitest" />
|
|
46776
46962
|
import { defineConfig } from "vite";
|
|
46777
|
-
import type { Plugin } from "vite";
|
|
46778
46963
|
import react from "@vitejs/plugin-react";
|
|
46779
|
-
import {
|
|
46780
|
-
|
|
46781
|
-
// @lotics/ui/icon.tsx deep-imports \`lucide-react-native/dist/esm/icons/<name>\`.
|
|
46782
|
-
// lucide-react-native's \`exports\` map lists only "." and "./icons", so a strict
|
|
46783
|
-
// resolver (the sandbox's vite dev optimizer) blocks the deep path with
|
|
46784
|
-
// \`Missing "./dist/esm/icons/<name>" specifier\`. lucide-react ships the same
|
|
46785
|
-
// per-icon files with NO exports map, so redirect there. A \`resolveId\` PLUGIN
|
|
46786
|
-
// (not \`resolve.alias\`) is required: the dep-optimizer SCAN runs plugin
|
|
46787
|
-
// resolveId hooks but does NOT apply regex \`resolve.alias\`, so the alias alone
|
|
46788
|
-
// fixes the production build but leaves the dev preview broken.
|
|
46789
|
-
function lucideIconsWebAlias(): Plugin {
|
|
46790
|
-
return {
|
|
46791
|
-
name: "lucide-icons-web-alias",
|
|
46792
|
-
enforce: "pre",
|
|
46793
|
-
async resolveId(source, importer, options) {
|
|
46794
|
-
const m = /^lucide-react-native\\/dist\\/esm\\/icons\\/(.+)$/.exec(source);
|
|
46795
|
-
if (!m) return null;
|
|
46796
|
-
return this.resolve(\`lucide-react/dist/esm/icons/\${m[1]}\`, importer, {
|
|
46797
|
-
...options,
|
|
46798
|
-
skipSelf: true,
|
|
46799
|
-
});
|
|
46800
|
-
},
|
|
46801
|
-
};
|
|
46802
|
-
}
|
|
46964
|
+
import { loticsResolve } from "@lotics/ui/vite";
|
|
46803
46965
|
|
|
46804
46966
|
// Vite default base (/) emits absolute asset URLs in index.html. Lotics's
|
|
46805
46967
|
// render endpoint rewrites those to /v1/apps/{id}/asset/... so the bundle
|
|
46806
46968
|
// loads via the platform's asset proxy. Don't change \`base\` unless you
|
|
46807
46969
|
// also adjust the rewrite logic in backend/api/apps.ts:rewriteAssetPaths.
|
|
46808
46970
|
export default defineConfig({
|
|
46809
|
-
plugins: [
|
|
46810
|
-
//
|
|
46811
|
-
//
|
|
46812
|
-
//
|
|
46813
|
-
//
|
|
46814
|
-
// interrupted/torn down \u2014 e.g. toggling @lotics/ui's Switch. rollup hits it;
|
|
46815
|
-
// the dev esbuild path masks it, so it only surfaces in the deployed app.
|
|
46816
|
-
// rn-web only reads \`global.x\` as a free var, so mapping it to \`globalThis\`
|
|
46817
|
-
// is safe. Define both so dev and the deployed build behave identically.
|
|
46818
|
-
define: { __DEV__: "false", global: "globalThis" },
|
|
46819
|
-
// The kit owns its own resolution \u2014 the react-native \u2192 react-native-web alias,
|
|
46820
|
-
// \`.web.tsx\`-first extensions, the React dedupe, and the \`LOTICS_UI_SRC\`
|
|
46821
|
-
// dev-link \u2014 for the same reason it owns \`loticsOptimizeDeps\`: every entry is
|
|
46822
|
-
// dictated by @lotics/ui's internals, so shipping it WITH the kit means it can
|
|
46823
|
-
// never drift from the version installed here. It also stops being an app's job
|
|
46824
|
-
// to hand-carry a load-bearing alias that a regex edit twice deleted (GAP-133/142).
|
|
46971
|
+
plugins: [react()],
|
|
46972
|
+
// The kit owns this block: the \`LOTICS_UI_SRC\` dev-link and the React dedupe
|
|
46973
|
+
// are decided by @lotics/ui's own delivery, so shipping it WITH the kit means
|
|
46974
|
+
// it can never drift from the version installed here \u2014 and it stops being an
|
|
46975
|
+
// app's job to hand-carry a line a regex edit twice deleted (GAP-133/142).
|
|
46825
46976
|
// \`lotics app codegen\` writes the matching \`paths\` into
|
|
46826
|
-
// .lotics/tsconfig.link.json, so tsc/vitest/eslint/your editor resolve the
|
|
46827
|
-
// @lotics/ui this does. To add your own alias, spread it:
|
|
46977
|
+
// .lotics/tsconfig.link.json, so tsc/vitest/eslint/your editor resolve the
|
|
46978
|
+
// same @lotics/ui this does. To add your own alias, spread it:
|
|
46828
46979
|
// const base = loticsResolve();
|
|
46829
46980
|
// resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
|
|
46830
46981
|
resolve: loticsResolve(),
|
|
46831
|
-
optimizeDeps: {
|
|
46832
|
-
// The dev dep-optimizer must pre-bundle @lotics/ui's RN-ecosystem + markdown
|
|
46833
|
-
// CJS-interop deps or \`lotics app dev\` blanks the iframe ("does not provide an
|
|
46834
|
-
// export named 'default'/'parse'"). Dev-ONLY: the prod rollup build resolves the
|
|
46835
|
-
// interop without it, so typecheck/lint/build stay green while the dev iframe
|
|
46836
|
-
// goes blank. The canonical list ships WITH @lotics/ui as \`loticsOptimizeDeps\`
|
|
46837
|
-
// (imported above) so it can never drift from what the kit's transitive deps
|
|
46838
|
-
// require across a major bump \u2014 the per-family rationale lives in that module.
|
|
46839
|
-
// To add app-specific entries, spread: include: [...loticsOptimizeDeps, "my-dep"].
|
|
46840
|
-
include: loticsOptimizeDeps,
|
|
46841
|
-
// The kit is ONE module graph, so it must never be pre-bundled: the optimizer
|
|
46842
|
-
// auto-discovers each @lotics/ui subpath and inlines a private copy of every
|
|
46843
|
-
// context-owning module it reaches (locale, tooltip, portal, popover), leaving
|
|
46844
|
-
// the provider filling one context and the component reading another.
|
|
46845
|
-
exclude: loticsOptimizeExclude,
|
|
46846
|
-
// The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
|
|
46847
|
-
// top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
|
|
46848
|
-
// still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
|
|
46849
|
-
// here too \u2014 belt-and-suspenders with the top-level \`define\` above.
|
|
46850
|
-
// The optimizer pre-bundles with a SEPARATE esbuild resolver that ignores
|
|
46851
|
-
// \`resolve.extensions\`, so \`.web.js\` must be repeated here or a pre-bundled
|
|
46852
|
-
// RN dep still resolves \`./Picker\` to the native build.
|
|
46853
|
-
esbuildOptions: {
|
|
46854
|
-
define: { __DEV__: "false", global: "globalThis" },
|
|
46855
|
-
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json", ".mjs", ".mts"],
|
|
46856
|
-
},
|
|
46857
|
-
},
|
|
46858
46982
|
build: {
|
|
46859
46983
|
outDir: "dist",
|
|
46860
46984
|
// No source maps in the production build: they ship the original source
|
|
@@ -46880,11 +47004,6 @@ export default defineConfig({
|
|
|
46880
47004
|
// cross-site WS hijack fix) \u2014 without this entry the proxied preview page
|
|
46881
47005
|
// loads over HTTP but HMR never connects, so edits stop live-updating.
|
|
46882
47006
|
allowedHosts: [".lotics-sandbox.app"],
|
|
46883
|
-
// @lotics/ui/fonts.css references the API's /iframe/fonts/*.woff2 files
|
|
46884
|
-
// by root-relative URL. A deployed app is served from the API origin so
|
|
46885
|
-
// they resolve directly; under \`lotics app dev\` the app runs on
|
|
46886
|
-
// localhost, so proxy /iframe to the API to load the real fonts.
|
|
46887
|
-
proxy: { "/iframe": { target: "https://api.lotics.ai", changeOrigin: true } },
|
|
46888
47007
|
},
|
|
46889
47008
|
test: {
|
|
46890
47009
|
environment: "jsdom",
|
|
@@ -46893,39 +47012,31 @@ export default defineConfig({
|
|
|
46893
47012
|
// EXIST either way: setupFiles naming a missing file makes vitest fail to
|
|
46894
47013
|
// load every test. See vitest.setup.ts.
|
|
46895
47014
|
setupFiles: ["./${VITEST_SETUP_FILENAME}"],
|
|
46896
|
-
//
|
|
46897
|
-
//
|
|
46898
|
-
//
|
|
46899
|
-
//
|
|
46900
|
-
//
|
|
46901
|
-
//
|
|
46902
|
-
//
|
|
46903
|
-
//
|
|
47015
|
+
// @lotics/ui ships built ESM whose every component module carries the
|
|
47016
|
+
// literal \`import "./button.css"\` that delivers its sheet. Vitest hands
|
|
47017
|
+
// node_modules to Node's ESM loader, which has no loader for \`.css\` \u2014 so
|
|
47018
|
+
// without this line EVERY suite that touches a kit component fails to load
|
|
47019
|
+
// with \`Unknown file extension ".css"\`, before a single assertion runs.
|
|
47020
|
+
// Inlining routes those modules through Vite's transform instead, where a
|
|
47021
|
+
// CSS import is inert under jsdom. Any package whose modules import CSS
|
|
47022
|
+
// needs the same entry.
|
|
47023
|
+
server: { deps: { inline: [/@lotics\\/ui/] } },
|
|
46904
47024
|
deps: {
|
|
46905
47025
|
optimizer: {
|
|
46906
47026
|
web: {
|
|
46907
47027
|
enabled: true,
|
|
46908
|
-
//
|
|
46909
|
-
//
|
|
46910
|
-
//
|
|
46911
|
-
//
|
|
46912
|
-
//
|
|
47028
|
+
// Everything here ships COMPILED JS, so vitest pre-bundles each into
|
|
47029
|
+
// its own chunk \u2014 and a chunk carrying its own react import splits the
|
|
47030
|
+
// instance and nulls the hooks dispatcher the moment a rendered tree
|
|
47031
|
+
// calls one ("Cannot read properties of null (reading 'useState')").
|
|
47032
|
+
// They ride in ONE optimized chunk with react and the renderer instead.
|
|
46913
47033
|
include: [
|
|
46914
47034
|
"react",
|
|
46915
47035
|
"react-dom",
|
|
46916
47036
|
"react-dom/client",
|
|
46917
47037
|
"react-dom/test-utils",
|
|
46918
|
-
"react-native",
|
|
46919
|
-
"react-native-web",
|
|
46920
|
-
"react-native-svg",
|
|
46921
|
-
"@react-native-picker/picker",
|
|
46922
47038
|
"@testing-library/react",
|
|
46923
47039
|
"@testing-library/dom",
|
|
46924
|
-
// @lotics/app-sdk ships compiled dist/ JS (unlike @lotics/ui, which
|
|
46925
|
-
// ships source vitest transforms inline) \u2014 pre-bundled into its own
|
|
46926
|
-
// chunk, its react import splits from the shared instance and nulls
|
|
46927
|
-
// the hooks dispatcher the instant a hook (useAgentRun / useQuery)
|
|
46928
|
-
// runs in a rendered App tree.
|
|
46929
47040
|
"@lotics/app-sdk",
|
|
46930
47041
|
// The /router subpath is a SEPARATE optimizer entry \u2014 without it,
|
|
46931
47042
|
// AppRouter's BrowserRouter bundles apart from the app's
|
|
@@ -46937,9 +47048,6 @@ export default defineConfig({
|
|
|
46937
47048
|
// ("Invalid hook call") when a test renders the routed App tree.
|
|
46938
47049
|
"react-router",
|
|
46939
47050
|
],
|
|
46940
|
-
esbuildOptions: {
|
|
46941
|
-
resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json", ".mjs", ".mts"],
|
|
46942
|
-
},
|
|
46943
47051
|
},
|
|
46944
47052
|
},
|
|
46945
47053
|
},
|
|
@@ -46963,8 +47071,8 @@ export default defineConfig({
|
|
|
46963
47071
|
* default of html/body being content-sized collapses every \`flex: 1\`
|
|
46964
47072
|
* to 0 and the surface shrinks to fit only its rendered content.
|
|
46965
47073
|
*
|
|
46966
|
-
* Also: #root is
|
|
46967
|
-
*
|
|
47074
|
+
* Also: #root is a flex column, so App.tsx's outermost \`flex: 1\` box
|
|
47075
|
+
* claims the full iframe height.
|
|
46968
47076
|
*/
|
|
46969
47077
|
html, body, #root { height: 100%; margin: 0; }
|
|
46970
47078
|
#root { display: flex; flex-direction: column; }
|
|
@@ -46979,45 +47087,47 @@ export default defineConfig({
|
|
|
46979
47087
|
},
|
|
46980
47088
|
{
|
|
46981
47089
|
path: "src/main.tsx",
|
|
46982
|
-
content: `import "@lotics/ui/
|
|
47090
|
+
content: `import "@lotics/ui/styles.css";
|
|
46983
47091
|
import "@lotics/ui/fonts.css";
|
|
46984
|
-
import { PortalHost } from "@lotics/ui/portal";
|
|
46985
47092
|
import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
|
|
46986
47093
|
import { mount } from "@lotics/app-sdk";
|
|
46987
47094
|
import App from "./App";
|
|
46988
47095
|
|
|
47096
|
+
// \`styles.css\` is not optional. It carries the cascade-layer order, every design
|
|
47097
|
+
// token and the reset, and the kit's chrome reads those variables with NO inline
|
|
47098
|
+
// fallback \u2014 without it every colour, size and radius is invalid and the screen
|
|
47099
|
+
// renders unstyled but working.
|
|
47100
|
+
//
|
|
46989
47101
|
// THIS APP LOOKS LIKE LOTICS unless you say otherwise, which is the right
|
|
46990
47102
|
// default: the kit's own palette and typeface are what every screen in the
|
|
46991
47103
|
// product wears, and an app that sets nothing belongs with them.
|
|
46992
47104
|
//
|
|
46993
|
-
// If this app carries someone else's brand
|
|
46994
|
-
//
|
|
46995
|
-
// \`
|
|
46996
|
-
//
|
|
46997
|
-
// import { applyLoticsTheme } from "@lotics/ui/theme";
|
|
46998
|
-
// applyLoticsTheme({ primary: "#0F766E" });
|
|
47105
|
+
// If this app carries someone else's brand, a theme is a STYLESHEET \u2014 there is
|
|
47106
|
+
// no theme function. Declare the shadcn/ui variables in your own CSS, imported
|
|
47107
|
+
// after \`styles.css\`; an unlayered rule of yours beats every layered kit rule at
|
|
47108
|
+
// any specificity, with no \`!important\`:
|
|
46999
47109
|
//
|
|
47000
|
-
//
|
|
47001
|
-
//
|
|
47110
|
+
// :root, .light { --primary: #0F766E; --primary-foreground: #fff; --ring: #0F766E; }
|
|
47111
|
+
// .dark { --primary: #5EEAD4; --primary-foreground: #042F2E; --ring: #5EEAD4; }
|
|
47002
47112
|
//
|
|
47003
|
-
//
|
|
47004
|
-
//
|
|
47005
|
-
//
|
|
47006
|
-
// deliberately want a toned surface
|
|
47007
|
-
// border hairlines \u2014 a register is mostly these, so a small change here is
|
|
47008
|
-
// felt against the whole screen
|
|
47009
|
-
// bodyFont the typeface for everything (a CSS stack you know RESOLVES, and
|
|
47010
|
-
// that carries Vietnamese diacritics \u2014 a face without them turns
|
|
47011
|
-
// "Th\u1EA9m \u0111\u1ECBnh" into tofu). Declare its @font-face in your own CSS.
|
|
47012
|
-
// displayFont display type only; defaults to bodyFont
|
|
47113
|
+
// Paste BOTH blocks of whatever palette you start from: a token set only on
|
|
47114
|
+
// \`:root\` keeps its light value in dark mode, because your unlayered rule also
|
|
47115
|
+
// beats the kit's \`.dark\`. The roles worth knowing:
|
|
47013
47116
|
//
|
|
47014
|
-
//
|
|
47015
|
-
//
|
|
47016
|
-
//
|
|
47017
|
-
//
|
|
47117
|
+
// --primary this is on \u2014 the filled button, a checkbox, an active tab's rail
|
|
47118
|
+
// --lotics-accent which of these \u2014 a selected row, an active filter (themed: set
|
|
47119
|
+
// it in both blocks, beside --primary)
|
|
47120
|
+
// --card the raised surface (Card, Drawer, Dialog)
|
|
47121
|
+
// --border hairlines \u2014 a register is mostly these, so a small change
|
|
47122
|
+
// here is felt against the whole screen
|
|
47123
|
+
// --font-sans the typeface for everything (a stack you know RESOLVES, and that
|
|
47124
|
+
// carries Vietnamese diacritics \u2014 a face without them turns
|
|
47125
|
+
// "Th\u1EA9m \u0111\u1ECBnh" into tofu). Declare its @font-face in your own CSS.
|
|
47126
|
+
// --lotics-font-display display type only; defaults to --font-sans
|
|
47018
47127
|
//
|
|
47019
|
-
//
|
|
47020
|
-
//
|
|
47128
|
+
// Everything derived \u2014 the selection wash, hover and press shades, the hairline
|
|
47129
|
+
// ladder \u2014 is computed in CSS from those, so there is nothing to keep in step by
|
|
47130
|
+
// hand. The full contract is \`node_modules/@lotics/ui/docs/theming.md\`.
|
|
47021
47131
|
//
|
|
47022
47132
|
// LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
|
|
47023
47133
|
// headers, select-all, the Drawer's record nav\u2026) in one language \u2014 Vietnamese
|
|
@@ -47025,9 +47135,7 @@ import App from "./App";
|
|
|
47025
47135
|
// LoticsLocale pack) for another language; remove it to fall back to English.
|
|
47026
47136
|
mount(
|
|
47027
47137
|
<LoticsLocaleProvider locale={vi}>
|
|
47028
|
-
<
|
|
47029
|
-
<App />
|
|
47030
|
-
</PortalHost>
|
|
47138
|
+
<App />
|
|
47031
47139
|
</LoticsLocaleProvider>,
|
|
47032
47140
|
);
|
|
47033
47141
|
`
|
|
@@ -47041,21 +47149,22 @@ mount(
|
|
|
47041
47149
|
// host never sees it, so navigation never reloads the app); standalone,
|
|
47042
47150
|
// real path URLs. Browser Back/Forward walk app screens in both.
|
|
47043
47151
|
// - The full-container layout pattern is preserved in `Screen`: an outer
|
|
47044
|
-
//
|
|
47045
|
-
// html/body/#root to 100% + #root is a flex column).
|
|
47046
|
-
//
|
|
47047
|
-
//
|
|
47152
|
+
// `flex: 1` box claims the iframe height (index.html sets
|
|
47153
|
+
// html/body/#root to 100% + #root is a flex column). It is a plain
|
|
47154
|
+
// `<div>` rather than a kit primitive because that chain has to stay
|
|
47155
|
+
// unbroken — `Stack` is the right thing INSIDE it, where the gutter is
|
|
47156
|
+
// what you want, not around a fill-remaining-space child.
|
|
47048
47157
|
// A single-screen app can delete the router and render one Screen directly.
|
|
47049
47158
|
content: `import type { ReactNode } from "react";
|
|
47050
|
-
import { View } from "react-native";
|
|
47051
47159
|
import { useNavigate, useParams } from "react-router";
|
|
47052
47160
|
import { AppRouter } from "@lotics/app-sdk/router";
|
|
47053
47161
|
import { useAiContext } from "@lotics/app-sdk";
|
|
47054
47162
|
import { Card } from "@lotics/ui/card";
|
|
47163
|
+
import { Stack } from "@lotics/ui/stack";
|
|
47055
47164
|
import { Text } from "@lotics/ui/text";
|
|
47056
47165
|
import { Button } from "@lotics/ui/button";
|
|
47057
47166
|
import { BackButton } from "@lotics/ui/back_button";
|
|
47058
|
-
import {
|
|
47167
|
+
import { RegionState } from "@lotics/ui/region_state";
|
|
47059
47168
|
|
|
47060
47169
|
// AppRouter makes the app's screens real URLs \u2014 write plain react-router
|
|
47061
47170
|
// (useNavigate / useParams / <Link>) and it handles both modes. The app owns its
|
|
@@ -47072,14 +47181,16 @@ const ITEMS = [
|
|
|
47072
47181
|
{ id: "3", name: "Third item" },
|
|
47073
47182
|
];
|
|
47074
47183
|
|
|
47075
|
-
//
|
|
47076
|
-
//
|
|
47077
|
-
//
|
|
47184
|
+
// The outer box claims the full iframe height \u2014 it works because index.html sets
|
|
47185
|
+
// html/body/#root to 100% and #root is a flex column. Keep this chain plain: a
|
|
47186
|
+
// box that says nothing about \`flex\` is what breaks it.
|
|
47078
47187
|
function Screen({ children }: { children: ReactNode }) {
|
|
47079
47188
|
return (
|
|
47080
|
-
<
|
|
47081
|
-
<
|
|
47082
|
-
|
|
47189
|
+
<div style={{ flex: 1, padding: 24, display: "flex", justifyContent: "center" }}>
|
|
47190
|
+
<Stack gap={16} style={{ maxWidth: 640, width: "100%" }}>
|
|
47191
|
+
{children}
|
|
47192
|
+
</Stack>
|
|
47193
|
+
</div>
|
|
47083
47194
|
);
|
|
47084
47195
|
}
|
|
47085
47196
|
|
|
@@ -47095,18 +47206,16 @@ function ListScreen() {
|
|
|
47095
47206
|
</Text>
|
|
47096
47207
|
{ITEMS.map((item) => (
|
|
47097
47208
|
<Card key={item.id}>
|
|
47098
|
-
<
|
|
47099
|
-
|
|
47100
|
-
|
|
47101
|
-
|
|
47102
|
-
|
|
47103
|
-
|
|
47104
|
-
gap: 12,
|
|
47105
|
-
}}
|
|
47209
|
+
<Stack
|
|
47210
|
+
direction="row"
|
|
47211
|
+
gap={8}
|
|
47212
|
+
align="center"
|
|
47213
|
+
justify="between"
|
|
47214
|
+
style={{ padding: 16 }}
|
|
47106
47215
|
>
|
|
47107
47216
|
<Text weight="medium">{item.name}</Text>
|
|
47108
47217
|
<Button title="Open" color="primary" onPress={() => navigate("/item/" + item.id)} />
|
|
47109
|
-
</
|
|
47218
|
+
</Stack>
|
|
47110
47219
|
</Card>
|
|
47111
47220
|
))}
|
|
47112
47221
|
</Screen>
|
|
@@ -47140,20 +47249,20 @@ function ItemDetailScreen() {
|
|
|
47140
47249
|
\`<Button title="Back">\` with no \`color\` draws NOTHING at rest \u2014 no
|
|
47141
47250
|
ground, no border, no underline \u2014 so its only affordance is a hover
|
|
47142
47251
|
wash, which touch and keyboard never see. BackButton also carries its
|
|
47143
|
-
own \`alignSelf\`, so it needs no wrapping
|
|
47252
|
+
own \`alignSelf\`, so it needs no wrapping box to sit left. */}
|
|
47144
47253
|
<BackButton onPress={() => navigate(-1)} />
|
|
47145
47254
|
<Text size="xl" weight="semibold">{item ? item.name : "Not found"}</Text>
|
|
47146
47255
|
{/* Every state gets its own content. Swapping only the TITLE left the card
|
|
47147
47256
|
below still describing the record the title had just said was missing. */}
|
|
47148
47257
|
{item ? (
|
|
47149
47258
|
<Card>
|
|
47150
|
-
<
|
|
47259
|
+
<Stack gap={4} style={{ padding: 16 }}>
|
|
47151
47260
|
<Text>Detail for item {id}.</Text>
|
|
47152
47261
|
<Text color="muted">Reached via in-app navigation, not a host route.</Text>
|
|
47153
|
-
</
|
|
47262
|
+
</Stack>
|
|
47154
47263
|
</Card>
|
|
47155
47264
|
) : (
|
|
47156
|
-
<
|
|
47265
|
+
<RegionState state="empty" message={"No item with id " + id + "."} />
|
|
47157
47266
|
)}
|
|
47158
47267
|
</Screen>
|
|
47159
47268
|
);
|
|
@@ -47224,25 +47333,6 @@ alias the binding is keyed by).
|
|
|
47224
47333
|
capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
|
|
47225
47334
|
(which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
|
|
47226
47335
|
each alias lives in \`package.json#lotics.workflows.<alias>\`.
|
|
47227
|
-
`
|
|
47228
|
-
},
|
|
47229
|
-
{
|
|
47230
|
-
path: "src/lucide-react-native.d.ts",
|
|
47231
|
-
content: `// Type shim for lucide-react-native's deep-icon imports
|
|
47232
|
-
// (\`lucide-react-native/dist/esm/icons/<name>\`). The package ships JS-only
|
|
47233
|
-
// files without per-file .d.ts. The lucideIconsWebAlias() plugin in
|
|
47234
|
-
// vite.config.ts redirects these to lucide-react at resolve time; this
|
|
47235
|
-
// declaration covers the TypeScript compile-time gap.
|
|
47236
|
-
declare module "lucide-react-native/dist/esm/icons/*" {
|
|
47237
|
-
import type { ComponentType } from "react";
|
|
47238
|
-
const Icon: ComponentType<{
|
|
47239
|
-
size: number;
|
|
47240
|
-
color: string;
|
|
47241
|
-
fill?: string;
|
|
47242
|
-
strokeWidth?: number;
|
|
47243
|
-
}>;
|
|
47244
|
-
export default Icon;
|
|
47245
|
-
}
|
|
47246
47336
|
`
|
|
47247
47337
|
},
|
|
47248
47338
|
{
|
|
@@ -47252,13 +47342,16 @@ declare module "lucide-react-native/dist/esm/icons/*" {
|
|
|
47252
47342
|
export default classes;
|
|
47253
47343
|
}
|
|
47254
47344
|
|
|
47255
|
-
// Plain side-effect CSS imports
|
|
47345
|
+
// Plain side-effect CSS imports \u2014 @lotics/ui/styles.css, and any sheet of
|
|
47346
|
+
// your own.
|
|
47256
47347
|
declare module "*.css";
|
|
47257
47348
|
`
|
|
47258
47349
|
},
|
|
47259
47350
|
{
|
|
47260
|
-
path: "src/harness.test.
|
|
47351
|
+
path: "src/harness.test.tsx",
|
|
47261
47352
|
content: `import { describe, test, expect } from "vitest";
|
|
47353
|
+
import { render } from "@testing-library/react";
|
|
47354
|
+
import { Text } from "@lotics/ui/text";
|
|
47262
47355
|
|
|
47263
47356
|
/**
|
|
47264
47357
|
* The one test the scaffold seeds, and it is about the TEST SETUP rather than
|
|
@@ -47266,11 +47359,19 @@ declare module "*.css";
|
|
|
47266
47359
|
* never has to be green over zero tests. It imports nothing of yours and
|
|
47267
47360
|
* nothing generated, so it passes on a fresh clone before any codegen has run.
|
|
47268
47361
|
*
|
|
47362
|
+
* It renders a KIT component deliberately. @lotics/ui ships built ESM whose
|
|
47363
|
+
* modules import their own \`.css\`, and a vitest that externalizes them dies
|
|
47364
|
+
* with \`Unknown file extension ".css"\` on every suite that touches one \u2014 a
|
|
47365
|
+
* whole-suite failure that a harness test importing only vitest would never
|
|
47366
|
+
* see. \`test.server.deps.inline\` in vite.config.ts is what prevents it; this
|
|
47367
|
+
* is what notices when that line goes.
|
|
47368
|
+
*
|
|
47269
47369
|
* Delete it once you have tests of your own, or keep it; it costs nothing.
|
|
47270
47370
|
*/
|
|
47271
47371
|
describe("test harness", () => {
|
|
47272
|
-
test("
|
|
47273
|
-
|
|
47372
|
+
test("renders a kit component in a DOM", () => {
|
|
47373
|
+
const { container } = render(<Text>ok</Text>);
|
|
47374
|
+
expect(container.textContent).toBe("ok");
|
|
47274
47375
|
});
|
|
47275
47376
|
});
|
|
47276
47377
|
`
|
|
@@ -47311,6 +47412,59 @@ dist
|
|
|
47311
47412
|
.DS_Store
|
|
47312
47413
|
.lotics
|
|
47313
47414
|
coverage
|
|
47415
|
+
`
|
|
47416
|
+
},
|
|
47417
|
+
{
|
|
47418
|
+
path: "AGENTS.md",
|
|
47419
|
+
content: `# ${args.app_name} \u2014 for the agent editing this app
|
|
47420
|
+
|
|
47421
|
+
A Lotics custom-code app: React + Vite, data through \`@lotics/app-sdk\`, every pixel from
|
|
47422
|
+
\`@lotics/ui\`. Both are installed here \u2014 read them from \`node_modules\`, never from memory.
|
|
47423
|
+
|
|
47424
|
+
## Read in this order, and stop when you have the answer
|
|
47425
|
+
|
|
47426
|
+
1. **\`node_modules/@lotics/ui/llms.txt\`** \u2014 every kit entry by the data role it answers, its
|
|
47427
|
+
import, and the one question that says where it does NOT go. One read, ~12k tokens.
|
|
47428
|
+
**Start here for any UI question**, before writing a component of your own.
|
|
47429
|
+
2. **\`node_modules/@lotics/ui/dist/<module>.d.ts\`** \u2014 the props of the entry you picked, with the
|
|
47430
|
+
source's own doc comments. **Never guess a prop; open the declaration.**
|
|
47431
|
+
3. **\`lotics docs ui/catalog/<Entry>\`** \u2014 that entry's full wrong-place test, one row.
|
|
47432
|
+
**Never read \`catalog.md\` whole**: it is 334 entries, and you need one.
|
|
47433
|
+
4. **\`node_modules/@lotics/ui/examples/tpl_*.tsx\`** \u2014 a whole screen as source, when the SHAPE is
|
|
47434
|
+
the question. Copy and adapt; never import from \`examples/\`.
|
|
47435
|
+
5. **\`node_modules/@lotics/app-sdk/AGENTS.md\`** \u2014 queries, workflows, files, AI.
|
|
47436
|
+
|
|
47437
|
+
\`lotics docs\` lists what is installed here at the versions installed, so a reference always
|
|
47438
|
+
describes the code beside it. Every answer is capped at a page; a long one hands back its section
|
|
47439
|
+
list, and \`lotics docs <area>/<section>\` prints one section.
|
|
47440
|
+
|
|
47441
|
+
## The rules that do not change
|
|
47442
|
+
|
|
47443
|
+
- **Reuse before you build.** A capability the kit lacks is added to the kit, never hand-rolled
|
|
47444
|
+
here. Search \`llms.txt\` for the role before writing a component.
|
|
47445
|
+
- **Customize through TOKENS and PROPS.** Tokens are the shadcn variables, the space rungs, the
|
|
47446
|
+
radius ladder and the type ramp; props are \`variant\`, \`size\`, \`tone\`, \`render\` and slots.
|
|
47447
|
+
\`className\`/\`style\` place an entry in your layout \u2014 they do not restyle it. The kit's DOM and
|
|
47448
|
+
class names are private, so CSS that reaches inside an entry breaks on its next release.
|
|
47449
|
+
- **\`import "@lotics/ui/styles.css"\` once**, in \`src/main.tsx\` \u2014 it carries every token the kit's
|
|
47450
|
+
\`var()\`s read, and without it the screen renders unstyled but working.
|
|
47451
|
+
- **A workflow is the only way this app writes.** Queries read; \`useWorkflow\` writes.
|
|
47452
|
+
|
|
47453
|
+
## Before you commit \u2014 six checks
|
|
47454
|
+
|
|
47455
|
+
1. **One value, one rendering.** A chart draws the entity its table draws, and a value's filter
|
|
47456
|
+
renders it the way its cell does.
|
|
47457
|
+
2. **Every figure is formatted, by the reader's locale** \u2014 money, dates, quantities. A number
|
|
47458
|
+
joined to a string is a number in somebody else's convention.
|
|
47459
|
+
3. **One accent.** Everything else is neutral or a valence, whose colour IS the meaning.
|
|
47460
|
+
4. **Every gap is a rung and the ratio is 1 : 3 : 6**: 4 inside a pair, 8 inside a group, 24
|
|
47461
|
+
between bands, 48 between sections.
|
|
47462
|
+
5. **No bare native control.** A \`<button>\`, \`<input>\`, \`<select>\` or \`<table>\` in your source is a
|
|
47463
|
+
control outside the type ramp, the locale and the keyboard contract.
|
|
47464
|
+
6. **No raw id reaches the reader.** \`rec_\`, \`opt_\` and \`fld_\` are keys; a cell shows the name.
|
|
47465
|
+
|
|
47466
|
+
Then \`npm run typecheck && npm run lint && npm test\`, and \`lotics app deploy\`.
|
|
47467
|
+
The full review \u2014 the gates, the probes, the measurement snippets \u2014 is \`lotics docs ui/reviewing\`.
|
|
47314
47468
|
`
|
|
47315
47469
|
},
|
|
47316
47470
|
{
|
|
@@ -47351,12 +47505,13 @@ import { Stack } from "@lotics/ui/stack";
|
|
|
47351
47505
|
import { Card } from "@lotics/ui/card";
|
|
47352
47506
|
import { Button } from "@lotics/ui/button";
|
|
47353
47507
|
import { Text } from "@lotics/ui/text";
|
|
47354
|
-
// ... and Grid, Dialog, DatePicker,
|
|
47508
|
+
// ... and Grid, Dialog, DatePicker, Select, BarChart, LineChart, PieChart, Metric, Progress, etc.
|
|
47355
47509
|
\`\`\`
|
|
47356
47510
|
|
|
47357
|
-
@lotics/ui ships React
|
|
47358
|
-
|
|
47359
|
-
|
|
47511
|
+
@lotics/ui ships built React DOM components with their own CSS \u2014 import
|
|
47512
|
+
\`@lotics/ui/styles.css\` once (main.tsx does) and every one of them is themed
|
|
47513
|
+
and addressable. Which entry answers which role is
|
|
47514
|
+
\`node_modules/@lotics/ui/llms.txt\`, one read.
|
|
47360
47515
|
|
|
47361
47516
|
## Routing
|
|
47362
47517
|
|
|
@@ -47369,11 +47524,15 @@ shareable and survives a full refresh. Standalone (\`<slug>.lotics.app\`) it's a
|
|
|
47369
47524
|
normal browser router with real path URLs. A single-screen app can drop the
|
|
47370
47525
|
router and render one screen directly.
|
|
47371
47526
|
|
|
47372
|
-
|
|
47373
|
-
|
|
47374
|
-
|
|
47375
|
-
|
|
47376
|
-
|
|
47527
|
+
## References
|
|
47528
|
+
|
|
47529
|
+
\`AGENTS.md\` in this directory is the reading order and the six checks before a
|
|
47530
|
+
commit. The references themselves ship inside the packages, so they always
|
|
47531
|
+
describe the versions installed here: \`node_modules/@lotics/ui/llms.txt\` and
|
|
47532
|
+
\`node_modules/@lotics/app-sdk/AGENTS.md\` are readable with no tooling at all.
|
|
47533
|
+
With the CLI on PATH, \`lotics docs\` indexes the same set and prints one page at
|
|
47534
|
+
a time \u2014 \`lotics docs building_an_app\` is the sequence, read once before
|
|
47535
|
+
building.
|
|
47377
47536
|
`
|
|
47378
47537
|
}
|
|
47379
47538
|
];
|
|
@@ -47567,9 +47726,22 @@ async function dispatchRpc(client, body, opts) {
|
|
|
47567
47726
|
}
|
|
47568
47727
|
}
|
|
47569
47728
|
|
|
47729
|
+
// ../shared/src/open_app_target.ts
|
|
47730
|
+
function openAppTarget(payload) {
|
|
47731
|
+
const appId = payload?.app_id;
|
|
47732
|
+
const route = payload?.route;
|
|
47733
|
+
if (typeof appId !== "string" || !/^app_[A-Za-z0-9]+$/.test(appId)) {
|
|
47734
|
+
throw new Error("openApp requires an app id (app_\u2026)");
|
|
47735
|
+
}
|
|
47736
|
+
if (typeof route !== "string" || !/^\/(?:[^/\\][^\\]*)?$/.test(route)) {
|
|
47737
|
+
throw new Error("openApp requires an in-app route starting with /");
|
|
47738
|
+
}
|
|
47739
|
+
return { app_id: appId, href: `/apps/${appId}?_loc=${encodeURIComponent(route)}` };
|
|
47740
|
+
}
|
|
47741
|
+
|
|
47570
47742
|
// src/dev/wrapper_page.ts
|
|
47571
47743
|
function buildWrapperPage(args) {
|
|
47572
|
-
const { app_name, app_id, workspace_id, vite_url, api_url } = args;
|
|
47744
|
+
const { app_name, app_id, workspace_id, vite_url, api_url, web_app_url } = args;
|
|
47573
47745
|
return `<!doctype html>
|
|
47574
47746
|
<html lang="en">
|
|
47575
47747
|
<head>
|
|
@@ -47733,6 +47905,19 @@ function buildWrapperPage(args) {
|
|
|
47733
47905
|
return undefined;
|
|
47734
47906
|
}
|
|
47735
47907
|
|
|
47908
|
+
// openApp \u2014 the cross-app hop. The product host routes to the sibling in
|
|
47909
|
+
// place; this wrapper serves ONE app and has no shell to route inside, so
|
|
47910
|
+
// the hop opens the sibling on the web app in a new tab. The check is the
|
|
47911
|
+
// host's own function, inlined by source.
|
|
47912
|
+
var openAppTarget = (${openAppTarget.toString()});
|
|
47913
|
+
function handleOpenApp(payload) {
|
|
47914
|
+
var target = openAppTarget(payload);
|
|
47915
|
+
// Same refusal as the host: a hop to itself is in-app navigation.
|
|
47916
|
+
if (target.app_id === ${JSON.stringify(app_id)}) throw new Error("openApp is for a sibling app; navigate inside this app with its own router");
|
|
47917
|
+
window.open(${JSON.stringify(web_app_url)} + target.href, "_blank", "noopener,noreferrer");
|
|
47918
|
+
return undefined;
|
|
47919
|
+
}
|
|
47920
|
+
|
|
47736
47921
|
// urlState.get/set \u2014 useUrlState keeps app view-state (filters, search) in
|
|
47737
47922
|
// THIS wrapper page's address bar so it survives refresh and is shareable,
|
|
47738
47923
|
// mirroring the production host. Handled locally (the URL is the store; no
|
|
@@ -47832,6 +48017,8 @@ function buildWrapperPage(args) {
|
|
|
47832
48017
|
? await handleUpload(msg.payload)
|
|
47833
48018
|
: msg.op === "openExternal"
|
|
47834
48019
|
? handleOpenExternal(msg.payload)
|
|
48020
|
+
: msg.op === "openApp"
|
|
48021
|
+
? handleOpenApp(msg.payload)
|
|
47835
48022
|
: msg.op === "urlState.get"
|
|
47836
48023
|
? readUrlParams()
|
|
47837
48024
|
: msg.op === "urlState.set"
|
|
@@ -47982,7 +48169,8 @@ async function startDevServer(args) {
|
|
|
47982
48169
|
app_id: args.app_id,
|
|
47983
48170
|
workspace_id: args.workspace_id,
|
|
47984
48171
|
vite_url: viteUrl,
|
|
47985
|
-
api_url: args.api_url
|
|
48172
|
+
api_url: args.api_url,
|
|
48173
|
+
web_app_url: WEB_APP_URL
|
|
47986
48174
|
});
|
|
47987
48175
|
const viteEntry = path7.join(args.projectDir, "node_modules", "vite", "bin", "vite.js");
|
|
47988
48176
|
if (!existsSync(viteEntry)) {
|
|
@@ -48292,7 +48480,8 @@ async function startDevServer(args) {
|
|
|
48292
48480
|
stop: async () => {
|
|
48293
48481
|
await stopAll();
|
|
48294
48482
|
await stoppingPromise;
|
|
48295
|
-
}
|
|
48483
|
+
},
|
|
48484
|
+
pendingRpc: () => inFlight
|
|
48296
48485
|
};
|
|
48297
48486
|
}
|
|
48298
48487
|
async function pickPort(preferred, ...avoid) {
|
|
@@ -48346,6 +48535,272 @@ function openBrowser(url2) {
|
|
|
48346
48535
|
child.unref();
|
|
48347
48536
|
}
|
|
48348
48537
|
|
|
48538
|
+
// src/app_screens_check.ts
|
|
48539
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
48540
|
+
import { dirname, join as join2 } from "node:path";
|
|
48541
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
48542
|
+
import { setTimeout as sleep3 } from "node:timers/promises";
|
|
48543
|
+
|
|
48544
|
+
// src/chrome.ts
|
|
48545
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
48546
|
+
import { existsSync as existsSync2, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
48547
|
+
import { tmpdir } from "node:os";
|
|
48548
|
+
import { join } from "node:path";
|
|
48549
|
+
import { setTimeout as sleep2 } from "node:timers/promises";
|
|
48550
|
+
function findChrome() {
|
|
48551
|
+
const env = process.env.LOTICS_CHROME || process.env.CHROME_PATH;
|
|
48552
|
+
if (env && existsSync2(env)) return env;
|
|
48553
|
+
const home = process.env.HOME || "";
|
|
48554
|
+
const pwDir = process.platform === "darwin" ? `${home}/Library/Caches/ms-playwright` : `${home}/.cache/ms-playwright`;
|
|
48555
|
+
if (existsSync2(pwDir)) {
|
|
48556
|
+
const rel = process.platform === "darwin" ? ["chrome-mac/Chromium.app/Contents/MacOS/Chromium"] : [
|
|
48557
|
+
"chrome-linux64/chrome",
|
|
48558
|
+
"chrome-linux/chrome",
|
|
48559
|
+
"chrome-headless-shell-linux64/chrome-headless-shell",
|
|
48560
|
+
"chrome-linux/headless_shell"
|
|
48561
|
+
];
|
|
48562
|
+
const revs = readdirSync(pwDir).filter((n) => n.startsWith("chromium-") || n.startsWith("chromium_headless_shell-")).sort().reverse();
|
|
48563
|
+
for (const rev2 of revs) {
|
|
48564
|
+
for (const r of rel) {
|
|
48565
|
+
const bin = join(pwDir, rev2, r);
|
|
48566
|
+
if (existsSync2(bin)) return bin;
|
|
48567
|
+
}
|
|
48568
|
+
}
|
|
48569
|
+
}
|
|
48570
|
+
const systemPaths = [
|
|
48571
|
+
"/usr/bin/google-chrome",
|
|
48572
|
+
"/usr/bin/google-chrome-stable",
|
|
48573
|
+
"/usr/bin/chromium",
|
|
48574
|
+
"/usr/bin/chromium-browser",
|
|
48575
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
48576
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
48577
|
+
];
|
|
48578
|
+
return systemPaths.find((p) => existsSync2(p)) ?? null;
|
|
48579
|
+
}
|
|
48580
|
+
var NO_CHROME_MESSAGE = "No Chrome/Chromium found. Set CHROME_PATH to a Chrome binary, or install one:\n npx playwright install chromium (then it's auto-detected)\n or install Google Chrome / Chromium via your package manager.";
|
|
48581
|
+
async function cdpConnect(wsUrl) {
|
|
48582
|
+
const ws = new WebSocket(wsUrl);
|
|
48583
|
+
await new Promise((res, rej) => {
|
|
48584
|
+
ws.onopen = () => res();
|
|
48585
|
+
ws.onerror = () => rej(new Error("CDP websocket failed to open"));
|
|
48586
|
+
});
|
|
48587
|
+
let id = 0;
|
|
48588
|
+
const pending = /* @__PURE__ */ new Map();
|
|
48589
|
+
ws.onclose = () => {
|
|
48590
|
+
for (const done of pending.values()) done(Promise.reject(new Error("CDP connection closed (Chrome exited?)")));
|
|
48591
|
+
pending.clear();
|
|
48592
|
+
};
|
|
48593
|
+
ws.onmessage = (e) => {
|
|
48594
|
+
const m = JSON.parse(String(e.data));
|
|
48595
|
+
if (m.id == null) return;
|
|
48596
|
+
const done = pending.get(m.id);
|
|
48597
|
+
if (done === void 0) return;
|
|
48598
|
+
pending.delete(m.id);
|
|
48599
|
+
done(m.error ? Promise.reject(new Error(m.error.message)) : m.result);
|
|
48600
|
+
};
|
|
48601
|
+
const send = (method, params = {}) => new Promise((res) => {
|
|
48602
|
+
const i2 = ++id;
|
|
48603
|
+
pending.set(i2, (r) => res(r));
|
|
48604
|
+
ws.send(JSON.stringify({ id: i2, method, params }));
|
|
48605
|
+
});
|
|
48606
|
+
return { send, close: () => ws.close() };
|
|
48607
|
+
}
|
|
48608
|
+
async function launchChrome(options = {}) {
|
|
48609
|
+
const chrome = findChrome();
|
|
48610
|
+
if (chrome === null) throw new Error(NO_CHROME_MESSAGE);
|
|
48611
|
+
if (typeof WebSocket === "undefined") {
|
|
48612
|
+
throw new Error("This command needs Node 22+ (it drives Chrome over CDP via the built-in WebSocket). Upgrade Node and retry.");
|
|
48613
|
+
}
|
|
48614
|
+
const udd = mkdtempSync(join(tmpdir(), "lotics-chrome-"));
|
|
48615
|
+
const child = spawn2(chrome, [
|
|
48616
|
+
"--headless=new",
|
|
48617
|
+
"--disable-gpu",
|
|
48618
|
+
"--no-sandbox",
|
|
48619
|
+
"--hide-scrollbars",
|
|
48620
|
+
// Large renders exhaust the (often tiny) /dev/shm in containers/WSL2 and
|
|
48621
|
+
// crash the tab; back shared memory with /tmp instead.
|
|
48622
|
+
"--disable-dev-shm-usage",
|
|
48623
|
+
`--force-device-scale-factor=${options.deviceScaleFactor ?? 2}`,
|
|
48624
|
+
...options.sameProcessFrames ? ["--disable-features=IsolateOrigins,site-per-process"] : [],
|
|
48625
|
+
"--remote-debugging-port=0",
|
|
48626
|
+
`--user-data-dir=${udd}`,
|
|
48627
|
+
"about:blank"
|
|
48628
|
+
], { stdio: "ignore" });
|
|
48629
|
+
const close = () => {
|
|
48630
|
+
child.kill();
|
|
48631
|
+
rmSync(udd, { recursive: true, force: true });
|
|
48632
|
+
};
|
|
48633
|
+
try {
|
|
48634
|
+
let cdpPort = 0;
|
|
48635
|
+
const portFile = join(udd, "DevToolsActivePort");
|
|
48636
|
+
for (let i2 = 0; i2 < 100 && !cdpPort; i2++) {
|
|
48637
|
+
if (existsSync2(portFile)) {
|
|
48638
|
+
const p = parseInt(readFileSync(portFile, "utf8").split("\n")[0], 10);
|
|
48639
|
+
if (p) cdpPort = p;
|
|
48640
|
+
}
|
|
48641
|
+
if (!cdpPort) await sleep2(100);
|
|
48642
|
+
}
|
|
48643
|
+
if (!cdpPort) throw new Error("Chrome did not expose a debugging port (launch failed?).");
|
|
48644
|
+
let target;
|
|
48645
|
+
for (let i2 = 0; i2 < 60 && !target?.webSocketDebuggerUrl; i2++) {
|
|
48646
|
+
try {
|
|
48647
|
+
const list2 = await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json();
|
|
48648
|
+
target = list2.find((t) => t.type === "page");
|
|
48649
|
+
} catch {
|
|
48650
|
+
}
|
|
48651
|
+
if (!target?.webSocketDebuggerUrl) await sleep2(100);
|
|
48652
|
+
}
|
|
48653
|
+
if (!target?.webSocketDebuggerUrl) throw new Error("No Chrome page target available.");
|
|
48654
|
+
const cdp = await cdpConnect(target.webSocketDebuggerUrl);
|
|
48655
|
+
await cdp.send("Page.enable");
|
|
48656
|
+
await cdp.send("Runtime.enable");
|
|
48657
|
+
return { cdp, chrome, close: () => {
|
|
48658
|
+
cdp.close();
|
|
48659
|
+
close();
|
|
48660
|
+
} };
|
|
48661
|
+
} catch (error52) {
|
|
48662
|
+
close();
|
|
48663
|
+
throw error52;
|
|
48664
|
+
}
|
|
48665
|
+
}
|
|
48666
|
+
|
|
48667
|
+
// src/app_screens_check.ts
|
|
48668
|
+
var HERE = dirname(fileURLToPath2(import.meta.url));
|
|
48669
|
+
var SCREEN_WIDTHS = [1280, 375];
|
|
48670
|
+
async function findAppFrame(cdp, viteUrl) {
|
|
48671
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
48672
|
+
const tree = await cdp.send("Page.getFrameTree");
|
|
48673
|
+
const child = (tree.frameTree.childFrames ?? []).find((frame) => frame.frame.url.startsWith(viteUrl));
|
|
48674
|
+
if (child !== void 0) return child.frame.id;
|
|
48675
|
+
await sleep3(100);
|
|
48676
|
+
}
|
|
48677
|
+
throw new Error(`The app's frame never appeared under the dev wrapper (${viteUrl}).`);
|
|
48678
|
+
}
|
|
48679
|
+
async function evaluate(cdp, contextId, expression) {
|
|
48680
|
+
const result = await cdp.send(
|
|
48681
|
+
"Runtime.evaluate",
|
|
48682
|
+
{ expression, contextId, returnByValue: true, awaitPromise: true }
|
|
48683
|
+
);
|
|
48684
|
+
if (result.exceptionDetails !== void 0) {
|
|
48685
|
+
throw new Error(
|
|
48686
|
+
`probe failed in the page: ${result.exceptionDetails.exception?.description ?? result.exceptionDetails.text}`
|
|
48687
|
+
);
|
|
48688
|
+
}
|
|
48689
|
+
return result.result.value;
|
|
48690
|
+
}
|
|
48691
|
+
var SETTLE_BUDGET_MS = 15e3;
|
|
48692
|
+
var SETTLE_TICK_MS = 250;
|
|
48693
|
+
async function settle(cdp, contextId, pendingRpc) {
|
|
48694
|
+
let previous = -1;
|
|
48695
|
+
let count2 = 0;
|
|
48696
|
+
for (let elapsed = 0; elapsed < SETTLE_BUDGET_MS; elapsed += SETTLE_TICK_MS) {
|
|
48697
|
+
count2 = await evaluate(cdp, contextId, "__loticsProbe.textLeafCount()");
|
|
48698
|
+
if (pendingRpc() === 0 && count2 > 0 && count2 === previous) return { count: count2, settled: true };
|
|
48699
|
+
previous = pendingRpc() === 0 ? count2 : -1;
|
|
48700
|
+
await sleep3(SETTLE_TICK_MS);
|
|
48701
|
+
}
|
|
48702
|
+
return { count: count2, settled: false };
|
|
48703
|
+
}
|
|
48704
|
+
function emptyCensus(screen, width) {
|
|
48705
|
+
return { screen, width, leaves: 0, money: 0, bare: 0, encoded: 0, strips: 0 };
|
|
48706
|
+
}
|
|
48707
|
+
async function checkAppScreens(client, args) {
|
|
48708
|
+
const bundlePath = join2(HERE, "..", "probe_page.js");
|
|
48709
|
+
if (!existsSync3(bundlePath)) {
|
|
48710
|
+
throw new Error(`Probe bundle missing at ${bundlePath} \u2014 reinstall @lotics/cli (build step failed).`);
|
|
48711
|
+
}
|
|
48712
|
+
const bundle = readFileSync2(bundlePath, "utf8");
|
|
48713
|
+
const dev = await startDevServer({
|
|
48714
|
+
projectDir: args.projectDir,
|
|
48715
|
+
app_id: args.app_id,
|
|
48716
|
+
app_name: args.app_name,
|
|
48717
|
+
workspace_id: args.workspace_id,
|
|
48718
|
+
api_url: client.baseUrl,
|
|
48719
|
+
client,
|
|
48720
|
+
commentsEnabled: args.commentsEnabled
|
|
48721
|
+
});
|
|
48722
|
+
await dev.ready;
|
|
48723
|
+
const wrapperUrl = `http://localhost:${dev.port}/`;
|
|
48724
|
+
const viteUrl = `http://localhost:${dev.vitePort}/`;
|
|
48725
|
+
const findings = [];
|
|
48726
|
+
const census = [];
|
|
48727
|
+
let session;
|
|
48728
|
+
try {
|
|
48729
|
+
session = await launchChrome({ deviceScaleFactor: 1, sameProcessFrames: true });
|
|
48730
|
+
for (const width of SCREEN_WIDTHS) {
|
|
48731
|
+
await session.cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
48732
|
+
width,
|
|
48733
|
+
height: 900,
|
|
48734
|
+
deviceScaleFactor: 1,
|
|
48735
|
+
mobile: width < 600
|
|
48736
|
+
});
|
|
48737
|
+
await session.cdp.send("Page.navigate", { url: wrapperUrl });
|
|
48738
|
+
const frameId = await findAppFrame(session.cdp, viteUrl);
|
|
48739
|
+
const world = await session.cdp.send("Page.createIsolatedWorld", {
|
|
48740
|
+
frameId,
|
|
48741
|
+
worldName: "lotics-probe"
|
|
48742
|
+
});
|
|
48743
|
+
const contextId = world.executionContextId;
|
|
48744
|
+
await evaluate(session.cdp, contextId, bundle);
|
|
48745
|
+
await settle(session.cdp, contextId, dev.pendingRpc);
|
|
48746
|
+
const tabs = await evaluate(session.cdp, contextId, "__loticsProbe.tabs()");
|
|
48747
|
+
const screens = tabs.length > 0 ? tabs : ["root"];
|
|
48748
|
+
for (let index = 0; index < screens.length; index++) {
|
|
48749
|
+
const screen = screens[index];
|
|
48750
|
+
if (tabs.length > 0) {
|
|
48751
|
+
await evaluate(session.cdp, contextId, `__loticsProbe.pressTab(${index})`);
|
|
48752
|
+
}
|
|
48753
|
+
const rendered = await settle(session.cdp, contextId, dev.pendingRpc);
|
|
48754
|
+
if (rendered.count === 0) {
|
|
48755
|
+
findings.push({ screen, width, probe: "render", total: 0, count: 1, detail: ["no text rendered \u2014 the screen did not load, so nothing below it was measured"] });
|
|
48756
|
+
census.push(emptyCensus(screen, width));
|
|
48757
|
+
continue;
|
|
48758
|
+
}
|
|
48759
|
+
if (!rendered.settled) {
|
|
48760
|
+
findings.push({
|
|
48761
|
+
screen,
|
|
48762
|
+
width,
|
|
48763
|
+
probe: "settle",
|
|
48764
|
+
total: rendered.count,
|
|
48765
|
+
count: 1,
|
|
48766
|
+
detail: [`still changing after ${SETTLE_BUDGET_MS / 1e3}s \u2014 measured as it was; a request still in flight, or a render that never stops`]
|
|
48767
|
+
});
|
|
48768
|
+
}
|
|
48769
|
+
const results = await evaluate(
|
|
48770
|
+
session.cdp,
|
|
48771
|
+
contextId,
|
|
48772
|
+
`__loticsProbe.run({ truncation: ${width < 600} })`
|
|
48773
|
+
);
|
|
48774
|
+
for (const result of results.findings) findings.push({ screen, width, ...result });
|
|
48775
|
+
census.push({ screen, width, ...results.census });
|
|
48776
|
+
}
|
|
48777
|
+
}
|
|
48778
|
+
} finally {
|
|
48779
|
+
session?.close();
|
|
48780
|
+
await dev.stop();
|
|
48781
|
+
}
|
|
48782
|
+
return { findings, census };
|
|
48783
|
+
}
|
|
48784
|
+
function formatScreenFindings(report) {
|
|
48785
|
+
const lines = ["Screens measured:"];
|
|
48786
|
+
for (const row of report.census) {
|
|
48787
|
+
lines.push(
|
|
48788
|
+
` ${row.screen} @${row.width} \xB7 ${row.leaves} text runs \xB7 ${row.money} money strings \xB7 ${row.bare} bare values / ${row.encoded} encoded \xB7 ${row.strips} tab strip${row.strips === 1 ? "" : "s"}`
|
|
48789
|
+
);
|
|
48790
|
+
}
|
|
48791
|
+
if (report.findings.length === 0) {
|
|
48792
|
+
lines.push(`Every screen measured clean at ${SCREEN_WIDTHS.join(" and ")}.`);
|
|
48793
|
+
return lines.join("\n");
|
|
48794
|
+
}
|
|
48795
|
+
lines.push(`Screens \u2014 ${report.findings.length} finding${report.findings.length === 1 ? "" : "s"}:`);
|
|
48796
|
+
for (const finding of report.findings) {
|
|
48797
|
+
lines.push(` ${finding.screen} @${finding.width} \xB7 ${finding.probe} (${finding.count} of ${finding.total})`);
|
|
48798
|
+
for (const detail of finding.detail) lines.push(` ${detail}`);
|
|
48799
|
+
}
|
|
48800
|
+
lines.push(" The probes are @lotics/ui docs/reviewing.md, by name \u2014 each names its measurement and its fix.");
|
|
48801
|
+
return lines.join("\n");
|
|
48802
|
+
}
|
|
48803
|
+
|
|
48349
48804
|
// src/output.ts
|
|
48350
48805
|
var machineMode = false;
|
|
48351
48806
|
var collectedWarnings = [];
|
|
@@ -49142,10 +49597,10 @@ function mergeDefs(...defs) {
|
|
|
49142
49597
|
function cloneDef(schema) {
|
|
49143
49598
|
return mergeDefs(schema._zod.def);
|
|
49144
49599
|
}
|
|
49145
|
-
function getElementAtPath(obj,
|
|
49146
|
-
if (!
|
|
49600
|
+
function getElementAtPath(obj, path14) {
|
|
49601
|
+
if (!path14)
|
|
49147
49602
|
return obj;
|
|
49148
|
-
return
|
|
49603
|
+
return path14.reduce((acc, key) => acc?.[key], obj);
|
|
49149
49604
|
}
|
|
49150
49605
|
function promiseAllObject(promisesObj) {
|
|
49151
49606
|
const keys3 = Object.keys(promisesObj);
|
|
@@ -49554,11 +50009,11 @@ function explicitlyAborted(x2, startIndex = 0) {
|
|
|
49554
50009
|
}
|
|
49555
50010
|
return false;
|
|
49556
50011
|
}
|
|
49557
|
-
function prefixIssues(
|
|
50012
|
+
function prefixIssues(path14, issues) {
|
|
49558
50013
|
return issues.map((iss) => {
|
|
49559
50014
|
var _a4;
|
|
49560
50015
|
(_a4 = iss).path ?? (_a4.path = []);
|
|
49561
|
-
iss.path.unshift(
|
|
50016
|
+
iss.path.unshift(path14);
|
|
49562
50017
|
return iss;
|
|
49563
50018
|
});
|
|
49564
50019
|
}
|
|
@@ -49705,16 +50160,16 @@ function flattenError(error52, mapper = (issue2) => issue2.message) {
|
|
|
49705
50160
|
}
|
|
49706
50161
|
function formatError(error52, mapper = (issue2) => issue2.message) {
|
|
49707
50162
|
const fieldErrors = { _errors: [] };
|
|
49708
|
-
const processError = (error53,
|
|
50163
|
+
const processError = (error53, path14 = []) => {
|
|
49709
50164
|
for (const issue2 of error53.issues) {
|
|
49710
50165
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
49711
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
50166
|
+
issue2.errors.map((issues) => processError({ issues }, [...path14, ...issue2.path]));
|
|
49712
50167
|
} else if (issue2.code === "invalid_key") {
|
|
49713
|
-
processError({ issues: issue2.issues }, [...
|
|
50168
|
+
processError({ issues: issue2.issues }, [...path14, ...issue2.path]);
|
|
49714
50169
|
} else if (issue2.code === "invalid_element") {
|
|
49715
|
-
processError({ issues: issue2.issues }, [...
|
|
50170
|
+
processError({ issues: issue2.issues }, [...path14, ...issue2.path]);
|
|
49716
50171
|
} else {
|
|
49717
|
-
const fullpath = [...
|
|
50172
|
+
const fullpath = [...path14, ...issue2.path];
|
|
49718
50173
|
if (fullpath.length === 0) {
|
|
49719
50174
|
fieldErrors._errors.push(mapper(issue2));
|
|
49720
50175
|
} else {
|
|
@@ -49741,17 +50196,17 @@ function formatError(error52, mapper = (issue2) => issue2.message) {
|
|
|
49741
50196
|
}
|
|
49742
50197
|
function treeifyError(error52, mapper = (issue2) => issue2.message) {
|
|
49743
50198
|
const result = { errors: [] };
|
|
49744
|
-
const processError = (error53,
|
|
50199
|
+
const processError = (error53, path14 = []) => {
|
|
49745
50200
|
var _a4, _b2;
|
|
49746
50201
|
for (const issue2 of error53.issues) {
|
|
49747
50202
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
49748
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
50203
|
+
issue2.errors.map((issues) => processError({ issues }, [...path14, ...issue2.path]));
|
|
49749
50204
|
} else if (issue2.code === "invalid_key") {
|
|
49750
|
-
processError({ issues: issue2.issues }, [...
|
|
50205
|
+
processError({ issues: issue2.issues }, [...path14, ...issue2.path]);
|
|
49751
50206
|
} else if (issue2.code === "invalid_element") {
|
|
49752
|
-
processError({ issues: issue2.issues }, [...
|
|
50207
|
+
processError({ issues: issue2.issues }, [...path14, ...issue2.path]);
|
|
49753
50208
|
} else {
|
|
49754
|
-
const fullpath = [...
|
|
50209
|
+
const fullpath = [...path14, ...issue2.path];
|
|
49755
50210
|
if (fullpath.length === 0) {
|
|
49756
50211
|
result.errors.push(mapper(issue2));
|
|
49757
50212
|
continue;
|
|
@@ -49783,8 +50238,8 @@ function treeifyError(error52, mapper = (issue2) => issue2.message) {
|
|
|
49783
50238
|
}
|
|
49784
50239
|
function toDotPath(_path) {
|
|
49785
50240
|
const segs = [];
|
|
49786
|
-
const
|
|
49787
|
-
for (const seg of
|
|
50241
|
+
const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
50242
|
+
for (const seg of path14) {
|
|
49788
50243
|
if (typeof seg === "number")
|
|
49789
50244
|
segs.push(`[${seg}]`);
|
|
49790
50245
|
else if (typeof seg === "symbol")
|
|
@@ -62476,13 +62931,13 @@ function resolveRef(ref2, ctx) {
|
|
|
62476
62931
|
if (!ref2.startsWith("#")) {
|
|
62477
62932
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
62478
62933
|
}
|
|
62479
|
-
const
|
|
62480
|
-
if (
|
|
62934
|
+
const path14 = ref2.slice(1).split("/").filter(Boolean);
|
|
62935
|
+
if (path14.length === 0) {
|
|
62481
62936
|
return ctx.rootSchema;
|
|
62482
62937
|
}
|
|
62483
62938
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
62484
|
-
if (
|
|
62485
|
-
const key =
|
|
62939
|
+
if (path14[0] === defsKey) {
|
|
62940
|
+
const key = path14[1];
|
|
62486
62941
|
if (!key || !ctx.defs[key]) {
|
|
62487
62942
|
throw new Error(`Reference not found: ${ref2}`);
|
|
62488
62943
|
}
|
|
@@ -65180,16 +65635,16 @@ function conditionValueIssue(type, operator, value2, fieldKey) {
|
|
|
65180
65635
|
return null;
|
|
65181
65636
|
}
|
|
65182
65637
|
}
|
|
65183
|
-
function conditionIssues(node,
|
|
65638
|
+
function conditionIssues(node, path14, fieldMap) {
|
|
65184
65639
|
if (!node || typeof node !== "object") {
|
|
65185
|
-
return [`${
|
|
65640
|
+
return [`${path14}: expected condition object, got ${typeof node}`];
|
|
65186
65641
|
}
|
|
65187
65642
|
const obj = node;
|
|
65188
65643
|
if (obj.type === "locked") {
|
|
65189
65644
|
const validOps2 = VALID_OPERATORS.locked;
|
|
65190
65645
|
if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
|
|
65191
65646
|
return [
|
|
65192
|
-
`${
|
|
65647
|
+
`${path14}: invalid operator '${String(obj.operator)}' for locked filter. Valid: ${validOps2.join(", ")}`
|
|
65193
65648
|
];
|
|
65194
65649
|
}
|
|
65195
65650
|
return [];
|
|
@@ -65198,12 +65653,12 @@ function conditionIssues(node, path13, fieldMap) {
|
|
|
65198
65653
|
const validOps2 = VALID_OPERATORS.current_member;
|
|
65199
65654
|
if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
|
|
65200
65655
|
return [
|
|
65201
|
-
`${
|
|
65656
|
+
`${path14}: invalid operator '${String(obj.operator)}' for current_member filter. Valid: ${validOps2.join(", ")}`
|
|
65202
65657
|
];
|
|
65203
65658
|
}
|
|
65204
65659
|
if (!Array.isArray(obj.value) || obj.value.length === 0 || !obj.value.every((v) => typeof v === "string" && v.length > 0)) {
|
|
65205
65660
|
return [
|
|
65206
|
-
`${
|
|
65661
|
+
`${path14}: current_member '${obj.operator}' expects a non-empty string[] of group IDs.`
|
|
65207
65662
|
];
|
|
65208
65663
|
}
|
|
65209
65664
|
return [];
|
|
@@ -65212,45 +65667,45 @@ function conditionIssues(node, path13, fieldMap) {
|
|
|
65212
65667
|
const validOps2 = VALID_OPERATORS.record_id;
|
|
65213
65668
|
if (typeof obj.operator !== "string" || !validOps2.includes(obj.operator)) {
|
|
65214
65669
|
return [
|
|
65215
|
-
`${
|
|
65670
|
+
`${path14}: invalid operator '${String(obj.operator)}' for record_id filter. Valid: ${validOps2.join(", ")}`
|
|
65216
65671
|
];
|
|
65217
65672
|
}
|
|
65218
65673
|
if (!Array.isArray(obj.value) || obj.value.length === 0 || !obj.value.every((v) => typeof v === "string" && v.length > 0)) {
|
|
65219
|
-
return [`${
|
|
65674
|
+
return [`${path14}: record_id '${obj.operator}' expects a non-empty string[] of record ids.`];
|
|
65220
65675
|
}
|
|
65221
65676
|
return [];
|
|
65222
65677
|
}
|
|
65223
65678
|
if (typeof obj.field_key !== "string" || !obj.field_key) {
|
|
65224
|
-
return [`${
|
|
65679
|
+
return [`${path14}: field_key must be a non-empty string`];
|
|
65225
65680
|
}
|
|
65226
65681
|
let filterType;
|
|
65227
65682
|
if (fieldMap) {
|
|
65228
65683
|
filterType = fieldMap.get(obj.field_key);
|
|
65229
65684
|
if (filterType === void 0) {
|
|
65230
65685
|
const available = Array.from(fieldMap.keys()).join(", ");
|
|
65231
|
-
return [`${
|
|
65686
|
+
return [`${path14}: unknown field_key '${obj.field_key}'. Available: ${available}`];
|
|
65232
65687
|
}
|
|
65233
65688
|
} else if (typeof obj.type === "string") {
|
|
65234
65689
|
filterType = obj.type;
|
|
65235
65690
|
}
|
|
65236
65691
|
if (filterType === "button") {
|
|
65237
|
-
return [`${
|
|
65692
|
+
return [`${path14}: a button field holds no data and cannot be filtered.`];
|
|
65238
65693
|
}
|
|
65239
65694
|
if (typeof filterType !== "string" || !VALID_FILTER_TYPES.includes(filterType)) {
|
|
65240
65695
|
const named = fieldMap && filterType !== obj.type ? `'${String(filterType)}' (resolved from field '${obj.field_key}')` : `'${String(obj.type)}'`;
|
|
65241
|
-
return [`${
|
|
65696
|
+
return [`${path14}: invalid filter type ${named}. Valid: ${VALID_FILTER_TYPES.join(", ")}`];
|
|
65242
65697
|
}
|
|
65243
65698
|
const validOps = VALID_OPERATORS[filterType];
|
|
65244
65699
|
if (typeof obj.operator !== "string" || !validOps.includes(obj.operator)) {
|
|
65245
65700
|
return [
|
|
65246
|
-
`${
|
|
65701
|
+
`${path14}: invalid operator '${String(obj.operator)}' for ${filterType} filter. Valid: ${validOps.join(", ")}`
|
|
65247
65702
|
];
|
|
65248
65703
|
}
|
|
65249
65704
|
const valueIssue = conditionValueIssue(filterType, obj.operator, obj.value, obj.field_key);
|
|
65250
65705
|
return valueIssue ? [valueIssue] : [];
|
|
65251
65706
|
}
|
|
65252
|
-
function validateFilterCondition(condition,
|
|
65253
|
-
return conditionIssues(condition,
|
|
65707
|
+
function validateFilterCondition(condition, path14 = "filter") {
|
|
65708
|
+
return conditionIssues(condition, path14, void 0);
|
|
65254
65709
|
}
|
|
65255
65710
|
function inheritedFilterType(type, format2, fallback) {
|
|
65256
65711
|
switch (type) {
|
|
@@ -67910,7 +68365,7 @@ __export(expression_string_exports, {
|
|
|
67910
68365
|
contains: () => contains2,
|
|
67911
68366
|
endsWith: () => endsWith,
|
|
67912
68367
|
formatNumber: () => formatNumber,
|
|
67913
|
-
join: () =>
|
|
68368
|
+
join: () => join3,
|
|
67914
68369
|
length: () => length,
|
|
67915
68370
|
lower: () => lower,
|
|
67916
68371
|
numberToWords: () => numberToWords,
|
|
@@ -67924,7 +68379,7 @@ __export(expression_string_exports, {
|
|
|
67924
68379
|
trim: () => trim,
|
|
67925
68380
|
upper: () => upper
|
|
67926
68381
|
});
|
|
67927
|
-
function
|
|
68382
|
+
function join3(arr, separator) {
|
|
67928
68383
|
if (arr == null) return "";
|
|
67929
68384
|
if (!Array.isArray(arr)) {
|
|
67930
68385
|
throw new Error("join: expected array, got " + typeof arr);
|
|
@@ -68377,9 +68832,9 @@ __export(expression_object_exports, {
|
|
|
68377
68832
|
pick: () => pick3,
|
|
68378
68833
|
values: () => values2
|
|
68379
68834
|
});
|
|
68380
|
-
function getNestedValue(obj,
|
|
68835
|
+
function getNestedValue(obj, path14) {
|
|
68381
68836
|
if (obj == null) return void 0;
|
|
68382
|
-
const parts =
|
|
68837
|
+
const parts = path14.split(".");
|
|
68383
68838
|
let value2 = obj;
|
|
68384
68839
|
for (const part of parts) {
|
|
68385
68840
|
if (value2 == null) return void 0;
|
|
@@ -68418,11 +68873,11 @@ function entries(obj) {
|
|
|
68418
68873
|
}
|
|
68419
68874
|
return Object.entries(obj);
|
|
68420
68875
|
}
|
|
68421
|
-
function get(obj,
|
|
68422
|
-
if (typeof
|
|
68876
|
+
function get(obj, path14, defaultValue) {
|
|
68877
|
+
if (typeof path14 !== "string") {
|
|
68423
68878
|
throw new Error("get: path must be a string");
|
|
68424
68879
|
}
|
|
68425
|
-
const value2 = getNestedValue(obj,
|
|
68880
|
+
const value2 = getNestedValue(obj, path14);
|
|
68426
68881
|
return value2 !== void 0 ? value2 : defaultValue;
|
|
68427
68882
|
}
|
|
68428
68883
|
function pick3(obj, fields) {
|
|
@@ -72448,24 +72903,24 @@ function isBelowGeneratedWidth(token) {
|
|
|
72448
72903
|
}
|
|
72449
72904
|
var SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".html", ".css", ".json"];
|
|
72450
72905
|
var SCAN_FILE_MAX_BYTES = 1024 * 1024;
|
|
72451
|
-
function normalizeScanPath(
|
|
72452
|
-
return
|
|
72906
|
+
function normalizeScanPath(path14) {
|
|
72907
|
+
return path14.replace(/\\/g, "/").replace(/^\.\//, "").replace(/^\/+/, "");
|
|
72453
72908
|
}
|
|
72454
|
-
function hasScannableExtension(
|
|
72455
|
-
return SCAN_EXTENSIONS.some((ext) =>
|
|
72909
|
+
function hasScannableExtension(path14) {
|
|
72910
|
+
return SCAN_EXTENSIONS.some((ext) => path14.endsWith(ext));
|
|
72456
72911
|
}
|
|
72457
|
-
function isConcreteIdScanPath(
|
|
72458
|
-
const name2 = normalizeScanPath(
|
|
72912
|
+
function isConcreteIdScanPath(path14) {
|
|
72913
|
+
const name2 = normalizeScanPath(path14);
|
|
72459
72914
|
if (name2.endsWith(".md")) return true;
|
|
72460
72915
|
if (!name2.startsWith("src/")) return false;
|
|
72461
72916
|
if (name2.startsWith("src/workflows/")) return false;
|
|
72462
72917
|
return hasScannableExtension(name2);
|
|
72463
72918
|
}
|
|
72464
|
-
function isTestFile(
|
|
72465
|
-
return /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(
|
|
72919
|
+
function isTestFile(path14) {
|
|
72920
|
+
return /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(path14);
|
|
72466
72921
|
}
|
|
72467
|
-
function isIdPlaceholderScanPath(
|
|
72468
|
-
const name2 = normalizeScanPath(
|
|
72922
|
+
function isIdPlaceholderScanPath(path14) {
|
|
72923
|
+
const name2 = normalizeScanPath(path14);
|
|
72469
72924
|
if (isTestFile(name2)) return false;
|
|
72470
72925
|
if (name2.endsWith(".md")) return true;
|
|
72471
72926
|
if (name2 === "package.json") return true;
|
|
@@ -72482,24 +72937,24 @@ function firstLines(text, pattern, keep, into) {
|
|
|
72482
72937
|
});
|
|
72483
72938
|
return into;
|
|
72484
72939
|
}
|
|
72485
|
-
function toFindings(
|
|
72486
|
-
return [...lines].map(([id, line]) => ({ path:
|
|
72940
|
+
function toFindings(path14, rule, lines) {
|
|
72941
|
+
return [...lines].map(([id, line]) => ({ path: path14, line, id, rule }));
|
|
72487
72942
|
}
|
|
72488
|
-
function scanFileForConcreteIds(
|
|
72489
|
-
if (!isConcreteIdScanPath(
|
|
72943
|
+
function scanFileForConcreteIds(path14, text) {
|
|
72944
|
+
if (!isConcreteIdScanPath(path14)) return [];
|
|
72490
72945
|
const lines = firstLines(text, CONCRETE_ID_SCAN, () => true, /* @__PURE__ */ new Map());
|
|
72491
72946
|
firstLines(text, WORKSPACE_ID_SCAN, isGeneratedIdBody, lines);
|
|
72492
|
-
return toFindings(
|
|
72947
|
+
return toFindings(path14, "concrete", lines);
|
|
72493
72948
|
}
|
|
72494
|
-
function scanFileForIdPlaceholders(
|
|
72495
|
-
if (!isIdPlaceholderScanPath(
|
|
72949
|
+
function scanFileForIdPlaceholders(path14, text) {
|
|
72950
|
+
if (!isIdPlaceholderScanPath(path14)) return [];
|
|
72496
72951
|
const lines = firstLines(text, ID_PLACEHOLDER_SCAN, isBelowGeneratedWidth, /* @__PURE__ */ new Map());
|
|
72497
|
-
return toFindings(
|
|
72952
|
+
return toFindings(path14, "placeholder", lines);
|
|
72498
72953
|
}
|
|
72499
|
-
function scanFileForIds(
|
|
72500
|
-
const concrete = scanFileForConcreteIds(
|
|
72954
|
+
function scanFileForIds(path14, text) {
|
|
72955
|
+
const concrete = scanFileForConcreteIds(path14, text);
|
|
72501
72956
|
const seen = new Set(concrete.map((finding) => finding.id));
|
|
72502
|
-
return [...concrete, ...scanFileForIdPlaceholders(
|
|
72957
|
+
return [...concrete, ...scanFileForIdPlaceholders(path14, text).filter((f) => !seen.has(f.id))].sort(
|
|
72503
72958
|
(a, b) => a.line - b.line || (a.id < b.id ? -1 : 1)
|
|
72504
72959
|
);
|
|
72505
72960
|
}
|
|
@@ -72988,7 +73443,7 @@ async function fetchWorkflowDts(client, app_id, alias, declaration) {
|
|
|
72988
73443
|
}
|
|
72989
73444
|
function runTar(args, cwd) {
|
|
72990
73445
|
return new Promise((resolve2, reject2) => {
|
|
72991
|
-
const proc =
|
|
73446
|
+
const proc = spawn3("tar", args, { cwd, stdio: ["ignore", "ignore", "pipe"] });
|
|
72992
73447
|
let stderr = "";
|
|
72993
73448
|
proc.stderr.on("data", (chunk) => {
|
|
72994
73449
|
stderr += chunk.toString();
|
|
@@ -73002,7 +73457,7 @@ function runTar(args, cwd) {
|
|
|
73002
73457
|
}
|
|
73003
73458
|
function npmIsInstalled() {
|
|
73004
73459
|
return new Promise((resolve2) => {
|
|
73005
|
-
const probe =
|
|
73460
|
+
const probe = spawn3("npm", ["--version"], {
|
|
73006
73461
|
shell: process.platform === "win32",
|
|
73007
73462
|
stdio: "ignore",
|
|
73008
73463
|
env: ipv4ChildEnv(process.env)
|
|
@@ -73014,7 +73469,7 @@ function npmIsInstalled() {
|
|
|
73014
73469
|
function runNpm(args, cwd) {
|
|
73015
73470
|
const quiet = isMachineOutput();
|
|
73016
73471
|
return new Promise((resolve2, reject2) => {
|
|
73017
|
-
const proc =
|
|
73472
|
+
const proc = spawn3("npm", args, {
|
|
73018
73473
|
cwd,
|
|
73019
73474
|
// On Windows `npm` is `npm.cmd`, a batch file Node will not execute
|
|
73020
73475
|
// without a shell — bare `spawn("npm")` dies at ENOENT, which took every
|
|
@@ -73341,8 +73796,8 @@ function warnAboutDevLink(projectDir, command) {
|
|
|
73341
73796
|
const linked = config2.includes("loticsResolve") || config2.includes("LOTICS_UI_SRC");
|
|
73342
73797
|
if (!linked) {
|
|
73343
73798
|
console.error(
|
|
73344
|
-
`\u26A0 LOTICS_UI_SRC is set but vite.config.ts never reads it \u2014 @lotics/ui will still resolve from node_modules, so kit edits will NOT ${command === "deploy" ? "ship" : "appear"}. Hand the whole resolve block to the kit (it owns the dev-link
|
|
73345
|
-
import {
|
|
73799
|
+
`\u26A0 LOTICS_UI_SRC is set but vite.config.ts never reads it \u2014 @lotics/ui will still resolve from node_modules, so kit edits will NOT ${command === "deploy" ? "ship" : "appear"}. Hand the whole resolve block to the kit (it owns the dev-link and the React dedupe):
|
|
73800
|
+
import { loticsResolve } from "@lotics/ui/vite";
|
|
73346
73801
|
// \u2026
|
|
73347
73802
|
resolve: loticsResolve(),`
|
|
73348
73803
|
);
|
|
@@ -73420,6 +73875,71 @@ A newer kit is published \u2014 \`npm update ${findings.map((f) => f.pkg).join("
|
|
|
73420
73875
|
` + lines.join("\n")
|
|
73421
73876
|
);
|
|
73422
73877
|
}
|
|
73878
|
+
var KIT_SPECIFIER = "@lotics/ui/button";
|
|
73879
|
+
var KIT_INSTALLED_PATH = "/node_modules/@lotics/ui/dist/button.js";
|
|
73880
|
+
function inlineEntries(source) {
|
|
73881
|
+
const out = [];
|
|
73882
|
+
const key = /\binline\s*:\s*/g;
|
|
73883
|
+
for (let match2 = key.exec(source); match2 !== null; match2 = key.exec(source)) {
|
|
73884
|
+
const i2 = match2.index + match2[0].length;
|
|
73885
|
+
if (source.startsWith("true", i2)) {
|
|
73886
|
+
out.push({ kind: "everything" });
|
|
73887
|
+
continue;
|
|
73888
|
+
}
|
|
73889
|
+
if (source[i2] !== "[") continue;
|
|
73890
|
+
for (let depth = 1, i22 = i2 + 1; i22 < source.length && depth > 0; ) {
|
|
73891
|
+
const c = source[i22];
|
|
73892
|
+
if (c === "[") {
|
|
73893
|
+
depth++;
|
|
73894
|
+
i22++;
|
|
73895
|
+
} else if (c === "]") {
|
|
73896
|
+
depth--;
|
|
73897
|
+
i22++;
|
|
73898
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
73899
|
+
let j = i22 + 1;
|
|
73900
|
+
while (j < source.length && source[j] !== c) j += source[j] === "\\" ? 2 : 1;
|
|
73901
|
+
out.push({ kind: "string", value: source.slice(i22 + 1, j) });
|
|
73902
|
+
i22 = j + 1;
|
|
73903
|
+
} else if (c === "/" && source[i22 + 1] === "/") {
|
|
73904
|
+
const nl = source.indexOf("\n", i22);
|
|
73905
|
+
i22 = nl === -1 ? source.length : nl + 1;
|
|
73906
|
+
} else if (c === "/" && source[i22 + 1] === "*") {
|
|
73907
|
+
const end = source.indexOf("*/", i22);
|
|
73908
|
+
i22 = end === -1 ? source.length : end + 2;
|
|
73909
|
+
} else if (c === "/") {
|
|
73910
|
+
let j = i22 + 1;
|
|
73911
|
+
for (let inClass = false; j < source.length; j++) {
|
|
73912
|
+
const ch = source[j];
|
|
73913
|
+
if (ch === "\\") j++;
|
|
73914
|
+
else if (ch === "[") inClass = true;
|
|
73915
|
+
else if (ch === "]") inClass = false;
|
|
73916
|
+
else if (ch === "/" && !inClass) break;
|
|
73917
|
+
else if (ch === "\n") break;
|
|
73918
|
+
}
|
|
73919
|
+
if (source[j] === "/") {
|
|
73920
|
+
let k = j + 1;
|
|
73921
|
+
while (k < source.length && /[a-z]/.test(source[k])) k++;
|
|
73922
|
+
out.push({ kind: "regex", source: source.slice(i22 + 1, j), flags: source.slice(j + 1, k) });
|
|
73923
|
+
i22 = k;
|
|
73924
|
+
} else i22++;
|
|
73925
|
+
} else i22++;
|
|
73926
|
+
}
|
|
73927
|
+
}
|
|
73928
|
+
return out;
|
|
73929
|
+
}
|
|
73930
|
+
function inlinesKitUnderVitest(viteConfig) {
|
|
73931
|
+
return inlineEntries(viteConfig).some((entry) => {
|
|
73932
|
+
if (entry.kind === "everything") return true;
|
|
73933
|
+
if (entry.kind === "string")
|
|
73934
|
+
return KIT_SPECIFIER === entry.value || KIT_SPECIFIER.startsWith(`${entry.value}/`);
|
|
73935
|
+
try {
|
|
73936
|
+
const re2 = new RegExp(entry.source, entry.flags.replace("g", ""));
|
|
73937
|
+
return re2.test(KIT_SPECIFIER) || re2.test(KIT_INSTALLED_PATH);
|
|
73938
|
+
} catch {
|
|
73939
|
+
return false;
|
|
73940
|
+
}
|
|
73941
|
+
});
|
|
73942
|
+
}
|
|
73423
73943
|
function ensureAppVitestSetup(projectDir) {
|
|
73424
73944
|
const setupPath = path9.join(projectDir, VITEST_SETUP_FILENAME);
|
|
73425
73945
|
if (!fs8.existsSync(setupPath)) {
|
|
@@ -73429,11 +73949,18 @@ function ensureAppVitestSetup(projectDir) {
|
|
|
73429
73949
|
const viteConfigPath = path9.join(projectDir, "vite.config.ts");
|
|
73430
73950
|
if (!fs8.existsSync(viteConfigPath)) return;
|
|
73431
73951
|
const viteConfig = fs8.readFileSync(viteConfigPath, "utf-8");
|
|
73432
|
-
if (/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME))
|
|
73433
|
-
|
|
73434
|
-
|
|
73952
|
+
if (!(/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME))) {
|
|
73953
|
+
console.error(
|
|
73954
|
+
`\u26A0 vite.config.ts does not wire ${VITEST_SETUP_FILENAME}. Nothing is broken by that \u2014 it is the slot for global test setup (matchers, stubs, timezone pinning). Add it to the \`test\` block when you need one:
|
|
73435
73955
|
setupFiles: ["./${VITEST_SETUP_FILENAME}"],`
|
|
73436
|
-
|
|
73956
|
+
);
|
|
73957
|
+
}
|
|
73958
|
+
if (!inlinesKitUnderVitest(viteConfig)) {
|
|
73959
|
+
console.error(
|
|
73960
|
+
`\u26A0 vite.config.ts does not inline @lotics/ui under vitest, so \`npm test\` fails to load every suite that renders a kit component ("Unknown file extension \\".css\\"") \u2014 the kit's modules import their own stylesheets and vitest hands node_modules to Node. Add it to the \`test\` block:
|
|
73961
|
+
server: { deps: { inline: [/@lotics\\/ui/] } },`
|
|
73962
|
+
);
|
|
73963
|
+
}
|
|
73437
73964
|
}
|
|
73438
73965
|
async function appCodegen(args) {
|
|
73439
73966
|
const projectDir = path9.resolve(args.projectDir ?? process.cwd());
|
|
@@ -73819,12 +74346,12 @@ async function appPull(client, args) {
|
|
|
73819
74346
|
const targetPath = path9.resolve(args.targetPath ?? defaultPullTarget(app.id, app.name));
|
|
73820
74347
|
fs8.mkdirSync(targetPath, { recursive: true });
|
|
73821
74348
|
const prior = readPriorStamp(targetPath);
|
|
73822
|
-
const tmpFile = path9.join(
|
|
74349
|
+
const tmpFile = path9.join(tmpdir2(), `lotics-app-${app.id}-${Date.now()}.tar.gz`);
|
|
73823
74350
|
console.error(`Downloading source archive...`);
|
|
73824
74351
|
await downloadToFile(sourceUrl, tmpFile);
|
|
73825
74352
|
const keptLocal = [];
|
|
73826
74353
|
const restoredFromArchive = [];
|
|
73827
|
-
const stagingDir = fs8.mkdtempSync(path9.join(
|
|
74354
|
+
const stagingDir = fs8.mkdtempSync(path9.join(tmpdir2(), `lotics-pull-${app.id}-`));
|
|
73828
74355
|
try {
|
|
73829
74356
|
await runTar(["-xzf", tmpFile, "-C", stagingDir], stagingDir);
|
|
73830
74357
|
console.error(`Extracting to ${targetPath}...`);
|
|
@@ -73963,8 +74490,8 @@ async function appDeploy(client, args) {
|
|
|
73963
74490
|
`Build did not produce a dist/ directory in ${projectDir}. Check that 'npm run build' is configured correctly (Vite/etc. defaults to dist/).`
|
|
73964
74491
|
);
|
|
73965
74492
|
}
|
|
73966
|
-
const tmpSource = path9.join(
|
|
73967
|
-
const tmpDist = path9.join(
|
|
74493
|
+
const tmpSource = path9.join(tmpdir2(), `lotics-source-${Date.now()}.tar.gz`);
|
|
74494
|
+
const tmpDist = path9.join(tmpdir2(), `lotics-dist-${Date.now()}.tar.gz`);
|
|
73968
74495
|
try {
|
|
73969
74496
|
note("Packaging source...");
|
|
73970
74497
|
await runTar(
|
|
@@ -74154,11 +74681,27 @@ async function appCheck(client, args = {}) {
|
|
|
74154
74681
|
console.error(err2 instanceof Error ? err2.message : String(err2));
|
|
74155
74682
|
typesFailing = true;
|
|
74156
74683
|
}
|
|
74157
|
-
|
|
74684
|
+
let screenFindings = 0;
|
|
74685
|
+
if (args.screens === true && !typesFailing) {
|
|
74686
|
+
const report = await checkAppScreens(client, {
|
|
74687
|
+
projectDir,
|
|
74688
|
+
app_id: meta3.app_id,
|
|
74689
|
+
app_name: app.name,
|
|
74690
|
+
workspace_id: meta3.workspace_id,
|
|
74691
|
+
commentsEnabled: meta3.capabilities?.comments
|
|
74692
|
+
});
|
|
74693
|
+
screenFindings = report.findings.length;
|
|
74694
|
+
noteScope({
|
|
74695
|
+
probe_findings: screenFindings,
|
|
74696
|
+
probes: [...new Set(report.findings.map((finding) => finding.probe))]
|
|
74697
|
+
});
|
|
74698
|
+
console.error(formatScreenFindings(report));
|
|
74699
|
+
}
|
|
74700
|
+
if (!nothingPending(pending) || unportable.length > 0 || bodiesFailing > 0 || typesFailing || screenFindings > 0) {
|
|
74158
74701
|
process.exit(1);
|
|
74159
74702
|
}
|
|
74160
74703
|
console.error(
|
|
74161
|
-
"Checked the app's types, bindings, capabilities, agent schemas, workflow bodies and id portability \u2014 nothing blocking."
|
|
74704
|
+
"Checked the app's types, bindings, capabilities, agent schemas, workflow bodies and id portability" + (args.screens === true ? ", and every screen at 1280 and 375" : "") + " \u2014 nothing blocking."
|
|
74162
74705
|
);
|
|
74163
74706
|
}
|
|
74164
74707
|
async function countFailingWorkflowBodies(client, projectDir, meta3) {
|
|
@@ -74440,13 +74983,13 @@ function derivedDeployMessage(pending) {
|
|
|
74440
74983
|
];
|
|
74441
74984
|
return parts.length === 0 ? "Deployed app code" : `Deployed app code; pushed ${parts.join(", ")}`;
|
|
74442
74985
|
}
|
|
74443
|
-
function copyTree(from, to, opts,
|
|
74986
|
+
function copyTree(from, to, opts, relative2 = "") {
|
|
74444
74987
|
const kept = [];
|
|
74445
74988
|
const created = [];
|
|
74446
74989
|
for (const entry of fs8.readdirSync(from, { withFileTypes: true })) {
|
|
74447
74990
|
const src = path9.join(from, entry.name);
|
|
74448
74991
|
const dest = path9.join(to, entry.name);
|
|
74449
|
-
const rel = path9.join(
|
|
74992
|
+
const rel = path9.join(relative2, entry.name);
|
|
74450
74993
|
if (opts.skip?.includes(rel.split(path9.sep).join("/"))) continue;
|
|
74451
74994
|
if (entry.isDirectory()) {
|
|
74452
74995
|
fs8.mkdirSync(dest, { recursive: true });
|
|
@@ -74606,6 +75149,13 @@ function isVersionBelow(version2, floor2) {
|
|
|
74606
75149
|
if (minor !== floorMinor) return minor < floorMinor;
|
|
74607
75150
|
return patch < floorPatch;
|
|
74608
75151
|
}
|
|
75152
|
+
function bundlesReactNative(projectDir) {
|
|
75153
|
+
const manifest = path9.join(projectDir, "package.json");
|
|
75154
|
+
if (!fs8.existsSync(manifest)) return false;
|
|
75155
|
+
const pkg = JSON.parse(fs8.readFileSync(manifest, "utf-8"));
|
|
75156
|
+
const declared = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
75157
|
+
return "react-native" in declared || "react-native-web" in declared;
|
|
75158
|
+
}
|
|
74609
75159
|
function warnIfProjectFootguns(projectDir, sourceText) {
|
|
74610
75160
|
const lines = [];
|
|
74611
75161
|
const sdkVersion = installedKitVersion(projectDir, "@lotics/app-sdk");
|
|
@@ -74621,7 +75171,7 @@ function warnIfProjectFootguns(projectDir, sourceText) {
|
|
|
74621
75171
|
);
|
|
74622
75172
|
}
|
|
74623
75173
|
const configName = VITE_CONFIG_NAMES.find((name2) => fs8.existsSync(path9.join(projectDir, name2)));
|
|
74624
|
-
if (configName !== void 0) {
|
|
75174
|
+
if (configName !== void 0 && bundlesReactNative(projectDir)) {
|
|
74625
75175
|
const config2 = fs8.readFileSync(path9.join(projectDir, configName), "utf-8");
|
|
74626
75176
|
const defined = defineBlockKeys(config2);
|
|
74627
75177
|
const missing = ["global", "__DEV__"].filter((name2) => !defined.has(name2));
|
|
@@ -75187,6 +75737,7 @@ function parseArgs(argv) {
|
|
|
75187
75737
|
printCreated: false,
|
|
75188
75738
|
cleanup: false,
|
|
75189
75739
|
prune: false,
|
|
75740
|
+
screens: false,
|
|
75190
75741
|
noSampleData: false,
|
|
75191
75742
|
adopt: false,
|
|
75192
75743
|
entity: [],
|
|
@@ -75357,6 +75908,9 @@ function parseArgs(argv) {
|
|
|
75357
75908
|
case "--prune":
|
|
75358
75909
|
flags.prune = true;
|
|
75359
75910
|
break;
|
|
75911
|
+
case "--screens":
|
|
75912
|
+
flags.screens = true;
|
|
75913
|
+
break;
|
|
75360
75914
|
case "--version":
|
|
75361
75915
|
case "-v":
|
|
75362
75916
|
flags.version = true;
|
|
@@ -75949,10 +76503,10 @@ var packageContractSchema = zod_default.object({
|
|
|
75949
76503
|
// copy WARNS (advisory) when a name has no doc in the workspace.
|
|
75950
76504
|
knowledge_expects: zod_default.array(zod_default.string()).default([])
|
|
75951
76505
|
});
|
|
75952
|
-
function checkTraversalFilter(node,
|
|
76506
|
+
function checkTraversalFilter(node, path14, contextEntity, fieldByEntity, errors2) {
|
|
75953
76507
|
if (contextEntity === null) {
|
|
75954
76508
|
errors2.push({
|
|
75955
|
-
path:
|
|
76509
|
+
path: path14,
|
|
75956
76510
|
message: `traversal filter "${node.path[0]}" resolves only directly over a from_table`
|
|
75957
76511
|
});
|
|
75958
76512
|
return;
|
|
@@ -75962,14 +76516,14 @@ function checkTraversalFilter(node, path13, contextEntity, fieldByEntity, errors
|
|
|
75962
76516
|
const parsed2 = parseFieldKeyName(hop);
|
|
75963
76517
|
if (parsed2 === null) {
|
|
75964
76518
|
errors2.push({
|
|
75965
|
-
path:
|
|
76519
|
+
path: path14,
|
|
75966
76520
|
message: `traversal path[${index}] "${hop}" is not an entity.field alias`
|
|
75967
76521
|
});
|
|
75968
76522
|
return;
|
|
75969
76523
|
}
|
|
75970
76524
|
if (parsed2.entity !== current) {
|
|
75971
76525
|
errors2.push({
|
|
75972
|
-
path:
|
|
76526
|
+
path: path14,
|
|
75973
76527
|
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}"`
|
|
75974
76528
|
});
|
|
75975
76529
|
return;
|
|
@@ -75977,14 +76531,14 @@ function checkTraversalFilter(node, path13, contextEntity, fieldByEntity, errors
|
|
|
75977
76531
|
const field = fieldByEntity.get(parsed2.entity)?.get(parsed2.field);
|
|
75978
76532
|
if (field === void 0) {
|
|
75979
76533
|
errors2.push({
|
|
75980
|
-
path:
|
|
76534
|
+
path: path14,
|
|
75981
76535
|
message: `traversal path[${index}] "${hop}" is not a field on entity "${parsed2.entity}"`
|
|
75982
76536
|
});
|
|
75983
76537
|
return;
|
|
75984
76538
|
}
|
|
75985
76539
|
if (field.type !== "select_record_link") {
|
|
75986
76540
|
errors2.push({
|
|
75987
|
-
path:
|
|
76541
|
+
path: path14,
|
|
75988
76542
|
message: `traversal path[${index}] "${hop}" is a ${field.type} field, not select_record_link`
|
|
75989
76543
|
});
|
|
75990
76544
|
return;
|
|
@@ -75996,7 +76550,7 @@ function checkTraversalFilter(node, path13, contextEntity, fieldByEntity, errors
|
|
|
75996
76550
|
const parsed = parseFieldKeyName(innerField);
|
|
75997
76551
|
if (parsed === null || parsed.entity !== current) {
|
|
75998
76552
|
errors2.push({
|
|
75999
|
-
path:
|
|
76553
|
+
path: path14,
|
|
76000
76554
|
message: `traversal condition "${innerField}" must be an entity.field alias on "${current}", the entity its path lands on`
|
|
76001
76555
|
});
|
|
76002
76556
|
return;
|
|
@@ -76004,12 +76558,12 @@ function checkTraversalFilter(node, path13, contextEntity, fieldByEntity, errors
|
|
|
76004
76558
|
const target = fieldByEntity.get(parsed.entity)?.get(parsed.field);
|
|
76005
76559
|
if (target === void 0) {
|
|
76006
76560
|
errors2.push({
|
|
76007
|
-
path:
|
|
76561
|
+
path: path14,
|
|
76008
76562
|
message: `traversal condition "${innerField}" is not a field on entity "${parsed.entity}"`
|
|
76009
76563
|
});
|
|
76010
76564
|
return;
|
|
76011
76565
|
}
|
|
76012
|
-
checkConditionShape(node.condition, parsed.entity, target,
|
|
76566
|
+
checkConditionShape(node.condition, parsed.entity, target, path14, fieldByEntity, errors2);
|
|
76013
76567
|
}
|
|
76014
76568
|
function contractFilterType(entityAlias, field, fieldByEntity, hops = 0) {
|
|
76015
76569
|
if (field.type === "formula") {
|
|
@@ -76025,12 +76579,12 @@ function contractFilterType(entityAlias, field, fieldByEntity, hops = 0) {
|
|
|
76025
76579
|
}
|
|
76026
76580
|
return resolveFilterType(field);
|
|
76027
76581
|
}
|
|
76028
|
-
function checkConditionShape(condition, entityAlias, field,
|
|
76582
|
+
function checkConditionShape(condition, entityAlias, field, path14, fieldByEntity, errors2) {
|
|
76029
76583
|
const type = contractFilterType(entityAlias, field, fieldByEntity);
|
|
76030
76584
|
if (type === void 0 && condition.type === void 0) return;
|
|
76031
76585
|
const typed = type === void 0 ? condition : { ...condition, type };
|
|
76032
|
-
for (const message2 of validateFilterCondition(typed,
|
|
76033
|
-
errors2.push({ path:
|
|
76586
|
+
for (const message2 of validateFilterCondition(typed, path14)) {
|
|
76587
|
+
errors2.push({ path: path14, message: message2.startsWith(`${path14}: `) ? message2.slice(path14.length + 2) : message2 });
|
|
76034
76588
|
}
|
|
76035
76589
|
if (field.type !== "select") return;
|
|
76036
76590
|
const declared = new Set(field.options.map((option) => option.alias));
|
|
@@ -76038,20 +76592,20 @@ function checkConditionShape(condition, entityAlias, field, path13, fieldByEntit
|
|
|
76038
76592
|
for (const raw of values3) {
|
|
76039
76593
|
if (typeof raw !== "string" || isTemplateExpression(raw) || declared.has(raw)) continue;
|
|
76040
76594
|
errors2.push({
|
|
76041
|
-
path:
|
|
76595
|
+
path: path14,
|
|
76042
76596
|
message: `filters field "${field.alias}" on option "${raw}", which that field does not declare (${declared.size > 0 ? [...declared].join(", ") : "it declares none"})`
|
|
76043
76597
|
});
|
|
76044
76598
|
}
|
|
76045
76599
|
}
|
|
76046
|
-
function checkFilterFields(filters,
|
|
76600
|
+
function checkFilterFields(filters, path14, contextEntity, fieldByEntity, errors2, checkField) {
|
|
76047
76601
|
if (filters.node_type === "group") {
|
|
76048
76602
|
for (const child of filters.children) {
|
|
76049
|
-
checkFilterFields(child,
|
|
76603
|
+
checkFilterFields(child, path14, contextEntity, fieldByEntity, errors2, checkField);
|
|
76050
76604
|
}
|
|
76051
76605
|
return;
|
|
76052
76606
|
}
|
|
76053
76607
|
if (filters.node_type === "traversal") {
|
|
76054
|
-
checkTraversalFilter(filters,
|
|
76608
|
+
checkTraversalFilter(filters, path14, contextEntity, fieldByEntity, errors2);
|
|
76055
76609
|
return;
|
|
76056
76610
|
}
|
|
76057
76611
|
if (filters.type === "current_member") return;
|
|
@@ -76059,13 +76613,13 @@ function checkFilterFields(filters, path13, contextEntity, fieldByEntity, errors
|
|
|
76059
76613
|
checkField(filters.field_key);
|
|
76060
76614
|
if (contextEntity === null) return;
|
|
76061
76615
|
const field = fieldByEntity.get(contextEntity)?.get(filters.field_key);
|
|
76062
|
-
if (field !== void 0) checkConditionShape(filters, contextEntity, field,
|
|
76616
|
+
if (field !== void 0) checkConditionShape(filters, contextEntity, field, path14, fieldByEntity, errors2);
|
|
76063
76617
|
}
|
|
76064
|
-
function checkInputRefs(input,
|
|
76618
|
+
function checkInputRefs(input, path14, entityAliases, roleAliases, optionAliases, fieldAliases, errors2) {
|
|
76065
76619
|
if (input.type === "record_link") {
|
|
76066
76620
|
if (!entityAliases.has(input.table_id)) {
|
|
76067
76621
|
errors2.push({
|
|
76068
|
-
path:
|
|
76622
|
+
path: path14,
|
|
76069
76623
|
message: `record_link input references entity "${input.table_id}" which is not a declared entity`
|
|
76070
76624
|
});
|
|
76071
76625
|
}
|
|
@@ -76074,7 +76628,7 @@ function checkInputRefs(input, path13, entityAliases, roleAliases, optionAliases
|
|
|
76074
76628
|
if (input.type === "member") {
|
|
76075
76629
|
if (input.group !== void 0 && !roleAliases.has(input.group)) {
|
|
76076
76630
|
errors2.push({
|
|
76077
|
-
path:
|
|
76631
|
+
path: path14,
|
|
76078
76632
|
message: `member input references role "${input.group}" which is not a declared role`
|
|
76079
76633
|
});
|
|
76080
76634
|
}
|
|
@@ -76084,7 +76638,7 @@ function checkInputRefs(input, path13, entityAliases, roleAliases, optionAliases
|
|
|
76084
76638
|
if (input.field !== void 0) {
|
|
76085
76639
|
if (!fieldAliases.has(input.field)) {
|
|
76086
76640
|
errors2.push({
|
|
76087
|
-
path:
|
|
76641
|
+
path: path14,
|
|
76088
76642
|
message: `select input references field "${input.field}" which is not a declared field alias (entity.field)`
|
|
76089
76643
|
});
|
|
76090
76644
|
}
|
|
@@ -76093,7 +76647,7 @@ function checkInputRefs(input, path13, entityAliases, roleAliases, optionAliases
|
|
|
76093
76647
|
for (const option of input.options ?? []) {
|
|
76094
76648
|
if (option.value.includes(":") && !optionAliases.has(option.value)) {
|
|
76095
76649
|
errors2.push({
|
|
76096
|
-
path:
|
|
76650
|
+
path: path14,
|
|
76097
76651
|
message: `select input option value "${option.value}" is not a declared option alias (entity.field:opt)`
|
|
76098
76652
|
});
|
|
76099
76653
|
}
|
|
@@ -76102,19 +76656,19 @@ function checkInputRefs(input, path13, entityAliases, roleAliases, optionAliases
|
|
|
76102
76656
|
}
|
|
76103
76657
|
if (input.type === "object") {
|
|
76104
76658
|
for (const [key, child] of Object.entries(input.fields)) {
|
|
76105
|
-
checkInputRefs(child, `${
|
|
76659
|
+
checkInputRefs(child, `${path14}.${key}`, entityAliases, roleAliases, optionAliases, fieldAliases, errors2);
|
|
76106
76660
|
}
|
|
76107
76661
|
return;
|
|
76108
76662
|
}
|
|
76109
76663
|
if (input.type === "array") {
|
|
76110
|
-
checkInputRefs(input.items, `${
|
|
76664
|
+
checkInputRefs(input.items, `${path14}[]`, entityAliases, roleAliases, optionAliases, fieldAliases, errors2);
|
|
76111
76665
|
}
|
|
76112
76666
|
}
|
|
76113
|
-
function checkOutputRefs(output,
|
|
76667
|
+
function checkOutputRefs(output, path14, entityAliases, optionAliases, fieldAliases, errors2) {
|
|
76114
76668
|
if (output.type === "record_link") {
|
|
76115
76669
|
if (!entityAliases.has(output.table_id)) {
|
|
76116
76670
|
errors2.push({
|
|
76117
|
-
path:
|
|
76671
|
+
path: path14,
|
|
76118
76672
|
message: `record_link output references entity "${output.table_id}" which is not a declared entity`
|
|
76119
76673
|
});
|
|
76120
76674
|
}
|
|
@@ -76124,7 +76678,7 @@ function checkOutputRefs(output, path13, entityAliases, optionAliases, fieldAlia
|
|
|
76124
76678
|
if (output.field !== void 0) {
|
|
76125
76679
|
if (!fieldAliases.has(output.field)) {
|
|
76126
76680
|
errors2.push({
|
|
76127
|
-
path:
|
|
76681
|
+
path: path14,
|
|
76128
76682
|
message: `select output references field "${output.field}" which is not a declared field alias (entity.field)`
|
|
76129
76683
|
});
|
|
76130
76684
|
}
|
|
@@ -76133,7 +76687,7 @@ function checkOutputRefs(output, path13, entityAliases, optionAliases, fieldAlia
|
|
|
76133
76687
|
for (const option of output.options ?? []) {
|
|
76134
76688
|
if (option.value.includes(":") && !optionAliases.has(option.value)) {
|
|
76135
76689
|
errors2.push({
|
|
76136
|
-
path:
|
|
76690
|
+
path: path14,
|
|
76137
76691
|
message: `select output option value "${option.value}" is not a declared option alias (entity.field:opt)`
|
|
76138
76692
|
});
|
|
76139
76693
|
}
|
|
@@ -76142,16 +76696,16 @@ function checkOutputRefs(output, path13, entityAliases, optionAliases, fieldAlia
|
|
|
76142
76696
|
}
|
|
76143
76697
|
if (output.type === "object") {
|
|
76144
76698
|
for (const [key, child] of Object.entries(output.fields)) {
|
|
76145
|
-
checkOutputRefs(child, `${
|
|
76699
|
+
checkOutputRefs(child, `${path14}.${key}`, entityAliases, optionAliases, fieldAliases, errors2);
|
|
76146
76700
|
}
|
|
76147
76701
|
return;
|
|
76148
76702
|
}
|
|
76149
76703
|
if (output.type === "array") {
|
|
76150
|
-
checkOutputRefs(output.items, `${
|
|
76704
|
+
checkOutputRefs(output.items, `${path14}[]`, entityAliases, optionAliases, fieldAliases, errors2);
|
|
76151
76705
|
}
|
|
76152
76706
|
}
|
|
76153
|
-
function checkSourceLinks(source,
|
|
76154
|
-
const recur = (child) => checkSourceLinks(child,
|
|
76707
|
+
function checkSourceLinks(source, path14, parentEntity, fieldByEntity, errors2) {
|
|
76708
|
+
const recur = (child) => checkSourceLinks(child, path14, parentEntity, fieldByEntity, errors2);
|
|
76155
76709
|
if (typeof source === "string") return;
|
|
76156
76710
|
if ("eq" in source) return source.eq.forEach(recur);
|
|
76157
76711
|
if ("neq" in source) return source.neq.forEach(recur);
|
|
@@ -76162,7 +76716,7 @@ function checkSourceLinks(source, path13, parentEntity, fieldByEntity, errors2)
|
|
|
76162
76716
|
if (!("link" in source)) return;
|
|
76163
76717
|
if (parentEntity === null) {
|
|
76164
76718
|
errors2.push({
|
|
76165
|
-
path:
|
|
76719
|
+
path: path14,
|
|
76166
76720
|
message: `link source "${source.link.source}" resolves only directly over a from_table`
|
|
76167
76721
|
});
|
|
76168
76722
|
return;
|
|
@@ -76172,14 +76726,14 @@ function checkSourceLinks(source, path13, parentEntity, fieldByEntity, errors2)
|
|
|
76172
76726
|
const sourceField = parentFields.get(source.link.source);
|
|
76173
76727
|
if (sourceField === void 0) {
|
|
76174
76728
|
errors2.push({
|
|
76175
|
-
path:
|
|
76729
|
+
path: path14,
|
|
76176
76730
|
message: `link source "${source.link.source}" is not a field on entity "${parentEntity}"`
|
|
76177
76731
|
});
|
|
76178
76732
|
return;
|
|
76179
76733
|
}
|
|
76180
76734
|
if (sourceField.type !== "select_record_link") {
|
|
76181
76735
|
errors2.push({
|
|
76182
|
-
path:
|
|
76736
|
+
path: path14,
|
|
76183
76737
|
message: `link source "${source.link.source}" on entity "${parentEntity}" is a ${sourceField.type} field, not select_record_link`
|
|
76184
76738
|
});
|
|
76185
76739
|
return;
|
|
@@ -76188,68 +76742,68 @@ function checkSourceLinks(source, path13, parentEntity, fieldByEntity, errors2)
|
|
|
76188
76742
|
if (targetFields === void 0) return;
|
|
76189
76743
|
if (!targetFields.has(source.link.field)) {
|
|
76190
76744
|
errors2.push({
|
|
76191
|
-
path:
|
|
76745
|
+
path: path14,
|
|
76192
76746
|
message: `link field "${source.link.field}" is not a field on target entity "${sourceField.target_entity}"`
|
|
76193
76747
|
});
|
|
76194
76748
|
}
|
|
76195
76749
|
}
|
|
76196
|
-
function checkQueryFilterTraversals(filters,
|
|
76197
|
-
checkFilterFields(filters,
|
|
76750
|
+
function checkQueryFilterTraversals(filters, path14, contextEntity, fieldByEntity, errors2) {
|
|
76751
|
+
checkFilterFields(filters, path14, contextEntity, fieldByEntity, errors2, () => {
|
|
76198
76752
|
});
|
|
76199
76753
|
}
|
|
76200
|
-
function checkQueryEntityRefs(node,
|
|
76754
|
+
function checkQueryEntityRefs(node, path14, entityAliases, fieldByEntity, errors2) {
|
|
76201
76755
|
const recur = (child, childPath) => checkQueryEntityRefs(child, childPath, entityAliases, fieldByEntity, errors2);
|
|
76202
76756
|
const linkParent = (from) => from.kind === "from_table" ? from.from_entity : null;
|
|
76203
76757
|
switch (node.kind) {
|
|
76204
76758
|
case "from_table":
|
|
76205
76759
|
if (!entityAliases.has(node.from_entity)) {
|
|
76206
76760
|
errors2.push({
|
|
76207
|
-
path:
|
|
76761
|
+
path: path14,
|
|
76208
76762
|
message: `from_table references entity "${node.from_entity}" which is not a declared entity`
|
|
76209
76763
|
});
|
|
76210
76764
|
return;
|
|
76211
76765
|
}
|
|
76212
76766
|
if (node.filter !== void 0) {
|
|
76213
|
-
checkQueryFilterTraversals(node.filter, `${
|
|
76767
|
+
checkQueryFilterTraversals(node.filter, `${path14}.filter`, node.from_entity, fieldByEntity, errors2);
|
|
76214
76768
|
}
|
|
76215
76769
|
return;
|
|
76216
76770
|
case "filter":
|
|
76217
76771
|
checkQueryFilterTraversals(
|
|
76218
76772
|
node.predicate,
|
|
76219
|
-
`${
|
|
76773
|
+
`${path14}.predicate`,
|
|
76220
76774
|
node.from.kind === "from_table" ? node.from.from_entity : null,
|
|
76221
76775
|
fieldByEntity,
|
|
76222
76776
|
errors2
|
|
76223
76777
|
);
|
|
76224
|
-
recur(node.from, `${
|
|
76778
|
+
recur(node.from, `${path14}.from`);
|
|
76225
76779
|
return;
|
|
76226
76780
|
case "join":
|
|
76227
|
-
recur(node.left, `${
|
|
76228
|
-
recur(node.right, `${
|
|
76781
|
+
recur(node.left, `${path14}.left`);
|
|
76782
|
+
recur(node.right, `${path14}.right`);
|
|
76229
76783
|
return;
|
|
76230
76784
|
case "union":
|
|
76231
|
-
node.sources.forEach((source, i2) => recur(source, `${
|
|
76785
|
+
node.sources.forEach((source, i2) => recur(source, `${path14}.sources[${i2}]`));
|
|
76232
76786
|
return;
|
|
76233
76787
|
case "project":
|
|
76234
76788
|
node.columns.forEach((column, i2) => {
|
|
76235
76789
|
if (typeof column === "string") return;
|
|
76236
|
-
checkSourceLinks(column.source, `${
|
|
76790
|
+
checkSourceLinks(column.source, `${path14}.columns[${i2}]`, linkParent(node.from), fieldByEntity, errors2);
|
|
76237
76791
|
});
|
|
76238
|
-
recur(node.from, `${
|
|
76792
|
+
recur(node.from, `${path14}.from`);
|
|
76239
76793
|
return;
|
|
76240
76794
|
case "unpivot":
|
|
76241
76795
|
node.passthrough.forEach((column, i2) => {
|
|
76242
|
-
checkSourceLinks(column.source, `${
|
|
76796
|
+
checkSourceLinks(column.source, `${path14}.passthrough[${i2}]`, linkParent(node.from), fieldByEntity, errors2);
|
|
76243
76797
|
});
|
|
76244
76798
|
node.rows.forEach((row, i2) => {
|
|
76245
76799
|
for (const [key, source] of Object.entries(row)) {
|
|
76246
|
-
checkSourceLinks(source, `${
|
|
76800
|
+
checkSourceLinks(source, `${path14}.rows[${i2}].${key}`, linkParent(node.from), fieldByEntity, errors2);
|
|
76247
76801
|
}
|
|
76248
76802
|
});
|
|
76249
|
-
recur(node.from, `${
|
|
76803
|
+
recur(node.from, `${path14}.from`);
|
|
76250
76804
|
return;
|
|
76251
76805
|
default:
|
|
76252
|
-
recur(node.from, `${
|
|
76806
|
+
recur(node.from, `${path14}.from`);
|
|
76253
76807
|
}
|
|
76254
76808
|
}
|
|
76255
76809
|
var WORKFLOW_BODY_TOKEN_PATTERN = /@@(entity|field|option|role|template):([^@]+)@@/g;
|
|
@@ -76753,6 +77307,357 @@ var bindingDriftSchema = zod_default.object({
|
|
|
76753
77307
|
id: zod_default.string().describe("The stale concrete id the alias was bound to")
|
|
76754
77308
|
});
|
|
76755
77309
|
|
|
77310
|
+
// ../shared/src/schemas/field_roles.ts
|
|
77311
|
+
var fieldRoleSchema = zod_default.enum([
|
|
77312
|
+
"identity",
|
|
77313
|
+
"mark",
|
|
77314
|
+
"lifecycle",
|
|
77315
|
+
"measure",
|
|
77316
|
+
"expected_set",
|
|
77317
|
+
"amount",
|
|
77318
|
+
"when",
|
|
77319
|
+
"party",
|
|
77320
|
+
"contact",
|
|
77321
|
+
"verdict"
|
|
77322
|
+
]);
|
|
77323
|
+
var FIELD_ROLE_TYPES = {
|
|
77324
|
+
identity: ["text", "autonumber", "select_record_link"],
|
|
77325
|
+
mark: ["files"],
|
|
77326
|
+
lifecycle: ["select"],
|
|
77327
|
+
measure: ["number", "formula", "rollup"],
|
|
77328
|
+
expected_set: ["select"],
|
|
77329
|
+
amount: ["number", "formula", "rollup"],
|
|
77330
|
+
when: ["date"],
|
|
77331
|
+
party: ["select_record_link"],
|
|
77332
|
+
contact: ["text"],
|
|
77333
|
+
verdict: ["boolean", "formula"]
|
|
77334
|
+
};
|
|
77335
|
+
var SINGLE_FIELD_ROLES = [
|
|
77336
|
+
"identity",
|
|
77337
|
+
"mark",
|
|
77338
|
+
"lifecycle",
|
|
77339
|
+
"amount",
|
|
77340
|
+
"when",
|
|
77341
|
+
"party",
|
|
77342
|
+
"contact",
|
|
77343
|
+
"verdict"
|
|
77344
|
+
];
|
|
77345
|
+
var fieldRoleDeclSchema = zod_default.object({
|
|
77346
|
+
role: fieldRoleSchema,
|
|
77347
|
+
against: zod_default.union([contractAliasSchema, zod_default.number()]).optional().describe("For a measure: the limit it is read against \u2014 a number field on the same entity, by alias, or a constant"),
|
|
77348
|
+
alert: zod_default.enum(["over", "under"]).optional().describe("With against: which side of the limit needs attention \u2014 over a capacity, under a minimum")
|
|
77349
|
+
}).strict();
|
|
77350
|
+
var fieldRolesSchema = zod_default.record(
|
|
77351
|
+
contractAliasSchema,
|
|
77352
|
+
zod_default.record(
|
|
77353
|
+
contractAliasSchema,
|
|
77354
|
+
zod_default.preprocess((value2) => typeof value2 === "string" ? { role: value2 } : value2, fieldRoleDeclSchema).nullable()
|
|
77355
|
+
)
|
|
77356
|
+
);
|
|
77357
|
+
function roleOf(roles, entity, field) {
|
|
77358
|
+
if (!Object.hasOwn(roles, entity)) return void 0;
|
|
77359
|
+
const fields = roles[entity];
|
|
77360
|
+
return Object.hasOwn(fields, field) ? fields[field] ?? void 0 : void 0;
|
|
77361
|
+
}
|
|
77362
|
+
function checkFieldRoles(entities, roles) {
|
|
77363
|
+
const errors2 = [];
|
|
77364
|
+
const entityByAlias = new Map(entities.map((entity) => [entity.alias, entity]));
|
|
77365
|
+
for (const [entityAlias, fields] of Object.entries(roles)) {
|
|
77366
|
+
const entityPath = `field_roles.${entityAlias}`;
|
|
77367
|
+
const entity = entityByAlias.get(entityAlias);
|
|
77368
|
+
if (entity === void 0) {
|
|
77369
|
+
errors2.push({ path: entityPath, message: `names entity "${entityAlias}", which this model does not declare` });
|
|
77370
|
+
continue;
|
|
77371
|
+
}
|
|
77372
|
+
const fieldByAlias = new Map(entity.fields.map((field) => [field.alias, field]));
|
|
77373
|
+
const seen = /* @__PURE__ */ new Map();
|
|
77374
|
+
for (const [fieldAlias, decl] of Object.entries(fields)) {
|
|
77375
|
+
const path14 = `${entityPath}.${fieldAlias}`;
|
|
77376
|
+
const field = fieldByAlias.get(fieldAlias);
|
|
77377
|
+
if (field === void 0) {
|
|
77378
|
+
errors2.push({ path: path14, message: `names "${fieldAlias}", which is not a field of "${entityAlias}"` });
|
|
77379
|
+
continue;
|
|
77380
|
+
}
|
|
77381
|
+
if (decl === null) {
|
|
77382
|
+
errors2.push({ path: path14, message: `null clears a role a preset declared \u2014 this file starts from none` });
|
|
77383
|
+
continue;
|
|
77384
|
+
}
|
|
77385
|
+
const allowed = FIELD_ROLE_TYPES[decl.role];
|
|
77386
|
+
if (!allowed.includes(field.type)) {
|
|
77387
|
+
errors2.push({ path: path14, message: `role "${decl.role}" sits on a ${allowed.join(" or ")} field, not a ${field.type}` });
|
|
77388
|
+
}
|
|
77389
|
+
if (SINGLE_FIELD_ROLES.includes(decl.role)) {
|
|
77390
|
+
const first3 = seen.get(decl.role);
|
|
77391
|
+
if (first3 !== void 0) {
|
|
77392
|
+
errors2.push({ path: path14, message: `role "${decl.role}" is already "${first3}"'s \u2014 an entity carries it once` });
|
|
77393
|
+
} else {
|
|
77394
|
+
seen.set(decl.role, fieldAlias);
|
|
77395
|
+
}
|
|
77396
|
+
}
|
|
77397
|
+
if (decl.role === "lifecycle" && field.type === "select" && field.multi === true) {
|
|
77398
|
+
errors2.push({ path: path14, message: `a lifecycle is one stage at a time \u2014 a multi-select cannot be one` });
|
|
77399
|
+
}
|
|
77400
|
+
if (decl.against === void 0 && decl.alert === void 0) continue;
|
|
77401
|
+
if (decl.role !== "measure") {
|
|
77402
|
+
errors2.push({ path: path14, message: `against and alert belong to a measure \u2014 this role is "${decl.role}"` });
|
|
77403
|
+
continue;
|
|
77404
|
+
}
|
|
77405
|
+
if (decl.against === void 0 || decl.alert === void 0) {
|
|
77406
|
+
errors2.push({ path: path14, message: `against and alert are declared together \u2014 the limit, and which side of it needs attention` });
|
|
77407
|
+
continue;
|
|
77408
|
+
}
|
|
77409
|
+
if (typeof decl.against === "number") continue;
|
|
77410
|
+
if (decl.against === fieldAlias) {
|
|
77411
|
+
errors2.push({ path: path14, message: `against names this field itself \u2014 a measure is read against another` });
|
|
77412
|
+
continue;
|
|
77413
|
+
}
|
|
77414
|
+
const limit = fieldByAlias.get(decl.against);
|
|
77415
|
+
if (limit === void 0) {
|
|
77416
|
+
errors2.push({ path: path14, message: `against names "${decl.against}", which is not a field of "${entityAlias}"` });
|
|
77417
|
+
} else if (limit.type !== "number") {
|
|
77418
|
+
errors2.push({
|
|
77419
|
+
path: path14,
|
|
77420
|
+
message: `against names "${decl.against}", a ${limit.type} \u2014 a limit a row can state is a number field`
|
|
77421
|
+
});
|
|
77422
|
+
}
|
|
77423
|
+
}
|
|
77424
|
+
}
|
|
77425
|
+
return errors2;
|
|
77426
|
+
}
|
|
77427
|
+
|
|
77428
|
+
// ../shared/src/schemas/workspace_plan.ts
|
|
77429
|
+
var slot = (role, required2 = false) => ({ role, required: required2 });
|
|
77430
|
+
var SHAPE_REGISTRY = {
|
|
77431
|
+
lifecycle_desk: {
|
|
77432
|
+
label: "lifecycle desk",
|
|
77433
|
+
slots: { stage: slot("lifecycle", true), identity: slot("identity", true), party: slot("party"), amount: slot("amount"), when: slot("when") },
|
|
77434
|
+
record: "drawer",
|
|
77435
|
+
tabs: "lifecycle"
|
|
77436
|
+
},
|
|
77437
|
+
party_register: {
|
|
77438
|
+
label: "party register",
|
|
77439
|
+
slots: { identity: slot("identity", true), mark: slot("mark"), contact: slot("contact"), worth: slot("measure"), risk: slot("verdict") },
|
|
77440
|
+
record: "page",
|
|
77441
|
+
tabs: "none"
|
|
77442
|
+
},
|
|
77443
|
+
offering_register: {
|
|
77444
|
+
label: "offering register",
|
|
77445
|
+
slots: { identity: slot("identity", true), mark: slot("mark"), price: slot("amount"), availability: slot("measure") },
|
|
77446
|
+
record: "page",
|
|
77447
|
+
tabs: "none"
|
|
77448
|
+
},
|
|
77449
|
+
transaction_ledger: {
|
|
77450
|
+
label: "transaction ledger",
|
|
77451
|
+
slots: { when: slot("when", true), amount: slot("amount", true), party: slot("party"), document: slot("expected_set") },
|
|
77452
|
+
record: "drawer",
|
|
77453
|
+
tabs: "none"
|
|
77454
|
+
},
|
|
77455
|
+
monitored_asset_set: {
|
|
77456
|
+
label: "monitored-asset set",
|
|
77457
|
+
slots: { identity: slot("identity", true), level: slot("measure", true), stage: slot("lifecycle") },
|
|
77458
|
+
record: "drawer",
|
|
77459
|
+
tabs: "none"
|
|
77460
|
+
},
|
|
77461
|
+
trend_deep_dive: {
|
|
77462
|
+
label: "trend deep-dive",
|
|
77463
|
+
slots: { when: slot("when", true), measure: slot("measure"), amount: slot("amount") },
|
|
77464
|
+
record: "drawer",
|
|
77465
|
+
tabs: "none"
|
|
77466
|
+
}
|
|
77467
|
+
};
|
|
77468
|
+
var SHAPE_NAMES = Object.keys(SHAPE_REGISTRY);
|
|
77469
|
+
var CUSTOM_SHAPE = "custom";
|
|
77470
|
+
var contractScreenSchema = zod_default.object({
|
|
77471
|
+
alias: contractAliasSchema.describe("Stable screen alias, unique within the app"),
|
|
77472
|
+
label: zod_default.string().min(1).describe("What the screen is called in the app"),
|
|
77473
|
+
shape: zod_default.enum([...SHAPE_NAMES, CUSTOM_SHAPE]).describe(`A shape, or "${CUSTOM_SHAPE}" with its own \`roles\``),
|
|
77474
|
+
entity: contractAliasSchema.describe("The entity whose rows this screen is over"),
|
|
77475
|
+
record: zod_default.enum(["drawer", "page"]).optional().describe("How one record opens from the list; absent, the shape decides"),
|
|
77476
|
+
tabs: contractAliasSchema.nullable().optional().describe("A select field on the entity whose options are the tab strip; null for none; absent, the shape decides"),
|
|
77477
|
+
slots: zod_default.record(zod_default.string(), contractAliasSchema).optional().describe("Slot \u2192 the field that fills it, where roles alone cannot decide"),
|
|
77478
|
+
roles: zod_default.record(zod_default.string(), fieldRoleSchema).optional().describe(`For "${CUSTOM_SHAPE}" only: slot \u2192 the role that fills it`)
|
|
77479
|
+
});
|
|
77480
|
+
var modelAppSchema = contractAppSchema.pick({ alias: true, name: true, description: true, icon: true, theme: true }).extend({ screens: zod_default.array(contractScreenSchema).min(1) });
|
|
77481
|
+
function resolveScreen(app, screen, entityByAlias, roles) {
|
|
77482
|
+
const path14 = `apps.${app.alias}.screens.${screen.alias}`;
|
|
77483
|
+
const findings = [];
|
|
77484
|
+
const entity = entityByAlias.get(screen.entity);
|
|
77485
|
+
if (entity === void 0) {
|
|
77486
|
+
findings.push({ path: `${path14}.entity`, message: `names entity "${screen.entity}", which this model does not declare` });
|
|
77487
|
+
}
|
|
77488
|
+
let slotSpec;
|
|
77489
|
+
let shapeLabel;
|
|
77490
|
+
let record2;
|
|
77491
|
+
let tabsRule;
|
|
77492
|
+
if (screen.shape === CUSTOM_SHAPE) {
|
|
77493
|
+
if (screen.roles === void 0 || Object.keys(screen.roles).length === 0) {
|
|
77494
|
+
findings.push({ path: `${path14}.roles`, message: `a "${CUSTOM_SHAPE}" screen declares its slots \u2014 \`roles\` maps each slot to the field role that fills it` });
|
|
77495
|
+
}
|
|
77496
|
+
slotSpec = Object.fromEntries(Object.entries(screen.roles ?? {}).map(([name2, role]) => [name2, slot(role, true)]));
|
|
77497
|
+
shapeLabel = CUSTOM_SHAPE;
|
|
77498
|
+
record2 = screen.record ?? "drawer";
|
|
77499
|
+
tabsRule = "none";
|
|
77500
|
+
} else {
|
|
77501
|
+
const registry2 = SHAPE_REGISTRY[screen.shape];
|
|
77502
|
+
if (screen.roles !== void 0) {
|
|
77503
|
+
findings.push({ path: `${path14}.roles`, message: `"${screen.shape}" declares its own slots \u2014 \`roles\` is for a "${CUSTOM_SHAPE}" screen` });
|
|
77504
|
+
}
|
|
77505
|
+
slotSpec = registry2.slots;
|
|
77506
|
+
shapeLabel = registry2.label;
|
|
77507
|
+
record2 = screen.record ?? registry2.record;
|
|
77508
|
+
tabsRule = registry2.tabs;
|
|
77509
|
+
}
|
|
77510
|
+
for (const name2 of Object.keys(screen.slots ?? {})) {
|
|
77511
|
+
if (Object.hasOwn(slotSpec, name2)) continue;
|
|
77512
|
+
findings.push({ path: `${path14}.slots.${name2}`, message: `"${shapeLabel}" has no slot "${name2}" \u2014 its slots are ${Object.keys(slotSpec).join(", ")}` });
|
|
77513
|
+
}
|
|
77514
|
+
if (entity === void 0) return { type: "invalid", findings };
|
|
77515
|
+
const fieldByAlias = new Map(entity.fields.map((field) => [field.alias, field]));
|
|
77516
|
+
const roleOfField = (field) => roleOf(roles, entity.alias, field.alias)?.role;
|
|
77517
|
+
const slots = [];
|
|
77518
|
+
const boundBy = /* @__PURE__ */ new Map();
|
|
77519
|
+
for (const [name2, spec] of Object.entries(slotSpec)) {
|
|
77520
|
+
const named = screen.slots?.[name2];
|
|
77521
|
+
let field = null;
|
|
77522
|
+
if (named !== void 0) {
|
|
77523
|
+
const candidate = fieldByAlias.get(named);
|
|
77524
|
+
if (candidate === void 0) {
|
|
77525
|
+
findings.push({ path: `${path14}.slots.${name2}`, message: `names "${named}", which is not a field of entity "${entity.alias}"` });
|
|
77526
|
+
continue;
|
|
77527
|
+
}
|
|
77528
|
+
const role = roleOfField(candidate);
|
|
77529
|
+
if (role !== spec.role) {
|
|
77530
|
+
findings.push({
|
|
77531
|
+
path: `${path14}.slots.${name2}`,
|
|
77532
|
+
message: `names "${named}", whose role is ${role === void 0 ? "not declared" : `"${role}"`} \u2014 this slot takes a "${spec.role}" field; declare the role in field_roles`
|
|
77533
|
+
});
|
|
77534
|
+
continue;
|
|
77535
|
+
}
|
|
77536
|
+
field = candidate;
|
|
77537
|
+
} else {
|
|
77538
|
+
const candidates = entity.fields.filter((f) => roleOfField(f) === spec.role);
|
|
77539
|
+
if (candidates.length > 1) {
|
|
77540
|
+
findings.push({
|
|
77541
|
+
path: `${path14}.slots.${name2}`,
|
|
77542
|
+
message: `"${entity.alias}" has ${candidates.length} "${spec.role}" fields (${candidates.map((f) => f.alias).join(", ")}) \u2014 name the one this slot takes`
|
|
77543
|
+
});
|
|
77544
|
+
continue;
|
|
77545
|
+
}
|
|
77546
|
+
if (candidates.length === 0 && spec.required) {
|
|
77547
|
+
findings.push({ path: path14, message: `a ${shapeLabel} needs a "${spec.role}" field on "${entity.alias}" for its ${name2} \u2014 none declares that role` });
|
|
77548
|
+
continue;
|
|
77549
|
+
}
|
|
77550
|
+
field = candidates[0] ?? null;
|
|
77551
|
+
}
|
|
77552
|
+
if (field !== null) {
|
|
77553
|
+
const other = boundBy.get(field.alias);
|
|
77554
|
+
if (other !== void 0) {
|
|
77555
|
+
findings.push({ path: `${path14}.slots.${name2}`, message: `"${field.alias}" already fills ${other} \u2014 a field fills one slot` });
|
|
77556
|
+
continue;
|
|
77557
|
+
}
|
|
77558
|
+
boundBy.set(field.alias, name2);
|
|
77559
|
+
}
|
|
77560
|
+
slots.push({ name: name2, role: spec.role, field });
|
|
77561
|
+
}
|
|
77562
|
+
let tabs = null;
|
|
77563
|
+
if (screen.tabs === void 0) {
|
|
77564
|
+
tabs = tabsRule === "lifecycle" ? entity.fields.find((field) => roleOfField(field) === "lifecycle") ?? null : null;
|
|
77565
|
+
} else if (screen.tabs !== null) {
|
|
77566
|
+
const field = fieldByAlias.get(screen.tabs);
|
|
77567
|
+
if (field === void 0) {
|
|
77568
|
+
findings.push({ path: `${path14}.tabs`, message: `names "${screen.tabs}", which is not a field of entity "${entity.alias}"` });
|
|
77569
|
+
} else if (field.type !== "select") {
|
|
77570
|
+
findings.push({ path: `${path14}.tabs`, message: `names "${screen.tabs}", a ${field.type} \u2014 a tab strip is a select field's options` });
|
|
77571
|
+
} else {
|
|
77572
|
+
tabs = field;
|
|
77573
|
+
}
|
|
77574
|
+
}
|
|
77575
|
+
if (findings.length > 0) return { type: "invalid", findings };
|
|
77576
|
+
return { type: "resolved", screen: { app, screen, entity, shapeLabel, record: record2, tabs, slots } };
|
|
77577
|
+
}
|
|
77578
|
+
function validateWorkspacePlan(model, roles, apps) {
|
|
77579
|
+
const findings = [];
|
|
77580
|
+
for (const dup of findDuplicates(apps.map((app) => app.alias))) {
|
|
77581
|
+
findings.push({ path: "apps", message: `duplicate app alias "${dup}"` });
|
|
77582
|
+
}
|
|
77583
|
+
for (const dup of findDuplicates(apps.map((app) => app.name))) {
|
|
77584
|
+
findings.push({ path: "apps", message: `duplicate app name "${dup}"` });
|
|
77585
|
+
}
|
|
77586
|
+
const entityByAlias = new Map(model.entities.map((entity) => [entity.alias, entity]));
|
|
77587
|
+
for (const app of apps) {
|
|
77588
|
+
for (const dup of findDuplicates(app.screens.map((screen) => screen.alias))) {
|
|
77589
|
+
findings.push({ path: `apps.${app.alias}.screens`, message: `duplicate screen alias "${dup}"` });
|
|
77590
|
+
}
|
|
77591
|
+
for (const dup of findDuplicates(app.screens.map((screen) => screen.label))) {
|
|
77592
|
+
findings.push({ path: `apps.${app.alias}.screens`, message: `duplicate screen label "${dup}"` });
|
|
77593
|
+
}
|
|
77594
|
+
for (const screen of app.screens) {
|
|
77595
|
+
const resolved = resolveScreen(app, screen, entityByAlias, roles);
|
|
77596
|
+
if (resolved.type === "invalid") findings.push(...resolved.findings);
|
|
77597
|
+
}
|
|
77598
|
+
}
|
|
77599
|
+
return findings;
|
|
77600
|
+
}
|
|
77601
|
+
function roleCoverage(model, roles, rows) {
|
|
77602
|
+
const notes = [];
|
|
77603
|
+
for (const entity of model.entities) {
|
|
77604
|
+
const entityRows = rows[entity.alias];
|
|
77605
|
+
if (entityRows === void 0 || entityRows.length === 0) continue;
|
|
77606
|
+
const fieldByAlias = new Map(entity.fields.map((field) => [field.alias, field]));
|
|
77607
|
+
for (const field of entity.fields) {
|
|
77608
|
+
const decl = roleOf(roles, entity.alias, field.alias);
|
|
77609
|
+
if (decl === void 0) continue;
|
|
77610
|
+
const path14 = `rows.${entity.alias}.${field.alias}`;
|
|
77611
|
+
const values3 = entityRows.map((row) => row.fields[field.alias]);
|
|
77612
|
+
switch (decl.role) {
|
|
77613
|
+
case "lifecycle":
|
|
77614
|
+
case "expected_set": {
|
|
77615
|
+
if (field.type !== "select") break;
|
|
77616
|
+
const used = new Set(values3.flatMap((value2) => Array.isArray(value2) ? value2 : [value2]));
|
|
77617
|
+
const unused = field.options.filter((option) => !used.has(option.alias)).map((o) => o.label);
|
|
77618
|
+
if (unused.length === 0) break;
|
|
77619
|
+
notes.push({
|
|
77620
|
+
path: path14,
|
|
77621
|
+
message: decl.role === "lifecycle" ? `no row is in ${unused.length === 1 ? "stage" : "stages"} ${unused.join(", ")} \u2014 the desk shows ${unused.length === 1 ? "an empty band" : "empty bands"} there` : `no row has ${unused.join(", ")} \u2014 ${unused.length === 1 ? "that entry" : "those entries"} of the required set would show as ghosts on every subject`
|
|
77622
|
+
});
|
|
77623
|
+
break;
|
|
77624
|
+
}
|
|
77625
|
+
case "mark": {
|
|
77626
|
+
const carried = values3.some((value2) => Array.isArray(value2) ? value2.length > 0 : typeof value2 === "string");
|
|
77627
|
+
if (!carried) notes.push({ path: path14, message: `no row carries a picture \u2014 every register over it shows no mark` });
|
|
77628
|
+
break;
|
|
77629
|
+
}
|
|
77630
|
+
case "measure": {
|
|
77631
|
+
const { against, alert } = decl;
|
|
77632
|
+
if (field.type !== "number" || against === void 0 || alert === void 0) break;
|
|
77633
|
+
const read = entityRows.filter((row) => typeof row.fields[field.alias] === "number");
|
|
77634
|
+
if (read.length === 0) {
|
|
77635
|
+
notes.push({ path: path14, message: `no row carries a value \u2014 the meter has nothing to draw` });
|
|
77636
|
+
break;
|
|
77637
|
+
}
|
|
77638
|
+
const limitOf = (row) => {
|
|
77639
|
+
const raw = typeof against === "number" ? against : row.fields[against];
|
|
77640
|
+
return typeof raw === "number" ? raw : void 0;
|
|
77641
|
+
};
|
|
77642
|
+
const attention = read.filter((row) => {
|
|
77643
|
+
const limit = limitOf(row);
|
|
77644
|
+
const level = row.fields[field.alias];
|
|
77645
|
+
return limit !== void 0 && (alert === "over" ? level > limit : level < limit);
|
|
77646
|
+
});
|
|
77647
|
+
if (attention.length === 0) {
|
|
77648
|
+
const limitName = typeof against === "number" ? String(against) : fieldByAlias.get(against)?.label ?? against;
|
|
77649
|
+
notes.push({ path: path14, message: `no row is ${alert} ${limitName} \u2014 nothing on the screen needs attention` });
|
|
77650
|
+
}
|
|
77651
|
+
break;
|
|
77652
|
+
}
|
|
77653
|
+
default:
|
|
77654
|
+
break;
|
|
77655
|
+
}
|
|
77656
|
+
}
|
|
77657
|
+
}
|
|
77658
|
+
return notes;
|
|
77659
|
+
}
|
|
77660
|
+
|
|
76756
77661
|
// ../shared/src/schemas/workspace_model.ts
|
|
76757
77662
|
var workspaceModelContractSchema = packageContractSchema.omit({ apps: true, fixtures: true, knowledge: true, knowledge_expects: true }).extend({
|
|
76758
77663
|
entities: zod_default.array(contractEntitySchema).describe("The tables this model creates, with their fields, options and views"),
|
|
@@ -76765,6 +77670,8 @@ var modelApplyEntrySchema = zod_default.object({
|
|
|
76765
77670
|
}).strict();
|
|
76766
77671
|
var workspaceModelFileSchema = workspaceModelContractSchema.extend({
|
|
76767
77672
|
rows: contractRowsSchema.optional(),
|
|
77673
|
+
field_roles: fieldRolesSchema.optional(),
|
|
77674
|
+
apps: zod_default.array(modelAppSchema).optional(),
|
|
76768
77675
|
preset: modelPresetSchema.optional(),
|
|
76769
77676
|
apply: zod_default.array(modelApplyEntrySchema).optional()
|
|
76770
77677
|
}).strict();
|
|
@@ -76774,6 +77681,8 @@ var workspaceModelOverlaySchema = zod_default.object({
|
|
|
76774
77681
|
rename: packageBindSchema.optional().describe("Entity alias \u2192 what this business calls that table and its fields"),
|
|
76775
77682
|
entities: zod_default.array(contractEntitySchema).optional().describe("Tables this business has that the preset does not declare"),
|
|
76776
77683
|
rows: contractRowsSchema.optional(),
|
|
77684
|
+
field_roles: fieldRolesSchema.optional().describe("Roles on the preset's fields and this business's own, over whatever the preset declares"),
|
|
77685
|
+
apps: zod_default.array(modelAppSchema).optional(),
|
|
76777
77686
|
apply: zod_default.array(modelApplyEntrySchema).optional()
|
|
76778
77687
|
}).strict();
|
|
76779
77688
|
function parseModelFile(raw) {
|
|
@@ -76832,13 +77741,25 @@ function resolveModelFrom(preset, overlay) {
|
|
|
76832
77741
|
}))
|
|
76833
77742
|
};
|
|
76834
77743
|
}
|
|
77744
|
+
const field_roles = { ...preset.field_roles };
|
|
77745
|
+
for (const [entity, fields] of Object.entries(overlay.field_roles ?? {})) {
|
|
77746
|
+
const merged2 = { ...field_roles[entity] };
|
|
77747
|
+
for (const [field, decl] of Object.entries(fields)) {
|
|
77748
|
+
if (decl === null) delete merged2[field];
|
|
77749
|
+
else merged2[field] = decl;
|
|
77750
|
+
}
|
|
77751
|
+
if (Object.keys(merged2).length > 0) field_roles[entity] = merged2;
|
|
77752
|
+
else delete field_roles[entity];
|
|
77753
|
+
}
|
|
76835
77754
|
return {
|
|
76836
77755
|
type: "resolved",
|
|
76837
77756
|
file: {
|
|
76838
77757
|
entities: [...bound.contract.entities, ...overlay.entities ?? []],
|
|
76839
77758
|
roles: contract.roles,
|
|
76840
77759
|
templates: contract.templates,
|
|
77760
|
+
...Object.keys(field_roles).length > 0 ? { field_roles } : {},
|
|
76841
77761
|
...overlay.rows !== void 0 ? { rows: overlay.rows } : {},
|
|
77762
|
+
...overlay.apps !== void 0 ? { apps: overlay.apps } : {},
|
|
76842
77763
|
...overlay.apply !== void 0 ? { apply: overlay.apply } : {}
|
|
76843
77764
|
}
|
|
76844
77765
|
};
|
|
@@ -76848,9 +77769,29 @@ function toPackageContract(model) {
|
|
|
76848
77769
|
}
|
|
76849
77770
|
var ENTITIES_MAX = 50;
|
|
76850
77771
|
var MODEL_ROWS_MAX = 2e3;
|
|
77772
|
+
var MODEL_DOCUMENTS_MAX = 2e3;
|
|
76851
77773
|
function asList(value2) {
|
|
76852
77774
|
return Array.isArray(value2) ? value2 : [value2];
|
|
76853
77775
|
}
|
|
77776
|
+
function isStoredFileId(value2) {
|
|
77777
|
+
return /^fil_[A-Za-z0-9]+$/.test(value2);
|
|
77778
|
+
}
|
|
77779
|
+
function modelRowDocuments(model, rows) {
|
|
77780
|
+
const out = [];
|
|
77781
|
+
for (const entity of model.entities) {
|
|
77782
|
+
const filesFields = new Set(entity.fields.filter((field) => field.type === "files").map((field) => field.alias));
|
|
77783
|
+
for (const row of rows[entity.alias] ?? []) {
|
|
77784
|
+
for (const [field, value2] of Object.entries(row.fields)) {
|
|
77785
|
+
if (!filesFields.has(field)) continue;
|
|
77786
|
+
for (const raw of asList(value2)) {
|
|
77787
|
+
if (typeof raw !== "string") continue;
|
|
77788
|
+
out.push({ entity: entity.alias, ref: row.ref, field, value: raw, kind: isStoredFileId(raw) ? "file_id" : "path" });
|
|
77789
|
+
}
|
|
77790
|
+
}
|
|
77791
|
+
}
|
|
77792
|
+
}
|
|
77793
|
+
return out;
|
|
77794
|
+
}
|
|
76854
77795
|
function quote(value2) {
|
|
76855
77796
|
return typeof value2 === "string" ? `"${value2}"` : `a ${typeof value2}`;
|
|
76856
77797
|
}
|
|
@@ -76881,29 +77822,29 @@ function validateModelRows(model, rows) {
|
|
|
76881
77822
|
}
|
|
76882
77823
|
seenRefs.add(row.ref);
|
|
76883
77824
|
for (const [fieldAlias, value2] of Object.entries(row.fields)) {
|
|
76884
|
-
const
|
|
77825
|
+
const path14 = `rows.${alias}.${row.ref}.${fieldAlias}`;
|
|
76885
77826
|
const field = fieldByAlias.get(fieldAlias);
|
|
76886
77827
|
if (field === void 0) {
|
|
76887
77828
|
errors2.push({
|
|
76888
|
-
path:
|
|
77829
|
+
path: path14,
|
|
76889
77830
|
message: `is not a field of entity "${alias}" \u2014 a row's keys are field aliases (${fieldByAlias.size > 0 ? [...fieldByAlias.keys()].join(", ") : "this entity declares none"})`
|
|
76890
77831
|
});
|
|
76891
77832
|
continue;
|
|
76892
77833
|
}
|
|
76893
|
-
checkValue(field, value2,
|
|
77834
|
+
checkValue(field, value2, path14, refs, errors2);
|
|
76894
77835
|
}
|
|
76895
77836
|
}
|
|
76896
77837
|
}
|
|
76897
77838
|
return errors2;
|
|
76898
77839
|
}
|
|
76899
|
-
function checkValue(field, value2,
|
|
77840
|
+
function checkValue(field, value2, path14, refs, errors2) {
|
|
76900
77841
|
switch (field.type) {
|
|
76901
77842
|
case "select": {
|
|
76902
77843
|
const declared = new Set(field.options.map((option) => option.alias));
|
|
76903
77844
|
for (const raw of asList(value2)) {
|
|
76904
77845
|
if (typeof raw !== "string" || !declared.has(raw)) {
|
|
76905
77846
|
errors2.push({
|
|
76906
|
-
path:
|
|
77847
|
+
path: path14,
|
|
76907
77848
|
message: `${quote(raw)} is not an option of this field \u2014 use an option alias (${declared.size > 0 ? [...declared].join(", ") : "this field declares none"})`
|
|
76908
77849
|
});
|
|
76909
77850
|
}
|
|
@@ -76914,7 +77855,7 @@ function checkValue(field, value2, path13, refs, errors2) {
|
|
|
76914
77855
|
for (const raw of asList(value2)) {
|
|
76915
77856
|
if (typeof raw !== "string" || !refs.has(raw)) {
|
|
76916
77857
|
errors2.push({
|
|
76917
|
-
path:
|
|
77858
|
+
path: path14,
|
|
76918
77859
|
message: `${quote(raw)} names no row in this request \u2014 use "<entity-alias>:<ref>"`
|
|
76919
77860
|
});
|
|
76920
77861
|
continue;
|
|
@@ -76922,7 +77863,7 @@ function checkValue(field, value2, path13, refs, errors2) {
|
|
|
76922
77863
|
const named = raw.slice(0, raw.indexOf(":"));
|
|
76923
77864
|
if (named === field.target_entity) continue;
|
|
76924
77865
|
errors2.push({
|
|
76925
|
-
path:
|
|
77866
|
+
path: path14,
|
|
76926
77867
|
message: `names a row of entity "${named}", but this field links to "${field.target_entity}"`
|
|
76927
77868
|
});
|
|
76928
77869
|
}
|
|
@@ -76931,17 +77872,23 @@ function checkValue(field, value2, path13, refs, errors2) {
|
|
|
76931
77872
|
case "select_member": {
|
|
76932
77873
|
if (value2 !== "self") {
|
|
76933
77874
|
errors2.push({
|
|
76934
|
-
path:
|
|
77875
|
+
path: path14,
|
|
76935
77876
|
message: `${quote(value2)} is not a member this model can name \u2014 use "self"`
|
|
76936
77877
|
});
|
|
76937
77878
|
}
|
|
76938
77879
|
return;
|
|
76939
77880
|
}
|
|
76940
77881
|
case "files": {
|
|
76941
|
-
|
|
76942
|
-
|
|
76943
|
-
|
|
76944
|
-
|
|
77882
|
+
for (const raw of asList(value2)) {
|
|
77883
|
+
if (typeof raw !== "string" || raw === "") {
|
|
77884
|
+
errors2.push({ path: path14, message: `${quote(raw)} is not a document \u2014 a path beside this file, or a fil_ id already uploaded here` });
|
|
77885
|
+
continue;
|
|
77886
|
+
}
|
|
77887
|
+
if (isStoredFileId(raw)) continue;
|
|
77888
|
+
if (raw.startsWith("/") || raw.split(/[\\/]/).includes("..")) {
|
|
77889
|
+
errors2.push({ path: path14, message: `"${raw}" must be a relative path beside this file, with no ".."` });
|
|
77890
|
+
}
|
|
77891
|
+
}
|
|
76945
77892
|
return;
|
|
76946
77893
|
}
|
|
76947
77894
|
case "formula":
|
|
@@ -76949,7 +77896,7 @@ function checkValue(field, value2, path13, refs, errors2) {
|
|
|
76949
77896
|
case "lookup":
|
|
76950
77897
|
case "autonumber": {
|
|
76951
77898
|
errors2.push({
|
|
76952
|
-
path:
|
|
77899
|
+
path: path14,
|
|
76953
77900
|
message: `is a ${field.type} field, which the platform computes \u2014 drop the value`
|
|
76954
77901
|
});
|
|
76955
77902
|
return;
|
|
@@ -76957,13 +77904,13 @@ function checkValue(field, value2, path13, refs, errors2) {
|
|
|
76957
77904
|
default: {
|
|
76958
77905
|
if (!isFixtureScalarValue(value2)) {
|
|
76959
77906
|
errors2.push({
|
|
76960
|
-
path:
|
|
77907
|
+
path: path14,
|
|
76961
77908
|
message: `must be a string, number, boolean or null on a ${field.type} field`
|
|
76962
77909
|
});
|
|
76963
77910
|
return;
|
|
76964
77911
|
}
|
|
76965
77912
|
if (field.type === "date" && parseFixtureDateExpression(value2).kind === "invalid") {
|
|
76966
|
-
errors2.push({ path:
|
|
77913
|
+
errors2.push({ path: path14, message: `${quote(value2)} is not a date \u2014 use ${FIXTURE_DATE_GRAMMAR}` });
|
|
76967
77914
|
}
|
|
76968
77915
|
}
|
|
76969
77916
|
}
|
|
@@ -76997,6 +77944,13 @@ function validateWorkspaceModel(model, rows) {
|
|
|
76997
77944
|
message: `carries ${total} rows, above the ${MODEL_ROWS_MAX}-row cap for one model \u2014 a real data set belongs in an import`
|
|
76998
77945
|
});
|
|
76999
77946
|
}
|
|
77947
|
+
const documents = new Set(modelRowDocuments(model, rows).map((document) => document.value)).size;
|
|
77948
|
+
if (documents > MODEL_DOCUMENTS_MAX) {
|
|
77949
|
+
errors2.push({
|
|
77950
|
+
path: "rows",
|
|
77951
|
+
message: `attaches ${documents} documents, above the ${MODEL_DOCUMENTS_MAX}-document cap for one model`
|
|
77952
|
+
});
|
|
77953
|
+
}
|
|
77000
77954
|
errors2.push(...validateModelRows(model, rows));
|
|
77001
77955
|
return errors2;
|
|
77002
77956
|
}
|
|
@@ -77037,14 +77991,14 @@ function validateModelPreset(model, preset, rows) {
|
|
|
77037
77991
|
}
|
|
77038
77992
|
return errors2;
|
|
77039
77993
|
}
|
|
77040
|
-
function unknownModelKeys(raw, kept,
|
|
77994
|
+
function unknownModelKeys(raw, kept, path14 = "") {
|
|
77041
77995
|
if (Array.isArray(raw)) {
|
|
77042
77996
|
if (!Array.isArray(kept)) return [];
|
|
77043
|
-
return raw.flatMap((item, i2) => unknownModelKeys(item, kept[i2],
|
|
77997
|
+
return raw.flatMap((item, i2) => unknownModelKeys(item, kept[i2], path14 === "" ? String(i2) : `${path14}.${i2}`));
|
|
77044
77998
|
}
|
|
77045
77999
|
if (!isPlainObject2(raw) || !isPlainObject2(kept)) return [];
|
|
77046
78000
|
return Object.keys(raw).flatMap((key) => {
|
|
77047
|
-
const here =
|
|
78001
|
+
const here = path14 === "" ? key : `${path14}.${key}`;
|
|
77048
78002
|
if (!(key in kept)) return [{ path: here, message: "unknown key \u2014 not part of the model" }];
|
|
77049
78003
|
return unknownModelKeys(raw[key], kept[key], here);
|
|
77050
78004
|
});
|
|
@@ -77061,16 +78015,18 @@ function readPresetModel(raw) {
|
|
|
77061
78015
|
findings: [{ path: "from", message: "a preset IS the model it publishes, and this file names `from`" }]
|
|
77062
78016
|
};
|
|
77063
78017
|
}
|
|
77064
|
-
const { preset, rows, apply, ...model } = parsed.parsed.file;
|
|
78018
|
+
const { preset, rows, apps, apply, field_roles = {}, ...model } = parsed.parsed.file;
|
|
77065
78019
|
const shape = [
|
|
77066
78020
|
...preset === void 0 ? [{ path: "preset", message: "a preset file carries a `preset` block \u2014 name, description, questions, variants" }] : [],
|
|
77067
78021
|
...rows === void 0 ? [] : [{ path: "rows", message: "a preset carries no first rows \u2014 they belong to the model a reader writes" }],
|
|
78022
|
+
...apps === void 0 ? [] : [{ path: "apps", message: "a preset carries no screens \u2014 the apps are the model a reader writes" }],
|
|
77068
78023
|
...apply === void 0 ? [] : [{ path: "apply", message: "a preset carries no `apply` \u2014 the packages to copy in are that reader's to name" }]
|
|
77069
78024
|
];
|
|
77070
78025
|
if (preset === void 0 || shape.length > 0) return { type: "invalid", findings: shape };
|
|
77071
78026
|
const findings = [
|
|
77072
78027
|
...unknownModelKeys(raw, parsed.parsed.file),
|
|
77073
78028
|
...validateWorkspaceModel(model, {}),
|
|
78029
|
+
...checkFieldRoles(model.entities, field_roles),
|
|
77074
78030
|
...validateModelPreset(model, preset, {})
|
|
77075
78031
|
];
|
|
77076
78032
|
if (findings.length > 0) return { type: "invalid", findings };
|
|
@@ -77527,9 +78483,10 @@ Captured ${totalRows} row${totalRows === 1 ? "" : "s"} across ${result.captured.
|
|
|
77527
78483
|
|
|
77528
78484
|
// src/scaffold_commands.ts
|
|
77529
78485
|
import fs10 from "node:fs";
|
|
78486
|
+
import path11 from "node:path";
|
|
77530
78487
|
|
|
77531
78488
|
// src/model_reference.md
|
|
77532
|
-
var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing the tables, fields, options, views, roles and first rows\na workspace starts with. `lotics scaffold check model.json` proves it offline \u2014\nno account, no network. `lotics setup model.json --email you@company.com` creates\nthe account and applies it. `lotics scaffold apply model.json` applies it again,\ninto the workspace the credential names.\n\n**There are two forms of this file.** The full one, below, spells the model out.\nThe `from` one names a published preset and carries only what this business\ndiffers by \u2014 see \xA7 Starting from a preset, and prefer it whenever a preset fits\nthe trade.\n\nApps are not a model\'s to declare \u2014 build one in the workspace the model\ncreated, then publish that workspace as a starter.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not scaffolded \u2014 scaffold the rest in a second call.\n- **Adoption is explicit.** `lotics setup` REFUSES an entity whose `label`\n already names a table in the workspace, naming every colliding label at once.\n `lotics scaffold apply` adopts those tables and adds the fields, options and\n views they are missing. Nothing is ever modified or deleted, so applying the\n same model twice creates nothing the second time.\n- **Adoption is by LABEL, not alias.** Change an entity\'s `label` and the next\n run asks for a NEW table beside the old one. Renames and deletions go through\n `lotics run update_table` / `lotics run delete_table`, never through the file.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere, and the result says\n `rows_skipped: true`: sample rows landing among a customer\'s real ones cannot\n be told apart from them.\n- **After the first run the WORKSPACE is the source of truth.** The file is an\n authoring input, not a mirror \u2014 scaffold never deletes what the file stopped\n naming.\n- **`lotics scaffold check` decides all of it offline**, and reports every\n problem in one run rather than the first: an alias that resolves to nothing, a\n link whose pair is not symmetric, and the rows themselves \u2014 a field the entity\n does not declare, an option alias the field does not declare, a link naming no\n row in the file, a `ref` used twice, a date that is not one, and a value on a\n files or platform-computed field.\n\n## Top level\n\n```jsonc\n{\n "entities": [ /* the tables */ ],\n "roles": [ /* workspace groups to create */ ], // optional\n "templates":[ /* inline html / email templates */ ], // optional\n "rows": { /* first records, keyed by entity alias */ }, // optional\n "apply": [ /* published packages to copy in afterwards */ ], // optional\n "preset": { /* a trade\'s branches, for a PUBLISHED model */ } // optional\n}\n```\n\nThe other form names a preset instead of restating one:\n\n```jsonc\n{\n "from": "field_service", // the preset this model starts from, by slug\n "variants": ["crews"], // optional \u2014 its branches to merge in, in order\n "rename": { // optional \u2014 what THIS business calls each table\n "job": { "label": "\u0110\u01A1n h\xE0ng", "fields": { "code": "M\xE3 \u0111\u01A1n" } }\n },\n "entities": [ /* tables the preset does not declare */ ], // optional\n "rows": { /* first records, keyed by entity alias */ }, // optional\n "apply": [ /* published packages to copy in afterwards */ ] // optional\n}\n```\n\n**A model may not carry** `apps`, `fixtures`, `knowledge` or `knowledge_expects`,\nand no `excel` / `word` / `pdf-form` template: each of those is content that\nlives in a published bundle, which a model has none of. An unknown top-level key is\nan error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because scaffold matches by label.\n\n## Entity\n\n```jsonc\n{\n "alias": "order",\n "label": "Orders", // the table\'s name\n "description": "\u2026", // optional\n "fields": [ /* at least one */ ],\n "views": [ /* optional; an entity with none still gets the default grid */ ]\n}\n```\n\n## Field\n\nEvery field carries `alias`, `label`, an optional `description`, and an optional\n`required` \u2014 advisory only, read by app forms and workflows; the table itself has\nno required constraint. `label` may not contain `{` or `}` (formulas reference\nfields by label at the platform level).\n\n`default` is the value pre-filled into a NEW record. It applies on create only;\nexisting records are never backfilled. Only the types listed below accept one.\n\n### `text`\n\n```jsonc\n{ "alias": "name", "label": "Name", "type": "text",\n "unique": false, // optional \u2014 require distinct values\n "format": "text", // optional \u2014 "text" | "link" | "markdown"\n "default": "" } // optional\n```\n\n### `number`\n\n```jsonc\n{ "alias": "amount", "label": "Amount", "type": "number",\n "format": "currency", // optional \u2014 "number" | "currency" | "percentage"\n "currency": "VND", // optional \u2014 ISO 4217\n "default": 0 } // optional\n```\n\n### `date`\n\n```jsonc\n{ "alias": "placed_on", "label": "Placed on", "type": "date",\n "format": "date", // optional \u2014 "date" | "datetime" | "date_range" | "datetime_range"\n "timezone": "Asia/Ho_Chi_Minh", // optional \u2014 IANA name\n "derive_from": "created_at", // optional \u2014 "created_at" | "updated_at"; makes the field read-only\n "default": "2026-01-01" } // optional; refused together with derive_from\n```\n\n### `boolean`\n\n```jsonc\n{ "alias": "paid", "label": "Paid", "type": "boolean", "default": false }\n```\n\n### `select`\n\n```jsonc\n{ "alias": "tier", "label": "Tier", "type": "select",\n "options": [ // at least one\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "multi": false, // optional\n "default": ["standard"] } // optional \u2014 option ALIASES; one unless multi\n```\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet.\n\n```jsonc\n{ "alias": "owner", "label": "Owner", "type": "select_member", "multi": false }\n```\n\n### `select_record_link`\n\n```jsonc\n{ "alias": "customer", "label": "Customer", "type": "select_record_link",\n "target_entity": "customer", // an entity alias this model declares\n "cardinality": "one", // optional \u2014 "one" | "many" (default "many")\n "sync_both_ways": true, // optional \u2014 keep a paired field on the target\n "paired_field_alias": "orders", // the partner field ON THE TARGET entity\n "display_field_aliases": ["name"] } // optional \u2014 what the link shows / the picker\'s columns\n```\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused. Declare\none side only (with no `paired_field_alias`) for a link with no back-reference.\n\n### `files`\n\n```jsonc\n{ "alias": "attachments", "label": "Attachments", "type": "files" }\n```\n\n### `formula`\n\n```jsonc\n{ "alias": "total", "label": "Total", "type": "formula",\n "formula": {\n "expression": "{amount} * 1.1", // fields on THIS entity, by alias, in braces\n "format": "currency", // optional \u2014 "number" | "currency" | "percentage" | "link"\n "currency": "VND" // optional\n } }\n```\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n```jsonc\n{ "alias": "total_ordered", "label": "Total ordered", "type": "rollup",\n "source_field_alias": "orders", // a select_record_link field on THIS entity\n "aggregate_option": {\n "operation": "sum", // count | sum | avg | median | min | max | range |\n // empty | filled | percent_empty | percent_filled |\n // unique | percent_unique |\n // earliest | latest | date_range |\n // checked | unchecked | percent_checked |\n // percent_unchecked\n "field_key": "amount" // a field ALIAS on the linked entity ("count" may omit it)\n },\n "filter": { /* optional \u2014 see Views; every field_key is an alias on the LINKED entity */ } }\n```\n\nThe operation must be one the aggregated field\'s type allows \u2014 `sum` over a\nnumber, `earliest` over a date, `filled` over anything.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n```jsonc\n{ "alias": "customer_tier", "label": "Customer tier", "type": "lookup",\n "source_field_alias": "customer", // a select_record_link field on THIS entity\n "lookup_field_alias": "tier", // a field alias on the linked entity\n "order_by": { "field_key": "placed_on", "direction": "desc" } } // optional \u2014 pick the single extreme row\n```\n\n### `autonumber`\n\n```jsonc\n{ "alias": "seq", "label": "No.", "type": "autonumber",\n "prefix": "SO-", // optional \u2014 ignored when template is set\n "padding": 4, // optional \u2014 1..20, zero-pads the integer\n "template": "SO-{YEAR}-{N:4}" } // optional \u2014 {N}, {N:W}, {YEAR}, {YEAR:2}, {MONTH}, {DAY}\n```\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n```jsonc\n{\n "alias": "gold",\n "label": "Gold customers",\n "description": "\u2026", // optional\n "columns": [ // optional \u2014 omit to show every field\n { "field_alias": "name", "visibility": "visible", "width": 240 },\n { "field_alias": "tier", "visibility": "hidden" }\n ],\n "filters": { // optional\n "node_type": "group",\n "logic": "and", // "and" | "or"\n "children": [\n { "node_type": "condition", "type": "select", "field_key": "tier",\n "operator": "has_any_of", "value": ["gold"] }\n ]\n },\n "sort": [ { "field_key": "name", "order": "asc" } ], // optional; order is "asc" | "desc" | null\n "summary": { "amount": "sum" }, // optional \u2014 field alias \u2192 footer operation\n "frozen_columns": 1 // optional\n}\n```\n\nA condition\'s `type` is the field\'s type and its `operator` is one that type\nadmits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n`columns`, when present, is exhaustive and must not be empty: a view renders\nexactly the entries it holds. Omit the key to show every field.\n\n## Roles\n\nA role becomes a workspace group. Members are added afterwards, in the app.\n\n```jsonc\n{ "alias": "sales", "label": "Sales" }\n```\n\n## Templates\n\nOnly inline `html` and `email` templates \u2014 the rest are file-backed and a model\nhas no bytes.\n\n```jsonc\n{ "alias": "order_ack", "label": "Order acknowledgement", "type": "email",\n "content": "<p>Hello {{customer}}\u2026</p>" }\n```\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model \u2014 a real data set belongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n`ref` is a local handle (lowercase letters, digits, underscores) that other rows\'\nlink fields address. It is never persisted.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | not allowed |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Applying packages\n\n`apply` copies published packages into the workspace AFTER the model\'s own\ntables exist \u2014 apps over the tables you just described, and any tables of their\nown they still need. Ordered, and run by `lotics setup` and `lotics scaffold\napply` alike.\n\n```jsonc\n"apply": [\n {\n "package": "apg_k3nf82ldpq",\n "bind": { // optional \u2014 which of YOUR tables each entity is\n "company": { "label": "Customers", "fields": { "name": "Company name" } }\n },\n "no_sample_data": true // optional\n }\n]\n```\n\n`bind` is keyed by the package\'s entity alias and holds the LABELS this\nworkspace uses: scaffold adopts by label, so binding points the package at the\ntables the model created instead of a second set beside them. Only naming\nmoves \u2014 a bound field must be the TYPE the package declares, or the copy is\nrefused. `lotics library list` is the shelf, and `lotics library show <apg_id>`\nlists the aliases to bind.\n\nEntries run in the order they are written, because a later one may bind onto a\ntable an earlier one created. **A refused entry stops the run and the entries\nbefore it stay** \u2014 they are separate copies, committed as they land, so the\nrefusal names them rather than leaving a caller to re-run the file and copy them\ntwice.\n\n## Presets\n\nA preset is a trade\'s model, published to be READ. An assistant reads it, asks\nat most two questions, picks a variant and writes a `model.json` from it \u2014\nnothing is copied, and a preset is a file rather than anything a workspace\ninstalls.\n\n```jsonc\n"preset": {\n "name": "Field service",\n "description": "Jobs, the crew that runs them, and what each one billed.",\n "questions": ["Do you dispatch crews, or one person per job?"], // at most 2\n "variants": {\n "crews": {\n "when": "work is dispatched to crews rather than to one person",\n "entities": [ /* tables this branch ADDS */ ],\n "fields": { "job": [ /* fields this branch ADDS to `job` */ ] }\n }\n }\n}\n```\n\nVariants are **additive only**: a branch adds entities and fields and never\nremoves them, so the base is a model in its own right rather than a draft.\n`lotics scaffold check` proves the base AND every variant merged onto it, so a\npreset ships with every branch already proven \u2014 the branch nobody took is the\none that fails in the workspace of whoever takes it.\n\n`preset` is not scaffolded. `lotics setup` and `lotics scaffold apply` ignore\nit and create the base model\'s tables.\n\n`lotics scaffold export` prints a workspace that already works as one of these\nfiles \u2014 the starting point for a preset or for another business\'s model, never a\nsource of truth: it carries one business\'s words and stops describing that\nworkspace the moment either changes.\n\n## Starting from a preset\n\n`lotics library list` is the shelf of them and `lotics library show <slug>`\nprints one whole: its questions, every table as `alias \xB7 label` with each field\nas `alias:type`, and each variant as `slug \xB7 when` followed by the tables and\nfields that branch adds. When one of them is the trade in front of you, do not\ntranscribe it \u2014 name it:\n\n```jsonc\n{\n "from": "field_service",\n "variants": ["crews"],\n "rename": { "job": { "label": "\u0110\u01A1n h\xE0ng", "fields": { "code": "M\xE3 \u0111\u01A1n" } } },\n "entities": [ /* a table this business has that the preset does not */ ],\n "rows": { "job": [ { "ref": "j1", "fields": { "code": "J-1" } } ] }\n}\n```\n\n- **`from`** is the preset\'s SLUG \u2014 its own file name, a lowercase slug. Naming\n it is what makes `entities` optional; every other rule on this page is\n unchanged, because the file is resolved into the full form and then checked and\n applied exactly as one. A slug nothing serves is refused with the ones there\n are, never resolved against something else.\n- **`variants`** names the branches to merge onto the base, in order. Pick the\n one whose `when` describes what the person said; a slug the preset does not\n declare is refused rather than ignored.\n- **`rename`** is keyed by the preset\'s entity alias and holds the labels this\n business uses \u2014 the same shape `apply[].bind` takes, and the same rule: only\n naming moves. An alias the preset does not declare, and a label that is\n already another table\'s, are both refused.\n- **`entities`** are added after the rename, already in this business\'s own\n words.\n- **`rows`** and **`apply`** mean exactly what they mean in the full form \u2014\n `"rows"` are this business\'s real first records, `"apply"` the packages copied\n in once its tables exist.\n\nThis is the ONE thing on this page that needs the network: `check` reads the\npreset it names, once. Everything after that read is the same offline check.\n\nWrite the full form when no preset is the trade.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "format": "currency",\n "currency": "VND"\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "format": "currency", "currency": "VND" }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\n`lotics scaffold check` on this file reports\n`2 tables, 9 fields, 2 links, 1 view, 1 role, 4 rows`.\n';
|
|
78489
|
+
var model_reference_default = '# The Lotics workspace model (`model.json`)\n\nOne JSON file describing the tables, fields, options, views, roles and first rows\na workspace starts with. `lotics scaffold check model.json` proves it offline \u2014\nno account, no network. `lotics setup model.json --email you@company.com` creates\nthe account and applies it. `lotics scaffold apply model.json` applies it again,\ninto the workspace the credential names.\n\n**There are two forms of this file.** The full one, below, spells the model out.\nThe `from` one names a published preset and carries only what this business\ndiffers by \u2014 see \xA7 Starting from a preset, and prefer it whenever a preset fits\nthe trade.\n\nApps are PLANNED here and built afterwards: `apps` names each app\'s screens as a\nshape over an entity, checked against the roles `field_roles` gives its fields,\nso the plan is refused before anyone builds a screen (\xA7 Apps and screens). The\nbuilt app lives in the workspace; publishing that workspace as a package is how\nit ships.\n\n## The rules\n\n- **At least one entity, at most 50.** More tables than that is a data model\n being designed, not scaffolded \u2014 scaffold the rest in a second call.\n- **Adoption is explicit.** `lotics setup` REFUSES an entity whose `label`\n already names a table in the workspace, naming every colliding label at once.\n `lotics scaffold apply` adopts those tables and adds the fields, options and\n views they are missing. Nothing is ever modified or deleted, so applying the\n same model twice creates nothing the second time.\n- **Adoption is by LABEL, not alias.** Change an entity\'s `label` and the next\n run asks for a NEW table beside the old one. Renames and deletions go through\n `lotics run update_table` / `lotics run delete_table`, never through the file.\n- **Rows land only where every bound table is empty.** One table already holding\n records and no rows are written anywhere, and the result says\n `rows_skipped: true`: sample rows landing among a customer\'s real ones cannot\n be told apart from them.\n- **After the first run the WORKSPACE is the source of truth.** The file is an\n authoring input, not a mirror \u2014 scaffold never deletes what the file stopped\n naming.\n- **`lotics scaffold check` decides all of it offline**, and reports every\n problem in one run rather than the first: an alias that resolves to nothing, a\n link whose pair is not symmetric, and the rows themselves \u2014 a field the entity\n does not declare, an option alias the field does not declare, a link naming no\n row in the file, a `ref` used twice, a date that is not one, a value on a\n platform-computed field, and a files cell that is neither a relative path\n beside this file nor a `fil_` id.\n\n## Top level\n\n```jsonc\n{\n "entities": [ /* the tables */ ],\n "roles": [ /* workspace groups to create */ ], // optional\n "templates":[ /* inline html / email templates */ ], // optional\n "rows": { /* first records, keyed by entity alias */ }, // optional\n "field_roles": { /* the reporting role each field plays, keyed by entity then field */ }, // optional\n "apps": [ /* the screens each app will have, as shapes over entities */ ], // optional\n "apply": [ /* published packages to copy in afterwards */ ], // optional\n "preset": { /* a trade\'s branches, for a PUBLISHED model */ } // optional\n}\n```\n\nThe other form names a preset instead of restating one:\n\n```jsonc\n{\n "from": "field_service", // the preset this model starts from, by slug\n "variants": ["crews"], // optional \u2014 its branches to merge in, in order\n "rename": { // optional \u2014 what THIS business calls each table\n "job": { "label": "\u0110\u01A1n h\xE0ng", "fields": { "code": "M\xE3 \u0111\u01A1n" } }\n },\n "entities": [ /* tables the preset does not declare */ ], // optional\n "rows": { /* first records, keyed by entity alias */ }, // optional\n "field_roles": { /* roles on the preset\'s fields and this business\'s own */ }, // optional\n "apps": [ /* the screens each app will have */ ], // optional\n "apply": [ /* published packages to copy in afterwards */ ] // optional\n}\n```\n\n**A model may not carry** `fixtures`, `knowledge` or `knowledge_expects`, and no\n`excel` / `word` / `pdf-form` template: each of those is content that lives in a\npublished bundle, which a model has none of. `apps` here is a plan of screens,\nnever built code. An unknown top-level key is an error, never ignored.\n\n### Aliases\n\nEvery `alias` is a lowercase slug \u2014 a letter, then letters, digits and\nunderscores (`unit_price`, `so_1001`). Aliases are how the file cross-references\nitself; they are never shown to anyone. `label` is what a person sees.\n\nLabels must be unique within their namespace \u2014 two entities, two fields on one\nentity, two options on one field, two views on one entity, two roles or two\ntemplates cannot share a label, because scaffold matches by label.\n\n## Entity\n\n```jsonc\n{\n "alias": "order",\n "label": "Orders", // the table\'s name\n "description": "\u2026", // optional\n "fields": [ /* at least one */ ],\n "views": [ /* optional; an entity with none still gets the default grid */ ]\n}\n```\n\n## Field\n\nEvery field carries `alias`, `label`, an optional `description`, and an optional\n`required` \u2014 advisory only, read by app forms and workflows; the table itself has\nno required constraint. `label` may not contain `{` or `}` (formulas reference\nfields by label at the platform level).\n\n`default` is the value pre-filled into a NEW record. It applies on create only;\nexisting records are never backfilled. Only the types listed below accept one.\n\n### `text`\n\n```jsonc\n{ "alias": "name", "label": "Name", "type": "text",\n "unique": false, // optional \u2014 require distinct values\n "format": "text", // optional \u2014 "text" | "link" | "markdown"\n "default": "" } // optional\n```\n\n### `number`\n\n```jsonc\n{ "alias": "amount", "label": "Amount", "type": "number",\n "format": "currency", // optional \u2014 "number" | "currency" | "percentage"\n "currency": "VND", // optional \u2014 ISO 4217\n "default": 0 } // optional\n```\n\n### `date`\n\n```jsonc\n{ "alias": "placed_on", "label": "Placed on", "type": "date",\n "format": "date", // optional \u2014 "date" | "datetime" | "date_range" | "datetime_range"\n "timezone": "Asia/Ho_Chi_Minh", // optional \u2014 IANA name\n "derive_from": "created_at", // optional \u2014 "created_at" | "updated_at"; makes the field read-only\n "default": "2026-01-01" } // optional; refused together with derive_from\n```\n\n### `boolean`\n\n```jsonc\n{ "alias": "paid", "label": "Paid", "type": "boolean", "default": false }\n```\n\n### `select`\n\n```jsonc\n{ "alias": "tier", "label": "Tier", "type": "select",\n "options": [ // at least one\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "multi": false, // optional\n "default": ["standard"] } // optional \u2014 option ALIASES; one unless multi\n```\n\n`color` is one of: `red`, `orange`, `amber`, `yellow`, `lime`, `green`,\n`emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`,\n`fuchsia`, `pink`, `rose`, `slate`, `gray`, `zinc`, `neutral`, `stone`.\n\n### `select_member`\n\nA person picker over the workspace\'s members. No default: a model cannot name\nmembers of a workspace that does not exist yet.\n\n```jsonc\n{ "alias": "owner", "label": "Owner", "type": "select_member", "multi": false }\n```\n\n### `select_record_link`\n\n```jsonc\n{ "alias": "customer", "label": "Customer", "type": "select_record_link",\n "target_entity": "customer", // an entity alias this model declares\n "cardinality": "one", // optional \u2014 "one" | "many" (default "many")\n "sync_both_ways": true, // optional \u2014 keep a paired field on the target\n "paired_field_alias": "orders", // the partner field ON THE TARGET entity\n "display_field_aliases": ["name"] } // optional \u2014 what the link shows / the picker\'s columns\n```\n\nA two-way link is declared on BOTH sides, each naming the other as its\n`paired_field_alias`; the pair must be symmetric or the model is refused. Declare\none side only (with no `paired_field_alias`) for a link with no back-reference.\n\n### `files`\n\n```jsonc\n{ "alias": "attachments", "label": "Attachments", "type": "files" }\n```\n\n### `formula`\n\n```jsonc\n{ "alias": "total", "label": "Total", "type": "formula",\n "formula": {\n "expression": "{amount} * 1.1", // fields on THIS entity, by alias, in braces\n "format": "currency", // optional \u2014 "number" | "currency" | "percentage" | "link"\n "currency": "VND" // optional\n } }\n```\n\n### `rollup`\n\nAggregates the records reached through a link on this entity.\n\n```jsonc\n{ "alias": "total_ordered", "label": "Total ordered", "type": "rollup",\n "source_field_alias": "orders", // a select_record_link field on THIS entity\n "aggregate_option": {\n "operation": "sum", // count | sum | avg | median | min | max | range |\n // empty | filled | percent_empty | percent_filled |\n // unique | percent_unique |\n // earliest | latest | date_range |\n // checked | unchecked | percent_checked |\n // percent_unchecked\n "field_key": "amount" // a field ALIAS on the linked entity ("count" may omit it)\n },\n "filter": { /* optional \u2014 see Views; every field_key is an alias on the LINKED entity */ } }\n```\n\nThe operation must be one the aggregated field\'s type allows \u2014 `sum` over a\nnumber, `earliest` over a date, `filled` over anything.\n\n### `lookup`\n\nDisplays a field from the linked records.\n\n```jsonc\n{ "alias": "customer_tier", "label": "Customer tier", "type": "lookup",\n "source_field_alias": "customer", // a select_record_link field on THIS entity\n "lookup_field_alias": "tier", // a field alias on the linked entity\n "order_by": { "field_key": "placed_on", "direction": "desc" } } // optional \u2014 pick the single extreme row\n```\n\n### `autonumber`\n\n```jsonc\n{ "alias": "seq", "label": "No.", "type": "autonumber",\n "prefix": "SO-", // optional \u2014 ignored when template is set\n "padding": 4, // optional \u2014 1..20, zero-pads the integer\n "template": "SO-{YEAR}-{N:4}" } // optional \u2014 {N}, {N:W}, {YEAR}, {YEAR:2}, {MONTH}, {DAY}\n```\n\n## Views\n\nSaved views live under the entity they belong to. Every field reference is a\nfield ALIAS on that entity.\n\n```jsonc\n{\n "alias": "gold",\n "label": "Gold customers",\n "description": "\u2026", // optional\n "columns": [ // optional \u2014 omit to show every field\n { "field_alias": "name", "visibility": "visible", "width": 240 },\n { "field_alias": "tier", "visibility": "hidden" }\n ],\n "filters": { // optional\n "node_type": "group",\n "logic": "and", // "and" | "or"\n "children": [\n { "node_type": "condition", "type": "select", "field_key": "tier",\n "operator": "has_any_of", "value": ["gold"] }\n ]\n },\n "sort": [ { "field_key": "name", "order": "asc" } ], // optional; order is "asc" | "desc" | null\n "summary": { "amount": "sum" }, // optional \u2014 field alias \u2192 footer operation\n "frozen_columns": 1 // optional\n}\n```\n\nA condition\'s `type` is the field\'s type and its `operator` is one that type\nadmits \u2014 `has_any_of` / `has_none_of` / `has_all_of` / `is_empty` /\n`is_not_empty` for a select, `equals` / `greater_than` / `less_than` for a\nnumber, `on` / `before` / `after` / `between` for a date, `contains` /\n`is_any_of` for text. A select condition\'s `value` names option ALIASES.\n\n`columns`, when present, is exhaustive and must not be empty: a view renders\nexactly the entries it holds. Omit the key to show every field.\n\n## Roles\n\nA role becomes a workspace group. Members are added afterwards, in the app.\n\n```jsonc\n{ "alias": "sales", "label": "Sales" }\n```\n\n## Templates\n\nOnly inline `html` and `email` templates \u2014 the rest are file-backed and a model\nhas no bytes. An `html` template renders to a PDF when a workflow generates\nfrom it; `{{name}}` is filled from the workflow\'s data.\n\n```jsonc\n{ "alias": "order_ack", "label": "Order acknowledgement", "type": "email",\n "content": "<p>Hello {{customer}}\u2026</p>" }\n```\n\nA paper that has to look like a counterparty produced it \u2014 an official letter,\nan acceptance minute, a supplier\'s bill \u2014 is the same `html` template with a\nshell around the body: a letterhead, a reference line, a seal and a signature\nblock, and paper grain over everything. One shell, many bodies; the data is the\nonly thing that changes, so a workflow can re-issue it over any record.\n\n```jsonc\n{ "alias": "cong_van", "label": "C\xF4ng v\u0103n", "type": "html",\n "content": "\u2026the page below, as one JSON string\u2026" }\n```\n\n```html\n<style>\n .sheet{position:relative;width:718px;padding:44px 58px 30px;background:#fbfaf6;color:#111;font:14.2px/1.5 \'Liberation Serif\',serif}\n .grain{position:absolute;inset:0;opacity:.34;mix-blend-mode:multiply;background:url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'140\' height=\'140\'><filter id=\'f\'><feTurbulence baseFrequency=\'.9\' numOctaves=\'2\'/><feColorMatrix values=\'0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .35 0\'/></filter><rect width=\'140\' height=\'140\' filter=\'url(%23f)\'/></svg>")}\n .top{display:flex;text-align:center;font-size:13.4px} .top>div{flex:1} .u{display:inline-block;border-bottom:1px solid #111;font-weight:700}\n .ref{display:flex;text-align:center;font-size:13.4px;margin-top:6px} .ref>div{flex:1} .ref .r{font-style:italic}\n h1{text-align:center;font-size:15.6px;margin:26px 0 18px} p{text-align:justify;text-indent:26px;margin:0 0 9px}\n .sig{display:flex;margin-top:20px} .sig .l{flex:1} .sig .r{width:290px;text-align:center;position:relative}\n .sig .nm{font-weight:700;margin-top:96px} .seal{position:absolute;left:4px;top:8px;width:166px;height:166px;opacity:.66;mix-blend-mode:multiply;transform:rotate(-17deg)}\n </style>\n <div class=\'sheet\'><div class=\'grain\'></div>\n <div class=\'top\'><div><b>{{issuer_parent}}</b><br><span class=\'u\'>{{issuer}}</span></div>\n <div><b>C\u1ED8NG H\xD2A X\xC3 H\u1ED8I CH\u1EE6 NGH\u0128A VI\u1EC6T NAM</b><br><span class=\'u\'>\u0110\u1ED9c l\u1EADp - T\u1EF1 do - H\u1EA1nh ph\xFAc</span></div></div>\n <div class=\'ref\'><div>S\u1ED1: {{number}}</div><div class=\'r\'>{{place}}, ng\xE0y {{day}} th\xE1ng {{month}} n\u0103m {{year}}</div></div>\n <h1>{{title}}</h1>\n <p>K\xEDnh g\u1EEDi: {{recipient}}.</p>\n {{{body}}}\n <div class=\'sig\'><div class=\'l\'><b>N\u01A1i nh\u1EADn:</b><br>- Nh\u01B0 tr\xEAn;<br>- L\u01B0u VT.</div>\n <div class=\'r\'><img class=\'seal\' src=\'{{seal_url}}\'><b>{{signer_title}}</b><div class=\'nm\'>{{signer}}</div></div></div>\n </div>\n```\n\n`lotics preview <file.html>` renders any such page to a PNG the way a demo\'s\nprops are made, sized to its content, so a paper can be looked at before it is\nput in a template.\n\n## Rows\n\nFirst records, keyed by entity alias. Up to 200 rows per entity and 2000 across\nthe model, attaching at most 2000 documents between them \u2014 a real data set\nbelongs in an import, not a model.\n\n```jsonc\n"rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } }\n ]\n}\n```\n\n`ref` is a local handle (lowercase letters, digits, underscores) that other rows\'\nlink fields address. It is never persisted.\n\nA `files` cell attaches documents: paths relative to this file (no `..`, never\nabsolute), which `check` proves exist and `apply` uploads into the workspace\nbefore any row is written \u2014 a paperwork business seeds its papers with its\nrows. The server accepts only `fil_` ids of files this workspace owns, which is\nwhat the upload leaves behind. After a run that wrote rows, `apply` writes the\nrecord ids beside the file (`<model>.last_run.json`): `delete_records` over\nthem is how a seeded set is reset, and applying again re-dates it.\n\n`fields` is keyed by field alias, and every value is read against the field\'s\nDECLARED type:\n\n| Field type | Value |\n|---|---|\n| `text` / `number` / `boolean` | the value itself |\n| `date` | `"2026-03-14"`, or a relative expression (below) |\n| `select` | the option ALIAS \u2014 `"gold"`, or `["gold","vip"]` for a multi-select |\n| `select_record_link` | `"<entity-alias>:<ref>"` naming another row in this file \u2014 `"customer:acme"`, or an array for several |\n| `select_member` | `"self"` only \u2014 the person applying the model |\n| `files` | paths beside this file \u2014 `["scans/pccc_letter.png"]` \u2014 uploaded by `apply`/`setup` before the rows are posted; or `fil_` ids of files already in this workspace |\n| `formula`, `rollup`, `lookup`, `autonumber` | not allowed \u2014 the platform writes these |\n\n### Relative dates\n\nA date cell holds a literal `YYYY-MM-DD`, or an expression relative to the day\nthe model is applied, so a screen that opens on "this month" is not empty a month\nlater:\n\n- `@today` \u2014 the day of the run, in the workspace\'s timezone\n- `@month-start` \u2014 the 1st of that month\n- either with a whole-day offset: `@today-14`, `@month-start+9`\n\n`@month-start` exists because `@today-N` cannot promise a month: applied on the\n2nd, `@today-3` lands in the previous one.\n\n## Field roles\n\n`field_roles` names the reporting role a field plays on its entity \u2014 keyed by\nentity alias, then field alias \u2014 so every screen over the entity agrees on\nwhich column names the row and which select is the stage. A shape\'s slot binds\nto it (\xA7 Apps and screens). Like `rows` and `apps`, it is this file\'s: `check`\nproves it and the workspace never sees it. Each role sits on the types that can\nanswer it:\n\n| Role | On | Meaning |\n|---|---|---|\n| `identity` | `text`, `autonumber`, `select_record_link` | names the row \u2014 the register\'s first column; a link where the row is "the product, at this branch". One per entity |\n| `mark` | `files` | the row\'s picture. One per entity |\n| `lifecycle` | single `select` | the ordered stages a row walks; option order is the order. One per entity |\n| `measure` | `number`, `formula`, `rollup` | a level read against a limit \u2014 see `against` and `alert` |\n| `expected_set` | `select` | its OPTIONS are the required set (documents, checks, services); an option no row has is a gap to show, not nothing |\n| `amount` | `number`, `formula`, `rollup` | THE signed money of a ledger row. One per entity |\n| `when` | `date` | the ledger or timeline date. One per entity |\n| `party` | `select_record_link` | the counterparty. One per entity |\n| `contact` | `text` | the one way to reach a party. One per entity |\n| `verdict` | `boolean`, `formula` | a settled pass/fail \u2014 ticked, or computed. One per entity |\n\nA bare role name is the common form. A `measure` takes the object form to name\nits limit: `against` \u2014 a `number` field on the same entity, by alias, or a\nconstant \u2014 and `alert`, which side of it needs attention, `over` a capacity or\n`under` a minimum. The two come together.\n\n```jsonc\n"field_roles": {\n "san_pham": { "ten": "identity", "anh": "mark" },\n "ton_kho": { "ton": { "role": "measure", "against": "ton_toi_thieu", "alert": "under" },\n "hieu_suat": { "role": "measure", "against": 80, "alert": "under" } }\n}\n```\n\nIn a file that starts from a preset (\xA7 Starting from a preset), `field_roles`\nmay name the preset\'s fields as well as this business\'s own; a role the preset\ndeclares itself is kept unless this file names the same field, and `null`\nclears it.\n\n## Apps and screens\n\n`apps` is the plan: each app the reader will build, and each of its screens as\na SHAPE over an ENTITY. Nothing here is built by the scaffold \u2014 the plan is what\n`lotics scaffold check` prints back, screen by screen with the field in every\nslot, so it is read and corrected before a screen exists.\n\n```jsonc\n"apps": [\n {\n "alias": "kinh_doanh", "name": "Kinh doanh",\n "description": "\u2026", "icon": "briefcase", "theme": { "color": "blue" }, // optional\n "screens": [\n { "alias": "khach_hang", "label": "Kh\xE1ch h\xE0ng", "shape": "party_register", "entity": "customer" },\n { "alias": "don_hang", "label": "\u0110\u01A1n h\xE0ng", "shape": "lifecycle_desk", "entity": "order",\n "record": "drawer", // optional \u2014 "drawer" | "page"; absent, the shape decides\n "tabs": "stage", // optional \u2014 a select on the entity, or null; absent, the shape decides\n "slots": { "identity": "code" } } // optional \u2014 slot \u2192 field, where the roles cannot decide alone\n ]\n }\n]\n```\n\nA shape is a proven screen with named SLOTS, each filled by a field carrying a\nrole (\xA7 Field roles). A slot with exactly one candidate on the entity binds by itself;\ntwo candidates need naming in `slots`; a field fills one slot; a required slot\nwith none is refused \u2014 a lifecycle desk over an entity with no `lifecycle`\nselect cannot be built.\n\n| Shape | Answers | Required | Also fills | Record | Tabs |\n|---|---|---|---|---|---|\n| `lifecycle_desk` | what is stuck, what do I move next | `lifecycle`, `identity` | `party`, `amount`, `when` | drawer | the lifecycle\'s stages |\n| `party_register` | who is this, our history, is there a risk | `identity` | `mark`, `contact`, `measure` (worth), `verdict` (risk) | page | none |\n| `offering_register` | what do we offer, at what price, can I sell it | `identity` | `mark`, `amount` (price), `measure` (availability) | page | none |\n| `transaction_ledger` | does this period reconcile, what is unexplained | `when`, `amount` | `party`, `expected_set` (document) | drawer | none |\n| `monitored_asset_set` | what needs attention, is that number normal | `identity`, `measure` (level) | `lifecycle` | drawer | none |\n| `trend_deep_dive` | how did the period go, and why | `when` | `measure`, `amount` | drawer | none |\n\n`"shape": "custom"` is a screen of its own shape: it declares its slots under\n`roles` (slot \u2192 role) and they bind the same way.\n\n```jsonc\n{ "alias": "bang_do", "label": "B\u1EA3ng \u0111o", "shape": "custom", "entity": "reading",\n "roles": { "subject": "identity", "reading": "measure" } }\n```\n\n## Applying packages\n\n`apply` copies published packages into the workspace AFTER the model\'s own\ntables exist \u2014 apps over the tables you just described, and any tables of their\nown they still need. Ordered, and run by `lotics setup` and `lotics scaffold\napply` alike.\n\n```jsonc\n"apply": [\n {\n "package": "apg_k3nf82ldpq",\n "bind": { // optional \u2014 which of YOUR tables each entity is\n "company": { "label": "Customers", "fields": { "name": "Company name" } }\n },\n "no_sample_data": true // optional\n }\n]\n```\n\n`bind` is keyed by the package\'s entity alias and holds the LABELS this\nworkspace uses: scaffold adopts by label, so binding points the package at the\ntables the model created instead of a second set beside them. Only naming\nmoves \u2014 a bound field must be the TYPE the package declares, or the copy is\nrefused. `lotics library list` is the shelf, and `lotics library show <apg_id>`\nlists the aliases to bind.\n\nEntries run in the order they are written, because a later one may bind onto a\ntable an earlier one created. **A refused entry stops the run and the entries\nbefore it stay** \u2014 they are separate copies, committed as they land, so the\nrefusal names them rather than leaving a caller to re-run the file and copy them\ntwice.\n\n## Presets\n\nA preset is a trade\'s model, published to be READ. An assistant reads it, asks\nat most two questions, picks a variant and writes a `model.json` from it \u2014\nnothing is copied, and a preset is a file rather than anything a workspace\ninstalls.\n\n```jsonc\n"preset": {\n "name": "Field service",\n "description": "Jobs, the crew that runs them, and what each one billed.",\n "questions": ["Do you dispatch crews, or one person per job?"], // at most 2\n "variants": {\n "crews": {\n "when": "work is dispatched to crews rather than to one person",\n "entities": [ /* tables this branch ADDS */ ],\n "fields": { "job": [ /* fields this branch ADDS to `job` */ ] }\n }\n }\n}\n```\n\nVariants are **additive only**: a branch adds entities and fields and never\nremoves them, so the base is a model in its own right rather than a draft.\n`lotics scaffold check` proves the base AND every variant merged onto it, so a\npreset ships with every branch already proven \u2014 the branch nobody took is the\none that fails in the workspace of whoever takes it.\n\n`preset` is not scaffolded. `lotics setup` and `lotics scaffold apply` ignore\nit and create the base model\'s tables.\n\n`lotics scaffold export` prints a workspace that already works as one of these\nfiles \u2014 the starting point for a preset or for another business\'s model, never a\nsource of truth: it carries one business\'s words and stops describing that\nworkspace the moment either changes.\n\n## Starting from a preset\n\n`lotics library list` is the shelf of them and `lotics library show <slug>`\nprints one whole: its questions, every table as `alias \xB7 label` with each field\nas `alias:type`, and each variant as `slug \xB7 when` followed by the tables and\nfields that branch adds. When one of them is the trade in front of you, do not\ntranscribe it \u2014 name it:\n\n```jsonc\n{\n "from": "field_service",\n "variants": ["crews"],\n "rename": { "job": { "label": "\u0110\u01A1n h\xE0ng", "fields": { "code": "M\xE3 \u0111\u01A1n" } } },\n "entities": [ /* a table this business has that the preset does not */ ],\n "rows": { "job": [ { "ref": "j1", "fields": { "code": "J-1" } } ] },\n "field_roles": { "job": { "code": "identity" } },\n "apps": [ /* the screens this business\'s apps will have */ ]\n}\n```\n\n- **`from`** is the preset\'s SLUG \u2014 its own file name, a lowercase slug. Naming\n it is what makes `entities` optional; every other rule on this page is\n unchanged, because the file is resolved into the full form and then checked and\n applied exactly as one. A slug nothing serves is refused with the ones there\n are, never resolved against something else.\n- **`variants`** names the branches to merge onto the base, in order. Pick the\n one whose `when` describes what the person said; a slug the preset does not\n declare is refused rather than ignored.\n- **`rename`** is keyed by the preset\'s entity alias and holds the labels this\n business uses \u2014 the same shape `apply[].bind` takes, and the same rule: only\n naming moves. An alias the preset does not declare, and a label that is\n already another table\'s, are both refused.\n- **`entities`** are added after the rename, already in this business\'s own\n words.\n- **`rows`**, **`field_roles`**, **`apps`** and **`apply`** mean exactly what\n they mean in the full form \u2014 `"rows"` are this business\'s real first records,\n `"field_roles"` may name the preset\'s fields as well as its own (a role the\n preset declares itself is kept unless this file names the same field, or\n clears it with `null`),\n `"apps"` the screens it will have (a preset carries none), `"apply"` the\n packages copied in once its tables exist.\n\nThis is the ONE thing on this page that needs the network: `check` reads the\npreset it names, once. Everything after that read is the same offline check.\n\nWrite the full form when no preset is the trade.\n\n## A complete model\n\n```json\n{\n "entities": [\n {\n "alias": "customer",\n "label": "Customers",\n "fields": [\n { "alias": "name", "label": "Name", "type": "text", "required": true },\n {\n "alias": "tier",\n "label": "Tier",\n "type": "select",\n "options": [\n { "alias": "standard", "label": "Standard", "color": "slate" },\n { "alias": "gold", "label": "Gold", "color": "amber" }\n ],\n "default": ["standard"]\n },\n {\n "alias": "orders",\n "label": "Orders",\n "type": "select_record_link",\n "target_entity": "order",\n "cardinality": "many",\n "sync_both_ways": true,\n "paired_field_alias": "customer",\n "display_field_aliases": ["code"]\n },\n {\n "alias": "total_ordered",\n "label": "Total ordered",\n "type": "rollup",\n "source_field_alias": "orders",\n "aggregate_option": { "operation": "sum", "field_key": "amount" }\n }\n ],\n "views": [\n {\n "alias": "gold",\n "label": "Gold customers",\n "filters": {\n "node_type": "condition",\n "type": "select",\n "field_key": "tier",\n "operator": "has_any_of",\n "value": ["gold"]\n },\n "sort": [{ "field_key": "name", "order": "asc" }]\n }\n ]\n },\n {\n "alias": "order",\n "label": "Orders",\n "fields": [\n { "alias": "code", "label": "Order no.", "type": "text", "unique": true },\n { "alias": "placed_on", "label": "Placed on", "type": "date", "format": "date" },\n {\n "alias": "amount",\n "label": "Amount",\n "type": "number",\n "format": "currency",\n "currency": "VND"\n },\n {\n "alias": "total",\n "label": "Total with VAT",\n "type": "formula",\n "formula": { "expression": "{amount} * 1.1", "format": "currency", "currency": "VND" }\n },\n {\n "alias": "customer",\n "label": "Customer",\n "type": "select_record_link",\n "target_entity": "customer",\n "cardinality": "one",\n "sync_both_ways": true,\n "paired_field_alias": "orders",\n "display_field_aliases": ["name"]\n }\n ]\n }\n ],\n "roles": [{ "alias": "sales", "label": "Sales" }],\n "field_roles": {\n "customer": { "name": "identity" },\n "order": { "code": "identity", "placed_on": "when", "amount": "amount", "customer": "party" }\n },\n "apps": [\n {\n "alias": "sales",\n "name": "Sales",\n "screens": [\n { "alias": "customers", "label": "Customers", "shape": "party_register", "entity": "customer" },\n { "alias": "orders", "label": "Orders", "shape": "transaction_ledger", "entity": "order" }\n ]\n }\n ],\n "rows": {\n "customer": [\n { "ref": "acme", "fields": { "name": "Acme Trading", "tier": "gold" } },\n { "ref": "bluebird", "fields": { "name": "Bluebird Foods", "tier": "standard" } }\n ],\n "order": [\n {\n "ref": "so_1001",\n "fields": {\n "code": "SO-1001",\n "placed_on": "@month-start+2",\n "amount": 4200000,\n "customer": "customer:acme"\n }\n },\n {\n "ref": "so_1002",\n "fields": {\n "code": "SO-1002",\n "placed_on": "@today-3",\n "amount": 1150000,\n "customer": "customer:bluebird"\n }\n }\n ]\n }\n}\n```\n\n`lotics scaffold check` on this file reports\n`2 tables, 9 fields, 2 links, 1 view, 1 role, 4 rows, 1 app, 2 screens`, then\nthe plan:\n\n```\nSales\n Customers \u2014 party register over Customers (2 rows) \xB7 page \xB7 tabs: none\n identity Name \xB7 mark (none) \xB7 contact (none) \xB7 worth (none) \xB7 risk (none)\n Orders \u2014 transaction ledger over Orders (2 rows) \xB7 drawer \xB7 tabs: none\n when Placed on \xB7 amount Amount \xB7 party Customer \xB7 document (none)\n```\n\nEvery `(none)` is a slot no field fills \u2014 the picture a register has none of,\nthe contact nobody declared. Read it as the screen a person will see.\n';
|
|
77533
78490
|
|
|
77534
78491
|
// src/scaffold_commands.ts
|
|
77535
78492
|
function printModelReference() {
|
|
@@ -77538,10 +78495,10 @@ function printModelReference() {
|
|
|
77538
78495
|
function count(n, noun) {
|
|
77539
78496
|
return `${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
77540
78497
|
}
|
|
77541
|
-
function issuePath(
|
|
77542
|
-
return
|
|
78498
|
+
function issuePath(path14) {
|
|
78499
|
+
return path14.length === 0 ? "the file" : path14.map((segment) => String(segment)).join(".");
|
|
77543
78500
|
}
|
|
77544
|
-
function checkWorkspaceModel(raw) {
|
|
78501
|
+
function checkWorkspaceModel(raw, options = {}) {
|
|
77545
78502
|
const parsed = workspaceModelFileSchema.safeParse(raw);
|
|
77546
78503
|
if (!parsed.success) {
|
|
77547
78504
|
return {
|
|
@@ -77552,28 +78509,87 @@ function checkWorkspaceModel(raw) {
|
|
|
77552
78509
|
model: null
|
|
77553
78510
|
};
|
|
77554
78511
|
}
|
|
77555
|
-
const { rows = {}, apply = [], preset, ...contract } = parsed.data;
|
|
78512
|
+
const { rows = {}, field_roles = {}, apps = [], apply = [], preset, ...contract } = parsed.data;
|
|
77556
78513
|
const findings = [
|
|
77557
78514
|
// What the parse dropped is a mistake the author cannot otherwise see.
|
|
77558
78515
|
...unknownModelKeys(raw, parsed.data),
|
|
77559
78516
|
...validateWorkspaceModel(contract, rows),
|
|
78517
|
+
// The roles, and every screen against them — the CLI's own checks, because
|
|
78518
|
+
// neither a role nor the plan reaches the server.
|
|
78519
|
+
...checkFieldRoles(contract.entities, field_roles),
|
|
78520
|
+
...validateWorkspacePlan(contract, field_roles, apps),
|
|
77560
78521
|
// Every BRANCH of a preset, applied onto the base and checked as a model of
|
|
77561
78522
|
// its own — the branch nobody took is the one that fails in the workspace of
|
|
77562
78523
|
// whoever takes it, who is the one reader who cannot fix it.
|
|
77563
|
-
...preset === void 0 ? [] : validateModelPreset(contract, preset, rows)
|
|
78524
|
+
...preset === void 0 ? [] : validateModelPreset(contract, preset, rows),
|
|
78525
|
+
// A document a row names by path has to be beside the file, and the check
|
|
78526
|
+
// is what proves it: `apply` uploading a path that is not there would fail
|
|
78527
|
+
// after the tables exist, which is the outcome a model's author is least
|
|
78528
|
+
// able to recover from.
|
|
78529
|
+
...options.baseDir === void 0 ? [] : missingDocuments(contract, rows, options.baseDir)
|
|
77564
78530
|
];
|
|
77565
78531
|
if (findings.length > 0) return { findings, model: null };
|
|
77566
|
-
return { findings, model: { contract, rows, apply } };
|
|
78532
|
+
return { findings, model: { contract, rows, field_roles, apps, apply } };
|
|
78533
|
+
}
|
|
78534
|
+
function missingDocuments(contract, rows, baseDir) {
|
|
78535
|
+
return modelRowDocuments(contract, rows).filter((document) => document.kind === "path" && !fs10.existsSync(path11.resolve(baseDir, document.value))).map((document) => ({
|
|
78536
|
+
path: `rows.${document.entity}.${document.ref}.${document.field}`,
|
|
78537
|
+
message: `"${document.value}" is not a file beside this model (looked in ${baseDir})`
|
|
78538
|
+
}));
|
|
78539
|
+
}
|
|
78540
|
+
async function uploadModelDocuments(client, model, baseDir) {
|
|
78541
|
+
const documents = modelRowDocuments(model.contract, model.rows).filter((document) => document.kind === "path");
|
|
78542
|
+
if (documents.length === 0) return model.rows;
|
|
78543
|
+
const paths = [...new Set(documents.map((document) => document.value))];
|
|
78544
|
+
note(`Uploading ${count(paths.length, "document")} the rows attach\u2026`);
|
|
78545
|
+
const idByPath = /* @__PURE__ */ new Map();
|
|
78546
|
+
for (const relative2 of paths) {
|
|
78547
|
+
const uploaded = await client.uploadFiles([path11.resolve(baseDir, relative2)]);
|
|
78548
|
+
const error52 = uploaded.errors[0];
|
|
78549
|
+
if (error52 !== void 0) {
|
|
78550
|
+
throw new Error(`"${relative2}" could not be uploaded (${error52.error}), so no row was written.`);
|
|
78551
|
+
}
|
|
78552
|
+
const id = uploaded.files[0]?.id;
|
|
78553
|
+
if (id === void 0) throw new Error(`The upload returned no file for "${relative2}".`);
|
|
78554
|
+
idByPath.set(relative2, id);
|
|
78555
|
+
note(` uploaded ${relative2} ${id}`);
|
|
78556
|
+
}
|
|
78557
|
+
const filesFields = new Map(
|
|
78558
|
+
model.contract.entities.map((entity) => [
|
|
78559
|
+
entity.alias,
|
|
78560
|
+
new Set(entity.fields.filter((field) => field.type === "files").map((field) => field.alias))
|
|
78561
|
+
])
|
|
78562
|
+
);
|
|
78563
|
+
return Object.fromEntries(
|
|
78564
|
+
Object.entries(model.rows).map(([entityAlias, entityRows]) => [
|
|
78565
|
+
entityAlias,
|
|
78566
|
+
entityRows.map((row) => ({
|
|
78567
|
+
...row,
|
|
78568
|
+
fields: Object.fromEntries(
|
|
78569
|
+
Object.entries(row.fields).map(([fieldAlias, value2]) => [
|
|
78570
|
+
fieldAlias,
|
|
78571
|
+
filesFields.get(entityAlias)?.has(fieldAlias) ? (Array.isArray(value2) ? value2 : [value2]).map(
|
|
78572
|
+
(raw) => typeof raw === "string" ? idByPath.get(raw) ?? raw : raw
|
|
78573
|
+
) : value2
|
|
78574
|
+
])
|
|
78575
|
+
)
|
|
78576
|
+
}))
|
|
78577
|
+
])
|
|
78578
|
+
);
|
|
77567
78579
|
}
|
|
77568
78580
|
function countModel(model) {
|
|
77569
78581
|
const fields = model.contract.entities.flatMap((entity) => entity.fields);
|
|
78582
|
+
const screens = model.apps.flatMap((app) => app.screens);
|
|
77570
78583
|
return {
|
|
77571
78584
|
tables: model.contract.entities.length,
|
|
77572
78585
|
fields: fields.length,
|
|
77573
78586
|
links: fields.filter((field) => field.type === "select_record_link").length,
|
|
77574
78587
|
views: model.contract.entities.reduce((n, entity) => n + entity.views.length, 0),
|
|
77575
78588
|
roles: model.contract.roles.length,
|
|
77576
|
-
rows: Object.values(model.rows).reduce((n, entityRows) => n + entityRows.length, 0)
|
|
78589
|
+
rows: Object.values(model.rows).reduce((n, entityRows) => n + entityRows.length, 0),
|
|
78590
|
+
apps: model.apps.length,
|
|
78591
|
+
screens: screens.length,
|
|
78592
|
+
custom: screens.filter((screen) => screen.shape === CUSTOM_SHAPE).length
|
|
77577
78593
|
};
|
|
77578
78594
|
}
|
|
77579
78595
|
function summarizeModel(model) {
|
|
@@ -77584,13 +78600,65 @@ function summarizeModel(model) {
|
|
|
77584
78600
|
count(counts.links, "link"),
|
|
77585
78601
|
count(counts.views, "view"),
|
|
77586
78602
|
count(counts.roles, "role"),
|
|
77587
|
-
count(counts.rows, "row")
|
|
78603
|
+
count(counts.rows, "row"),
|
|
78604
|
+
...counts.apps > 0 ? [count(counts.apps, "app"), count(counts.screens, "screen")] : [],
|
|
78605
|
+
...counts.custom > 0 ? [`${counts.custom} custom`] : []
|
|
77588
78606
|
].join(", ");
|
|
77589
78607
|
}
|
|
78608
|
+
function resolvePlan(model) {
|
|
78609
|
+
const entityByAlias = new Map(model.contract.entities.map((entity) => [entity.alias, entity]));
|
|
78610
|
+
return model.apps.flatMap(
|
|
78611
|
+
(app) => app.screens.flatMap((screen) => {
|
|
78612
|
+
const resolved = resolveScreen(app, screen, entityByAlias, model.field_roles);
|
|
78613
|
+
if (resolved.type === "invalid") {
|
|
78614
|
+
throw new Error(`plan did not resolve: ${JSON.stringify(resolved.findings)}`);
|
|
78615
|
+
}
|
|
78616
|
+
return [resolved.screen];
|
|
78617
|
+
})
|
|
78618
|
+
);
|
|
78619
|
+
}
|
|
78620
|
+
function describePlan(model) {
|
|
78621
|
+
const lines = [];
|
|
78622
|
+
let lastApp;
|
|
78623
|
+
for (const resolved of resolvePlan(model)) {
|
|
78624
|
+
if (resolved.app.alias !== lastApp) {
|
|
78625
|
+
lines.push(resolved.app.name);
|
|
78626
|
+
lastApp = resolved.app.alias;
|
|
78627
|
+
}
|
|
78628
|
+
const rows = model.rows[resolved.entity.alias]?.length ?? 0;
|
|
78629
|
+
const tabs = resolved.tabs === null ? "none" : resolved.tabs.type === "select" ? `${resolved.tabs.label} (${resolved.tabs.options.map((option) => option.label).join(" \u2192 ")})` : resolved.tabs.label;
|
|
78630
|
+
lines.push(
|
|
78631
|
+
` ${resolved.screen.label} \u2014 ${resolved.shapeLabel} over ${resolved.entity.label} (${count(rows, "row")}) \xB7 ${resolved.record} \xB7 tabs: ${tabs}`
|
|
78632
|
+
);
|
|
78633
|
+
lines.push(
|
|
78634
|
+
` ${resolved.slots.map((entry) => `${entry.name} ${entry.field === null ? "(none)" : entry.field.label}`).join(" \xB7 ")}`
|
|
78635
|
+
);
|
|
78636
|
+
}
|
|
78637
|
+
return lines;
|
|
78638
|
+
}
|
|
78639
|
+
function describeCoverage(model) {
|
|
78640
|
+
return roleCoverage(model.contract, model.field_roles, model.rows).map((note2) => ` ${note2.path}: ${note2.message}`);
|
|
78641
|
+
}
|
|
78642
|
+
function planJson(model) {
|
|
78643
|
+
return model.apps.map((app) => ({
|
|
78644
|
+
alias: app.alias,
|
|
78645
|
+
name: app.name,
|
|
78646
|
+
screens: resolvePlan(model).filter((resolved) => resolved.app.alias === app.alias).map((resolved) => ({
|
|
78647
|
+
alias: resolved.screen.alias,
|
|
78648
|
+
label: resolved.screen.label,
|
|
78649
|
+
shape: resolved.screen.shape,
|
|
78650
|
+
entity: resolved.entity.alias,
|
|
78651
|
+
record: resolved.record,
|
|
78652
|
+
tabs: resolved.tabs === null ? null : resolved.tabs.alias,
|
|
78653
|
+
slots: Object.fromEntries(resolved.slots.map((entry) => [entry.name, entry.field?.alias ?? null]))
|
|
78654
|
+
}))
|
|
78655
|
+
}));
|
|
78656
|
+
}
|
|
77590
78657
|
var presetSourceSchema = zod_default.object({
|
|
77591
78658
|
entities: zod_default.array(contractEntitySchema),
|
|
77592
78659
|
roles: zod_default.array(contractRoleSchema),
|
|
77593
78660
|
templates: zod_default.array(contractInlineTemplateSchema),
|
|
78661
|
+
field_roles: fieldRolesSchema.default({}),
|
|
77594
78662
|
preset: modelPresetSchema
|
|
77595
78663
|
});
|
|
77596
78664
|
async function resolveOverlay(file2, raw, overlay) {
|
|
@@ -77622,7 +78690,7 @@ async function resolveOverlay(file2, raw, overlay) {
|
|
|
77622
78690
|
const { preset, ...contract } = source.data;
|
|
77623
78691
|
const resolved = resolveModelFrom({ ...contract, variants: preset.variants }, overlay);
|
|
77624
78692
|
if (resolved.type === "invalid") return { kind: "error", findings: resolved.findings };
|
|
77625
|
-
const { findings, model } = checkWorkspaceModel(resolved.file);
|
|
78693
|
+
const { findings, model } = checkWorkspaceModel(resolved.file, { baseDir: path11.dirname(path11.resolve(file2)) });
|
|
77626
78694
|
const dropped = unknownModelKeys(raw, overlay);
|
|
77627
78695
|
if (model === null || dropped.length > 0) {
|
|
77628
78696
|
return { kind: "error", findings: [...dropped, ...findings] };
|
|
@@ -77674,7 +78742,7 @@ async function checkModelFile(file2) {
|
|
|
77674
78742
|
if (parsed.parsed.form === "from") {
|
|
77675
78743
|
return await resolveOverlay(file2, raw, parsed.parsed.overlay);
|
|
77676
78744
|
}
|
|
77677
|
-
const { findings, model } = checkWorkspaceModel(raw);
|
|
78745
|
+
const { findings, model } = checkWorkspaceModel(raw, { baseDir: path11.dirname(path11.resolve(file2)) });
|
|
77678
78746
|
if (model === null) return { kind: "error", findings };
|
|
77679
78747
|
return { kind: "ok", model };
|
|
77680
78748
|
}
|
|
@@ -77748,7 +78816,8 @@ Applying ${entry.package}\u2026`);
|
|
|
77748
78816
|
async function scaffoldApply(client, model, options) {
|
|
77749
78817
|
const labels = new Map(model.contract.entities.map((entity) => [entity.alias, entity.label]));
|
|
77750
78818
|
note(`Applying the model \u2014 ${summarizeModel(model)}\u2026`);
|
|
77751
|
-
const
|
|
78819
|
+
const rows = await uploadModelDocuments(client, model, path11.dirname(path11.resolve(options.file)));
|
|
78820
|
+
const result = await postModel(client, { ...model, rows }, options);
|
|
77752
78821
|
const width = Math.max(...result.entities.map((entity) => entity.alias.length), 0);
|
|
77753
78822
|
for (const entity of result.entities) {
|
|
77754
78823
|
note(
|
|
@@ -77771,6 +78840,16 @@ No rows were written \u2014 a table this model bound already holds records${adop
|
|
|
77771
78840
|
the sample data, or add the rows yourself.`
|
|
77772
78841
|
);
|
|
77773
78842
|
}
|
|
78843
|
+
const written = Object.values(result.record_ids).reduce((n, ids) => n + ids.length, 0);
|
|
78844
|
+
if (written > 0) {
|
|
78845
|
+
const runFile = `${path11.resolve(options.file)}.last_run.json`;
|
|
78846
|
+
fs10.writeFileSync(
|
|
78847
|
+
runFile,
|
|
78848
|
+
`${JSON.stringify({ applied_at: (/* @__PURE__ */ new Date()).toISOString(), record_ids: result.record_ids }, null, 2)}
|
|
78849
|
+
`
|
|
78850
|
+
);
|
|
78851
|
+
note(` rows written to ${path11.basename(runFile)} \u2014 delete_records over them resets the set`);
|
|
78852
|
+
}
|
|
77774
78853
|
const applied = await applyModelPackages(client, model.apply);
|
|
77775
78854
|
const created = result.entities.filter((entity) => entity.created).length;
|
|
77776
78855
|
const apps = applied.flatMap((entry) => entry.apps);
|
|
@@ -77944,7 +79023,7 @@ import fs12 from "node:fs";
|
|
|
77944
79023
|
|
|
77945
79024
|
// src/file_command_io.ts
|
|
77946
79025
|
import fs11 from "node:fs";
|
|
77947
|
-
import
|
|
79026
|
+
import path12 from "node:path";
|
|
77948
79027
|
var CliError = class extends Error {
|
|
77949
79028
|
constructor(message2) {
|
|
77950
79029
|
super(message2);
|
|
@@ -77967,8 +79046,8 @@ function rejectExtraArgs(rest2, arity, usage) {
|
|
|
77967
79046
|
fail2(`${usage} takes ${arity} argument${arity === 1 ? "" : "s"} after <file>, got ${rest2.length}. Unused: ${rest2.slice(arity).join(" ")}`);
|
|
77968
79047
|
}
|
|
77969
79048
|
function writeFileAtomic(filePath, bytes) {
|
|
77970
|
-
const dir =
|
|
77971
|
-
const tmp =
|
|
79049
|
+
const dir = path12.dirname(path12.resolve(filePath));
|
|
79050
|
+
const tmp = path12.join(dir, `.${path12.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
77972
79051
|
fs11.writeFileSync(tmp, bytes);
|
|
77973
79052
|
try {
|
|
77974
79053
|
fs11.renameSync(tmp, filePath);
|
|
@@ -80771,16 +81850,16 @@ var MatcherView = class {
|
|
|
80771
81850
|
* @returns {string|undefined}
|
|
80772
81851
|
*/
|
|
80773
81852
|
getCurrentTag() {
|
|
80774
|
-
const
|
|
80775
|
-
return
|
|
81853
|
+
const path14 = this._matcher.path;
|
|
81854
|
+
return path14.length > 0 ? path14[path14.length - 1].tag : void 0;
|
|
80776
81855
|
}
|
|
80777
81856
|
/**
|
|
80778
81857
|
* Get current namespace.
|
|
80779
81858
|
* @returns {string|undefined}
|
|
80780
81859
|
*/
|
|
80781
81860
|
getCurrentNamespace() {
|
|
80782
|
-
const
|
|
80783
|
-
return
|
|
81861
|
+
const path14 = this._matcher.path;
|
|
81862
|
+
return path14.length > 0 ? path14[path14.length - 1].namespace : void 0;
|
|
80784
81863
|
}
|
|
80785
81864
|
/**
|
|
80786
81865
|
* Get current node's attribute value.
|
|
@@ -80788,9 +81867,9 @@ var MatcherView = class {
|
|
|
80788
81867
|
* @returns {*}
|
|
80789
81868
|
*/
|
|
80790
81869
|
getAttrValue(attrName) {
|
|
80791
|
-
const
|
|
80792
|
-
if (
|
|
80793
|
-
return
|
|
81870
|
+
const path14 = this._matcher.path;
|
|
81871
|
+
if (path14.length === 0) return void 0;
|
|
81872
|
+
return path14[path14.length - 1].values?.[attrName];
|
|
80794
81873
|
}
|
|
80795
81874
|
/**
|
|
80796
81875
|
* Check if current node has an attribute.
|
|
@@ -80798,9 +81877,9 @@ var MatcherView = class {
|
|
|
80798
81877
|
* @returns {boolean}
|
|
80799
81878
|
*/
|
|
80800
81879
|
hasAttr(attrName) {
|
|
80801
|
-
const
|
|
80802
|
-
if (
|
|
80803
|
-
const current =
|
|
81880
|
+
const path14 = this._matcher.path;
|
|
81881
|
+
if (path14.length === 0) return false;
|
|
81882
|
+
const current = path14[path14.length - 1];
|
|
80804
81883
|
return current.values !== void 0 && attrName in current.values;
|
|
80805
81884
|
}
|
|
80806
81885
|
/**
|
|
@@ -80826,18 +81905,18 @@ var MatcherView = class {
|
|
|
80826
81905
|
* @returns {number}
|
|
80827
81906
|
*/
|
|
80828
81907
|
getPosition() {
|
|
80829
|
-
const
|
|
80830
|
-
if (
|
|
80831
|
-
return
|
|
81908
|
+
const path14 = this._matcher.path;
|
|
81909
|
+
if (path14.length === 0) return -1;
|
|
81910
|
+
return path14[path14.length - 1].position ?? 0;
|
|
80832
81911
|
}
|
|
80833
81912
|
/**
|
|
80834
81913
|
* Get current node's repeat counter (occurrence count of this tag name).
|
|
80835
81914
|
* @returns {number}
|
|
80836
81915
|
*/
|
|
80837
81916
|
getCounter() {
|
|
80838
|
-
const
|
|
80839
|
-
if (
|
|
80840
|
-
return
|
|
81917
|
+
const path14 = this._matcher.path;
|
|
81918
|
+
if (path14.length === 0) return -1;
|
|
81919
|
+
return path14[path14.length - 1].counter ?? 0;
|
|
80841
81920
|
}
|
|
80842
81921
|
/**
|
|
80843
81922
|
* Get current node's sibling index (alias for getPosition).
|
|
@@ -86495,10 +87574,10 @@ function resolveRelativePath(basePath, relativePath) {
|
|
|
86495
87574
|
}
|
|
86496
87575
|
return resolved.join("/");
|
|
86497
87576
|
}
|
|
86498
|
-
function findZipEntry(zipEntries,
|
|
86499
|
-
if (zipEntries[
|
|
86500
|
-
if (zipEntries[`xl/${
|
|
86501
|
-
const noSlash =
|
|
87577
|
+
function findZipEntry(zipEntries, path14) {
|
|
87578
|
+
if (zipEntries[path14]) return zipEntries[path14];
|
|
87579
|
+
if (zipEntries[`xl/${path14}`]) return zipEntries[`xl/${path14}`];
|
|
87580
|
+
const noSlash = path14.replace(/^\//, "");
|
|
86502
87581
|
if (zipEntries[noSlash]) return zipEntries[noSlash];
|
|
86503
87582
|
return void 0;
|
|
86504
87583
|
}
|
|
@@ -88399,9 +89478,9 @@ function parseExcelFromZip(zip, options) {
|
|
|
88399
89478
|
}
|
|
88400
89479
|
return result;
|
|
88401
89480
|
}
|
|
88402
|
-
function findEntry(zip,
|
|
88403
|
-
if (zip[
|
|
88404
|
-
const lower2 =
|
|
89481
|
+
function findEntry(zip, path14) {
|
|
89482
|
+
if (zip[path14]) return zip[path14];
|
|
89483
|
+
const lower2 = path14.toLowerCase();
|
|
88405
89484
|
for (const key of Object.keys(zip)) {
|
|
88406
89485
|
if (key.toLowerCase() === lower2) return zip[key];
|
|
88407
89486
|
}
|
|
@@ -89038,12 +90117,12 @@ function extractPivotRoundTripInfo(originalZip) {
|
|
|
89038
90117
|
if (paths.length > 0) pivotTablesBySheetIndex.set(i2, paths);
|
|
89039
90118
|
}
|
|
89040
90119
|
const pivotXmlPaths = [];
|
|
89041
|
-
for (const
|
|
89042
|
-
if (
|
|
89043
|
-
pivotXmlPaths.push(
|
|
90120
|
+
for (const path14 of Object.keys(originalZip)) {
|
|
90121
|
+
if (path14.startsWith("xl/pivotTables/") && path14.endsWith(".xml")) {
|
|
90122
|
+
pivotXmlPaths.push(path14);
|
|
89044
90123
|
}
|
|
89045
|
-
if (
|
|
89046
|
-
pivotXmlPaths.push(
|
|
90124
|
+
if (path14.startsWith("xl/pivotCache/") && path14.endsWith(".xml")) {
|
|
90125
|
+
pivotXmlPaths.push(path14);
|
|
89047
90126
|
}
|
|
89048
90127
|
}
|
|
89049
90128
|
return { cachesByWorkbook, pivotTablesBySheetIndex, pivotXmlPaths };
|
|
@@ -89051,15 +90130,15 @@ function extractPivotRoundTripInfo(originalZip) {
|
|
|
89051
90130
|
function decode3(bytes) {
|
|
89052
90131
|
return new TextDecoder().decode(bytes);
|
|
89053
90132
|
}
|
|
89054
|
-
function pivotContentTypeFor(
|
|
89055
|
-
if (
|
|
89056
|
-
return `<Override PartName="/${
|
|
90133
|
+
function pivotContentTypeFor(path14) {
|
|
90134
|
+
if (path14.startsWith("xl/pivotTables/") && path14.endsWith(".xml")) {
|
|
90135
|
+
return `<Override PartName="/${path14}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml"/>`;
|
|
89057
90136
|
}
|
|
89058
|
-
if (
|
|
89059
|
-
return `<Override PartName="/${
|
|
90137
|
+
if (path14.includes("/pivotCacheDefinition") && path14.endsWith(".xml")) {
|
|
90138
|
+
return `<Override PartName="/${path14}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml"/>`;
|
|
89060
90139
|
}
|
|
89061
|
-
if (
|
|
89062
|
-
return `<Override PartName="/${
|
|
90140
|
+
if (path14.includes("/pivotCacheRecords") && path14.endsWith(".xml")) {
|
|
90141
|
+
return `<Override PartName="/${path14}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml"/>`;
|
|
89063
90142
|
}
|
|
89064
90143
|
return void 0;
|
|
89065
90144
|
}
|
|
@@ -89132,16 +90211,16 @@ function exportWorkbook(workbook, originalZip) {
|
|
|
89132
90211
|
regeneratedPaths.add(`xl/worksheets/sheet${i2 + 1}.xml`);
|
|
89133
90212
|
regeneratedPaths.add(`xl/worksheets/_rels/sheet${i2 + 1}.xml.rels`);
|
|
89134
90213
|
}
|
|
89135
|
-
for (const
|
|
89136
|
-
if (
|
|
89137
|
-
regeneratedPaths.add(
|
|
90214
|
+
for (const path14 of Object.keys(originalZip)) {
|
|
90215
|
+
if (path14.startsWith("xl/drawings/") || path14.startsWith("xl/charts/") || path14.startsWith("xl/tables/") || path14.startsWith("xl/media/")) {
|
|
90216
|
+
regeneratedPaths.add(path14);
|
|
89138
90217
|
}
|
|
89139
90218
|
}
|
|
89140
|
-
for (const [
|
|
89141
|
-
if (regeneratedPaths.has(
|
|
89142
|
-
const ext =
|
|
90219
|
+
for (const [path14, data2] of Object.entries(originalZip)) {
|
|
90220
|
+
if (regeneratedPaths.has(path14)) continue;
|
|
90221
|
+
const ext = path14.slice(path14.lastIndexOf(".") + 1).toLowerCase();
|
|
89143
90222
|
if (!DECLARED_EXTENSIONS.includes(ext)) continue;
|
|
89144
|
-
entries2[
|
|
90223
|
+
entries2[path14] = data2;
|
|
89145
90224
|
}
|
|
89146
90225
|
}
|
|
89147
90226
|
const sharedStrings = buildSharedStrings(workbook);
|
|
@@ -89157,8 +90236,8 @@ function exportWorkbook(workbook, originalZip) {
|
|
|
89157
90236
|
buildWorkbookRels(workbook, pivotInfo)
|
|
89158
90237
|
);
|
|
89159
90238
|
const extraContentTypes = [];
|
|
89160
|
-
for (const
|
|
89161
|
-
const ct = pivotContentTypeFor(
|
|
90239
|
+
for (const path14 of pivotInfo.pivotXmlPaths) {
|
|
90240
|
+
const ct = pivotContentTypeFor(path14);
|
|
89162
90241
|
if (ct) extraContentTypes.push(ct);
|
|
89163
90242
|
}
|
|
89164
90243
|
let globalChartIndex = 1;
|
|
@@ -89480,11 +90559,11 @@ function buildXfXml(s, fontId, fillId, borderId, numFmtId) {
|
|
|
89480
90559
|
if (s.horizontalAlign || s.verticalAlign || s.wrapText || s.indent || s.textRotation || s.shrinkToFit) {
|
|
89481
90560
|
const hAlign = s.horizontalAlign ? ` horizontal="${s.horizontalAlign}"` : "";
|
|
89482
90561
|
const vAlign = s.verticalAlign ? ` vertical="${s.verticalAlign}"` : "";
|
|
89483
|
-
const
|
|
90562
|
+
const wrap2 = s.wrapText ? ' wrapText="1"' : "";
|
|
89484
90563
|
const indent = s.indent ? ` indent="${s.indent}"` : "";
|
|
89485
90564
|
const rotation = s.textRotation !== void 0 ? ` textRotation="${s.textRotation === "vertical" ? 255 : s.textRotation}"` : "";
|
|
89486
90565
|
const shrink = s.shrinkToFit ? ' shrinkToFit="1"' : "";
|
|
89487
|
-
return `<xf ${attrs} applyAlignment="1"><alignment${hAlign}${vAlign}${
|
|
90566
|
+
return `<xf ${attrs} applyAlignment="1"><alignment${hAlign}${vAlign}${wrap2}${indent}${rotation}${shrink}/></xf>`;
|
|
89488
90567
|
}
|
|
89489
90568
|
return `<xf ${attrs}/>`;
|
|
89490
90569
|
}
|
|
@@ -91521,9 +92600,9 @@ function computeExpression(tokens) {
|
|
|
91521
92600
|
break;
|
|
91522
92601
|
}
|
|
91523
92602
|
}
|
|
91524
|
-
return
|
|
92603
|
+
return evaluate2(values3, operator);
|
|
91525
92604
|
}
|
|
91526
|
-
function
|
|
92605
|
+
function evaluate2(values3, operator) {
|
|
91527
92606
|
let result = false;
|
|
91528
92607
|
switch (operator) {
|
|
91529
92608
|
case ">":
|
|
@@ -98344,14 +99423,14 @@ var DependencyGraph = class {
|
|
|
98344
99423
|
stack.push({ cell: p, path: [cell, p] });
|
|
98345
99424
|
}
|
|
98346
99425
|
while (stack.length > 0) {
|
|
98347
|
-
const { cell: current, path:
|
|
98348
|
-
if (current === cell || originCoarse.has(current)) return
|
|
99426
|
+
const { cell: current, path: path14 } = stack.pop();
|
|
99427
|
+
if (current === cell || originCoarse.has(current)) return path14;
|
|
98349
99428
|
if (visited.has(current)) continue;
|
|
98350
99429
|
visited.add(current);
|
|
98351
99430
|
const nextPrec = this.precedents.get(current);
|
|
98352
99431
|
if (!nextPrec) continue;
|
|
98353
99432
|
for (const p of nextPrec) {
|
|
98354
|
-
stack.push({ cell: p, path: [...
|
|
99433
|
+
stack.push({ cell: p, path: [...path14, p] });
|
|
98355
99434
|
}
|
|
98356
99435
|
}
|
|
98357
99436
|
return null;
|
|
@@ -104869,8 +105948,8 @@ function buildParagraphElement(para, ctx, bookmarks) {
|
|
|
104869
105948
|
for (let i2 = 0; i2 <= runElements.length; i2++) slots.push([]);
|
|
104870
105949
|
if (bookmarks) {
|
|
104871
105950
|
for (const bm of bookmarks) {
|
|
104872
|
-
const
|
|
104873
|
-
slots[
|
|
105951
|
+
const slot2 = Math.max(0, Math.min(runElements.length, bm.beforeRun));
|
|
105952
|
+
slots[slot2].push(bm.element);
|
|
104874
105953
|
}
|
|
104875
105954
|
}
|
|
104876
105955
|
const children = [];
|
|
@@ -105310,8 +106389,8 @@ async function docModelToDocx(model) {
|
|
|
105310
106389
|
// ../ooxml/src/opc.ts
|
|
105311
106390
|
var import_jszip = __toESM(require_lib4(), 1);
|
|
105312
106391
|
function dropFolderEntries(zip) {
|
|
105313
|
-
for (const [
|
|
105314
|
-
if (entry.dir) delete zip.files[
|
|
106392
|
+
for (const [path14, entry] of Object.entries(zip.files)) {
|
|
106393
|
+
if (entry.dir) delete zip.files[path14];
|
|
105315
106394
|
}
|
|
105316
106395
|
}
|
|
105317
106396
|
|
|
@@ -105566,11 +106645,11 @@ async function parseDocx(buffer) {
|
|
|
105566
106645
|
const zip = await import_jszip3.default.loadAsync(buffer);
|
|
105567
106646
|
const parts = /* @__PURE__ */ new Map();
|
|
105568
106647
|
const filePromises = [];
|
|
105569
|
-
zip.forEach((
|
|
106648
|
+
zip.forEach((path14, file2) => {
|
|
105570
106649
|
if (file2.dir) return;
|
|
105571
106650
|
filePromises.push(
|
|
105572
106651
|
file2.async("uint8array").then((bytes) => {
|
|
105573
|
-
parts.set(
|
|
106652
|
+
parts.set(path14, bytes);
|
|
105574
106653
|
})
|
|
105575
106654
|
);
|
|
105576
106655
|
});
|
|
@@ -105802,9 +106881,9 @@ var DEFAULT_PARTS = [
|
|
|
105802
106881
|
["word/theme/theme1.xml", () => encoder.encode(THEME_XML)]
|
|
105803
106882
|
];
|
|
105804
106883
|
function fillDefaultParts(parts) {
|
|
105805
|
-
for (const [
|
|
105806
|
-
if (!parts.has(
|
|
105807
|
-
parts.set(
|
|
106884
|
+
for (const [path14, makeBytes] of DEFAULT_PARTS) {
|
|
106885
|
+
if (!parts.has(path14)) {
|
|
106886
|
+
parts.set(path14, makeBytes());
|
|
105808
106887
|
}
|
|
105809
106888
|
}
|
|
105810
106889
|
return parts;
|
|
@@ -105814,9 +106893,9 @@ function fillDefaultParts(parts) {
|
|
|
105814
106893
|
async function serializeDocx(doc) {
|
|
105815
106894
|
const zip = new import_jszip4.default();
|
|
105816
106895
|
const parts = fillDefaultParts(new Map(doc.parts));
|
|
105817
|
-
for (const [
|
|
105818
|
-
if (
|
|
105819
|
-
zip.file(
|
|
106896
|
+
for (const [path14, bytes] of parts) {
|
|
106897
|
+
if (path14 === "word/document.xml") continue;
|
|
106898
|
+
zip.file(path14, bytes);
|
|
105820
106899
|
}
|
|
105821
106900
|
const documentXml = serializeDocumentXml(doc);
|
|
105822
106901
|
zip.file("word/document.xml", documentXml);
|
|
@@ -106155,18 +107234,18 @@ function replaceInParagraph(p, search, replace2) {
|
|
|
106155
107234
|
}
|
|
106156
107235
|
if (matches.length === 0) continue;
|
|
106157
107236
|
count2 += matches.length;
|
|
106158
|
-
for (const
|
|
107237
|
+
for (const slot2 of segment) {
|
|
106159
107238
|
let out = "";
|
|
106160
|
-
let cursor =
|
|
107239
|
+
let cursor = slot2.start;
|
|
106161
107240
|
for (const m of matches) {
|
|
106162
|
-
if (m.end <=
|
|
106163
|
-
const keepUntil = Math.max(cursor, Math.min(m.start,
|
|
107241
|
+
if (m.end <= slot2.start || m.start >= slot2.end) continue;
|
|
107242
|
+
const keepUntil = Math.max(cursor, Math.min(m.start, slot2.end));
|
|
106164
107243
|
if (keepUntil > cursor) out += joined.slice(cursor, keepUntil);
|
|
106165
|
-
if (m.start >=
|
|
106166
|
-
cursor = Math.max(cursor, Math.min(m.end,
|
|
107244
|
+
if (m.start >= slot2.start && m.start < slot2.end) out += replace2;
|
|
107245
|
+
cursor = Math.max(cursor, Math.min(m.end, slot2.end));
|
|
106167
107246
|
}
|
|
106168
|
-
if (cursor <
|
|
106169
|
-
rewritten.set(`${
|
|
107247
|
+
if (cursor < slot2.end) out += joined.slice(cursor, slot2.end);
|
|
107248
|
+
rewritten.set(`${slot2.inlineIndex}:${slot2.childIndex}`, out);
|
|
106170
107249
|
}
|
|
106171
107250
|
}
|
|
106172
107251
|
if (count2 === 0) return [p, 0];
|
|
@@ -106608,167 +107687,113 @@ async function runKnowledgeCommand(client, subcommand, toolArgs, flags, restArgs
|
|
|
106608
107687
|
}
|
|
106609
107688
|
|
|
106610
107689
|
// src/preview.ts
|
|
106611
|
-
import { spawn as spawn3 } from "node:child_process";
|
|
106612
107690
|
import { createServer } from "node:http";
|
|
106613
|
-
import { readFileSync, writeFileSync, existsSync as
|
|
106614
|
-
import {
|
|
106615
|
-
import {
|
|
106616
|
-
import {
|
|
106617
|
-
|
|
106618
|
-
var HERE = dirname(fileURLToPath2(import.meta.url));
|
|
107691
|
+
import { readFileSync as readFileSync3, writeFileSync, existsSync as existsSync4, realpathSync, statSync } from "node:fs";
|
|
107692
|
+
import { join as join4, dirname as dirname2, resolve, extname, basename, isAbsolute, relative } from "node:path";
|
|
107693
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
107694
|
+
import { setTimeout as sleep4 } from "node:timers/promises";
|
|
107695
|
+
var HERE2 = dirname2(fileURLToPath3(import.meta.url));
|
|
106619
107696
|
function fail3(msg) {
|
|
106620
107697
|
console.error(msg);
|
|
106621
107698
|
process.exit(1);
|
|
106622
107699
|
}
|
|
106623
|
-
function findChrome() {
|
|
106624
|
-
const env = process.env.LOTICS_CHROME || process.env.CHROME_PATH;
|
|
106625
|
-
if (env && existsSync2(env)) return env;
|
|
106626
|
-
const home = process.env.HOME || "";
|
|
106627
|
-
const pwDir = process.platform === "darwin" ? `${home}/Library/Caches/ms-playwright` : `${home}/.cache/ms-playwright`;
|
|
106628
|
-
if (existsSync2(pwDir)) {
|
|
106629
|
-
const rel = process.platform === "darwin" ? ["chrome-mac/Chromium.app/Contents/MacOS/Chromium"] : ["chrome-linux/chrome", "chrome-linux/headless_shell"];
|
|
106630
|
-
const revs = readdirSync(pwDir).filter((n) => n.startsWith("chromium-") || n.startsWith("chromium_headless_shell-")).sort().reverse();
|
|
106631
|
-
for (const rev2 of revs) {
|
|
106632
|
-
for (const r of rel) {
|
|
106633
|
-
const bin = join2(pwDir, rev2, r);
|
|
106634
|
-
if (existsSync2(bin)) return bin;
|
|
106635
|
-
}
|
|
106636
|
-
}
|
|
106637
|
-
}
|
|
106638
|
-
const systemPaths = [
|
|
106639
|
-
"/usr/bin/google-chrome",
|
|
106640
|
-
"/usr/bin/google-chrome-stable",
|
|
106641
|
-
"/usr/bin/chromium",
|
|
106642
|
-
"/usr/bin/chromium-browser",
|
|
106643
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
106644
|
-
"/Applications/Chromium.app/Contents/MacOS/Chromium"
|
|
106645
|
-
];
|
|
106646
|
-
return systemPaths.find((p) => existsSync2(p)) ?? null;
|
|
106647
|
-
}
|
|
106648
|
-
async function cdpConnect(wsUrl) {
|
|
106649
|
-
const ws = new WebSocket(wsUrl);
|
|
106650
|
-
await new Promise((res, rej) => {
|
|
106651
|
-
ws.onopen = () => res();
|
|
106652
|
-
ws.onerror = () => rej(new Error("CDP websocket failed to open"));
|
|
106653
|
-
});
|
|
106654
|
-
let id = 0;
|
|
106655
|
-
const pending = /* @__PURE__ */ new Map();
|
|
106656
|
-
ws.onclose = () => {
|
|
106657
|
-
for (const done of pending.values()) done(Promise.reject(new Error("CDP connection closed (Chrome exited?)")));
|
|
106658
|
-
pending.clear();
|
|
106659
|
-
};
|
|
106660
|
-
ws.onmessage = (e) => {
|
|
106661
|
-
const m = JSON.parse(String(e.data));
|
|
106662
|
-
if (m.id != null && pending.has(m.id)) {
|
|
106663
|
-
const done = pending.get(m.id);
|
|
106664
|
-
pending.delete(m.id);
|
|
106665
|
-
done(m.error ? Promise.reject(new Error(m.error.message)) : m.result);
|
|
106666
|
-
}
|
|
106667
|
-
};
|
|
106668
|
-
const send = (method, params = {}) => new Promise((res) => {
|
|
106669
|
-
const i2 = ++id;
|
|
106670
|
-
pending.set(i2, (r) => res(r));
|
|
106671
|
-
ws.send(JSON.stringify({ id: i2, method, params }));
|
|
106672
|
-
});
|
|
106673
|
-
return { send, close: () => ws.close() };
|
|
106674
|
-
}
|
|
106675
|
-
function isStoredFileId(target) {
|
|
106676
|
-
return /^fil_[A-Za-z0-9]+$/.test(target);
|
|
106677
|
-
}
|
|
106678
107700
|
function defaultPreviewOutputPath(filename, cwd) {
|
|
106679
|
-
return
|
|
107701
|
+
return join4(cwd, basename(filename, extname(filename)) + ".png");
|
|
106680
107702
|
}
|
|
107703
|
+
var STATIC_TYPES = {
|
|
107704
|
+
".html": "text/html; charset=utf-8",
|
|
107705
|
+
".htm": "text/html; charset=utf-8",
|
|
107706
|
+
".css": "text/css",
|
|
107707
|
+
".js": "application/javascript",
|
|
107708
|
+
".png": "image/png",
|
|
107709
|
+
".jpg": "image/jpeg",
|
|
107710
|
+
".jpeg": "image/jpeg",
|
|
107711
|
+
".gif": "image/gif",
|
|
107712
|
+
".svg": "image/svg+xml",
|
|
107713
|
+
".webp": "image/webp",
|
|
107714
|
+
".woff": "font/woff",
|
|
107715
|
+
".woff2": "font/woff2",
|
|
107716
|
+
".ttf": "font/ttf"
|
|
107717
|
+
};
|
|
106681
107718
|
async function runPreviewCommand(filePath, flags) {
|
|
106682
|
-
if (!filePath) fail3("Usage: lotics preview <file.docx|.xlsx | fil_id> [-o <file.png>]");
|
|
107719
|
+
if (!filePath) fail3("Usage: lotics preview <file.docx|.xlsx|.html | fil_id> [-o <file.png>]");
|
|
106683
107720
|
const abs2 = resolve(filePath);
|
|
106684
|
-
if (!
|
|
107721
|
+
if (!existsSync4(abs2)) fail3(`File not found: ${abs2}`);
|
|
106685
107722
|
const ext = extname(abs2).toLowerCase();
|
|
106686
|
-
const type = ext === ".docx" ? "docx" : ext === ".xlsx" || ext === ".xls" || ext === ".csv" ? "xlsx" : null;
|
|
106687
|
-
if (!type) fail3(`Unsupported file type "${ext}" \u2014 preview supports .docx
|
|
106688
|
-
|
|
106689
|
-
|
|
106690
|
-
|
|
106691
|
-
|
|
106692
|
-
|
|
106693
|
-
|
|
106694
|
-
|
|
106695
|
-
|
|
106696
|
-
|
|
106697
|
-
|
|
107723
|
+
const type = ext === ".docx" ? "docx" : ext === ".xlsx" || ext === ".xls" || ext === ".csv" ? "xlsx" : ext === ".html" || ext === ".htm" ? "html" : null;
|
|
107724
|
+
if (!type) fail3(`Unsupported file type "${ext}" \u2014 preview supports .docx, .xlsx/.csv and .html. (PDFs open directly \u2014 no preview needed.)`);
|
|
107725
|
+
let server;
|
|
107726
|
+
let pageUrl;
|
|
107727
|
+
if (type === "html") {
|
|
107728
|
+
const dir = realpathSync(dirname2(abs2));
|
|
107729
|
+
server = createServer((req, res) => {
|
|
107730
|
+
let urlPath;
|
|
107731
|
+
try {
|
|
107732
|
+
urlPath = decodeURIComponent((req.url ?? "/").split("?")[0]);
|
|
107733
|
+
} catch {
|
|
107734
|
+
res.writeHead(400);
|
|
107735
|
+
res.end();
|
|
107736
|
+
return;
|
|
107737
|
+
}
|
|
107738
|
+
const target = resolve(dir, `.${urlPath}`);
|
|
107739
|
+
if (!existsSync4(target) || !statSync(target).isFile()) {
|
|
107740
|
+
res.writeHead(404);
|
|
107741
|
+
res.end();
|
|
107742
|
+
return;
|
|
107743
|
+
}
|
|
107744
|
+
const inside = relative(dir, realpathSync(target));
|
|
107745
|
+
if (inside.startsWith("..") || isAbsolute(inside)) {
|
|
107746
|
+
res.writeHead(404);
|
|
107747
|
+
res.end();
|
|
107748
|
+
return;
|
|
107749
|
+
}
|
|
107750
|
+
res.writeHead(200, { "content-type": STATIC_TYPES[extname(target).toLowerCase()] ?? "application/octet-stream" });
|
|
107751
|
+
res.end(readFileSync3(target));
|
|
107752
|
+
});
|
|
107753
|
+
pageUrl = (port) => `http://127.0.0.1:${port}/${encodeURIComponent(basename(abs2))}`;
|
|
107754
|
+
} else {
|
|
107755
|
+
const bundlePath = join4(HERE2, "..", "render_page.js");
|
|
107756
|
+
if (!existsSync4(bundlePath)) fail3(`Render bundle missing at ${bundlePath} \u2014 reinstall @lotics/cli (build step failed).`);
|
|
107757
|
+
const b64 = readFileSync3(abs2).toString("base64");
|
|
107758
|
+
const bundle = readFileSync3(bundlePath, "utf8");
|
|
107759
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;background:#fff;font-family:sans-serif}#root{padding:20px;max-width:1240px;margin:0 auto}</style></head><body><div id="root"></div><script>window.__LOTICS_RENDER=${JSON.stringify({ type, b64 })}</script><script src="/render_page.js"></script></body></html>`;
|
|
107760
|
+
server = createServer((req, res) => {
|
|
107761
|
+
if (req.url === "/render_page.js") {
|
|
107762
|
+
res.writeHead(200, { "content-type": "application/javascript" });
|
|
107763
|
+
res.end(bundle);
|
|
107764
|
+
} else {
|
|
107765
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
107766
|
+
res.end(html);
|
|
107767
|
+
}
|
|
107768
|
+
});
|
|
107769
|
+
pageUrl = (port) => `http://127.0.0.1:${port}/`;
|
|
106698
107770
|
}
|
|
106699
|
-
const b64 = readFileSync(abs2).toString("base64");
|
|
106700
|
-
const bundle = readFileSync(bundlePath, "utf8");
|
|
106701
|
-
const html = `<!doctype html><html><head><meta charset="utf-8"><style>body{margin:0;background:#fff;font-family:sans-serif}#root{padding:20px;max-width:1240px;margin:0 auto}</style></head><body><div id="root"></div><script>window.__LOTICS_RENDER=${JSON.stringify({ type, b64 })}</script><script src="/render_page.js"></script></body></html>`;
|
|
106702
|
-
const server = createServer((req, res) => {
|
|
106703
|
-
if (req.url === "/render_page.js") {
|
|
106704
|
-
res.writeHead(200, { "content-type": "application/javascript" });
|
|
106705
|
-
res.end(bundle);
|
|
106706
|
-
} else {
|
|
106707
|
-
res.writeHead(200, { "content-type": "text/html" });
|
|
106708
|
-
res.end(html);
|
|
106709
|
-
}
|
|
106710
|
-
});
|
|
106711
107771
|
await new Promise((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
106712
107772
|
const httpPort = server.address().port;
|
|
106713
|
-
|
|
106714
|
-
|
|
106715
|
-
|
|
106716
|
-
|
|
106717
|
-
|
|
106718
|
-
|
|
106719
|
-
|
|
106720
|
-
|
|
106721
|
-
"--disable-dev-shm-usage",
|
|
106722
|
-
"--force-device-scale-factor=2",
|
|
106723
|
-
"--remote-debugging-port=0",
|
|
106724
|
-
`--user-data-dir=${udd}`,
|
|
106725
|
-
"about:blank"
|
|
106726
|
-
], { stdio: "ignore" });
|
|
107773
|
+
let session;
|
|
107774
|
+
try {
|
|
107775
|
+
session = await launchChrome();
|
|
107776
|
+
} catch (error52) {
|
|
107777
|
+
server.close();
|
|
107778
|
+
fail3(error52 instanceof Error ? error52.message : String(error52));
|
|
107779
|
+
}
|
|
107780
|
+
const { cdp, chrome } = session;
|
|
106727
107781
|
const cleanup = () => {
|
|
106728
|
-
|
|
106729
|
-
child.kill();
|
|
106730
|
-
} catch {
|
|
106731
|
-
}
|
|
107782
|
+
session.close();
|
|
106732
107783
|
try {
|
|
106733
107784
|
server.close();
|
|
106734
107785
|
} catch {
|
|
106735
107786
|
}
|
|
106736
|
-
try {
|
|
106737
|
-
rmSync(udd, { recursive: true, force: true });
|
|
106738
|
-
} catch {
|
|
106739
|
-
}
|
|
106740
107787
|
};
|
|
106741
107788
|
try {
|
|
106742
|
-
|
|
106743
|
-
const
|
|
106744
|
-
for (let i2 = 0; i2 < 100 && !cdpPort; i2++) {
|
|
106745
|
-
if (existsSync2(portFile)) {
|
|
106746
|
-
const p = parseInt(readFileSync(portFile, "utf8").split("\n")[0], 10);
|
|
106747
|
-
if (p) cdpPort = p;
|
|
106748
|
-
}
|
|
106749
|
-
if (!cdpPort) await sleep2(100);
|
|
106750
|
-
}
|
|
106751
|
-
if (!cdpPort) throw new Error("Chrome did not expose a debugging port (launch failed?).");
|
|
106752
|
-
let target;
|
|
106753
|
-
for (let i2 = 0; i2 < 60 && !target?.webSocketDebuggerUrl; i2++) {
|
|
106754
|
-
try {
|
|
106755
|
-
const list2 = await (await fetch(`http://127.0.0.1:${cdpPort}/json/list`)).json();
|
|
106756
|
-
target = list2.find((t) => t.type === "page");
|
|
106757
|
-
} catch {
|
|
106758
|
-
}
|
|
106759
|
-
if (!target?.webSocketDebuggerUrl) await sleep2(100);
|
|
106760
|
-
}
|
|
106761
|
-
if (!target?.webSocketDebuggerUrl) throw new Error("No Chrome page target available.");
|
|
106762
|
-
const cdp = await cdpConnect(target.webSocketDebuggerUrl);
|
|
106763
|
-
await cdp.send("Page.enable");
|
|
106764
|
-
await cdp.send("Runtime.enable");
|
|
106765
|
-
await cdp.send("Page.navigate", { url: `http://127.0.0.1:${httpPort}/` });
|
|
107789
|
+
await cdp.send("Page.navigate", { url: pageUrl(httpPort) });
|
|
107790
|
+
const doneExpression = type === "html" ? "({done: document.readyState === 'complete' && Array.from(document.images).every((i) => i.complete), err: '', warnings: []})" : "({done: !!window.__loticsDone, err: window.__loticsError || '', warnings: window.__loticsWarnings || []})";
|
|
106766
107791
|
let err2;
|
|
106767
107792
|
let done = false;
|
|
106768
107793
|
const warnings = [];
|
|
106769
107794
|
for (let i2 = 0; i2 < 200; i2++) {
|
|
106770
107795
|
const r = await cdp.send("Runtime.evaluate", {
|
|
106771
|
-
expression:
|
|
107796
|
+
expression: doneExpression,
|
|
106772
107797
|
returnByValue: true
|
|
106773
107798
|
});
|
|
106774
107799
|
const v = r.result?.value;
|
|
@@ -106778,15 +107803,24 @@ async function runPreviewCommand(filePath, flags) {
|
|
|
106778
107803
|
if (v.warnings?.length) warnings.push(...v.warnings);
|
|
106779
107804
|
break;
|
|
106780
107805
|
}
|
|
106781
|
-
await
|
|
107806
|
+
await sleep4(75);
|
|
106782
107807
|
}
|
|
106783
107808
|
if (!done) throw new Error("Render timed out (page never signaled completion).");
|
|
106784
107809
|
if (err2) throw new Error(`Render engine error: ${err2}`);
|
|
106785
|
-
const
|
|
106786
|
-
|
|
107810
|
+
const measure = async () => {
|
|
107811
|
+
const metrics = await cdp.send("Page.getLayoutMetrics");
|
|
107812
|
+
const size2 = metrics.cssContentSize ?? metrics.contentSize ?? { width: 1240, height: 1600 };
|
|
107813
|
+
return { w: Math.ceil(size2.width), h: Math.ceil(size2.height) };
|
|
107814
|
+
};
|
|
107815
|
+
const first3 = await measure();
|
|
107816
|
+
await cdp.send("Emulation.setDeviceMetricsOverride", {
|
|
107817
|
+
width: Math.min(first3.w, 15e3),
|
|
107818
|
+
height: Math.min(first3.h, 15e3),
|
|
107819
|
+
deviceScaleFactor: 2,
|
|
107820
|
+
mobile: false
|
|
107821
|
+
});
|
|
107822
|
+
const { w, h } = await measure();
|
|
106787
107823
|
const MAX_CAPTURE = 15e3;
|
|
106788
|
-
const w = Math.ceil(size2.width);
|
|
106789
|
-
const h = Math.ceil(size2.height);
|
|
106790
107824
|
if (w > MAX_CAPTURE || h > MAX_CAPTURE) {
|
|
106791
107825
|
warnings.push(`content ${w}\xD7${h}px clipped to ${Math.min(w, MAX_CAPTURE)}\xD7${Math.min(h, MAX_CAPTURE)}px (exceeds the ${MAX_CAPTURE}px capture limit)`);
|
|
106792
107826
|
}
|
|
@@ -106795,8 +107829,7 @@ async function runPreviewCommand(filePath, flags) {
|
|
|
106795
107829
|
captureBeyondViewport: true,
|
|
106796
107830
|
clip: { x: 0, y: 0, width: Math.min(w, MAX_CAPTURE), height: Math.min(h, MAX_CAPTURE), scale: 1 }
|
|
106797
107831
|
});
|
|
106798
|
-
|
|
106799
|
-
const outPath = flags.output ? resolve(flags.output) : join2(dirname(abs2), basename(abs2, ext) + ".png");
|
|
107832
|
+
const outPath = flags.output ? resolve(flags.output) : join4(dirname2(abs2), basename(abs2, ext) + ".png");
|
|
106800
107833
|
writeFileSync(outPath, Buffer.from(shot.data, "base64"));
|
|
106801
107834
|
console.log(`Rendered ${basename(abs2)} \u2192 ${outPath} (${Math.min(w, MAX_CAPTURE)}\xD7${Math.min(h, MAX_CAPTURE)}, via ${basename(chrome)})`);
|
|
106802
107835
|
for (const warn2 of warnings) console.error(` \u26A0 ${warn2}`);
|
|
@@ -106958,8 +107991,8 @@ function prompt(question) {
|
|
|
106958
107991
|
});
|
|
106959
107992
|
});
|
|
106960
107993
|
}
|
|
106961
|
-
async function publicPost(
|
|
106962
|
-
const response = await fetch(`${API_BASE_URL}${
|
|
107994
|
+
async function publicPost(path14, body) {
|
|
107995
|
+
const response = await fetch(`${API_BASE_URL}${path14}`, {
|
|
106963
107996
|
method: "POST",
|
|
106964
107997
|
headers: { "Content-Type": "application/json" },
|
|
106965
107998
|
body: JSON.stringify(body)
|
|
@@ -107262,13 +108295,13 @@ async function resolveWorkspace(client, ctx) {
|
|
|
107262
108295
|
function resolveUploadPaths(rawPaths) {
|
|
107263
108296
|
const result = [];
|
|
107264
108297
|
for (const p of rawPaths) {
|
|
107265
|
-
const resolved =
|
|
108298
|
+
const resolved = path13.resolve(p);
|
|
107266
108299
|
const stat = fs15.statSync(resolved);
|
|
107267
108300
|
if (stat.isDirectory()) {
|
|
107268
108301
|
const entries2 = fs15.readdirSync(resolved, { withFileTypes: true });
|
|
107269
108302
|
for (const entry of entries2) {
|
|
107270
108303
|
if (entry.isFile()) {
|
|
107271
|
-
result.push(
|
|
108304
|
+
result.push(path13.join(resolved, entry.name));
|
|
107272
108305
|
}
|
|
107273
108306
|
}
|
|
107274
108307
|
} else {
|
|
@@ -107439,7 +108472,7 @@ async function main() {
|
|
|
107439
108472
|
if (!toolArgs) {
|
|
107440
108473
|
console.error(`Usage: lotics scaffold ${subcommand} <model.json>`);
|
|
107441
108474
|
console.error(
|
|
107442
|
-
subcommand === "check" ? ' Validates the model offline \u2014 no account, no network. Reports every\n problem in one run; exits 1 if there is one.\n A file written as {"from": "<preset_id>", \u2026} is the exception: it names a\n published preset, so checking it READS that preset and needs the network\n for that one step. Everything after it is the same offline check.\n --json prints {ok, tables, fields, links, views, roles, rows} or the findings.' : " Creates the model's tables, fields, options, views, roles and first rows\n in this workspace. Additive: an existing table of the same name is\n ADOPTED and given the fields, options and views it is missing, never\n modified \u2014 while `setup` refuses that name instead. Rows land only\n where every bound table is empty.\n --json prints one object and nothing else."
|
|
108475
|
+
subcommand === "check" ? ' Validates the model offline \u2014 no account, no network. Reports every\n problem in one run; exits 1 if there is one.\n A file written as {"from": "<preset_id>", \u2026} is the exception: it names a\n published preset, so checking it READS that preset and needs the network\n for that one step. Everything after it is the same offline check.\n Prints the counts, then every planned screen with the field in each of\n its slots, then what the first rows would show.\n --json prints {ok, tables, fields, links, views, roles, rows, apps, screens,\n plan, coverage} or the findings.' : " Creates the model's tables, fields, options, views, roles and first rows\n in this workspace. Additive: an existing table of the same name is\n ADOPTED and given the fields, options and views it is missing, never\n modified \u2014 while `setup` refuses that name instead. Rows land only\n where every bound table is empty.\n --json prints one object and nothing else."
|
|
107443
108476
|
);
|
|
107444
108477
|
console.error(" How to write one: lotics scaffold docs");
|
|
107445
108478
|
process.exit(1);
|
|
@@ -107447,13 +108480,30 @@ async function main() {
|
|
|
107447
108480
|
if (flags.json) setMachineOutput(true);
|
|
107448
108481
|
const checked = await checkModelFile(toolArgs);
|
|
107449
108482
|
if (checked.kind === "error") {
|
|
108483
|
+
noteScope({ plan_findings: checked.findings.length });
|
|
107450
108484
|
if (subcommand === "check" && flags.json) emitJson({ ok: false, findings: checked.findings });
|
|
107451
108485
|
else reportModelFindings(toolArgs, checked.findings);
|
|
107452
108486
|
process.exit(1);
|
|
107453
108487
|
}
|
|
108488
|
+
const counts = countModel(checked.model);
|
|
108489
|
+
noteScope({ plan_screens: counts.screens, plan_custom: counts.custom, plan_findings: 0 });
|
|
107454
108490
|
if (subcommand === "check") {
|
|
107455
|
-
|
|
107456
|
-
|
|
108491
|
+
const coverage = describeCoverage(checked.model);
|
|
108492
|
+
if (flags.json) {
|
|
108493
|
+
emitJson({
|
|
108494
|
+
ok: true,
|
|
108495
|
+
...countModel(checked.model),
|
|
108496
|
+
plan: planJson(checked.model),
|
|
108497
|
+
coverage: roleCoverage(checked.model.contract, checked.model.field_roles, checked.model.rows)
|
|
108498
|
+
});
|
|
108499
|
+
} else {
|
|
108500
|
+
console.log(summarizeModel(checked.model));
|
|
108501
|
+
for (const line of describePlan(checked.model)) console.log(line);
|
|
108502
|
+
if (coverage.length > 0) {
|
|
108503
|
+
console.log("What the rows would show:");
|
|
108504
|
+
for (const line of coverage) console.log(line);
|
|
108505
|
+
}
|
|
108506
|
+
}
|
|
107457
108507
|
return;
|
|
107458
108508
|
}
|
|
107459
108509
|
const { client: client2, ctx: ctx2 } = await requireClient(flags);
|
|
@@ -107774,7 +108824,7 @@ async function main() {
|
|
|
107774
108824
|
if (command === "preview") {
|
|
107775
108825
|
if (subcommand && isStoredFileId(subcommand)) {
|
|
107776
108826
|
const { client: client2 } = await requireClient(flags);
|
|
107777
|
-
const tmpDir = fs15.mkdtempSync(
|
|
108827
|
+
const tmpDir = fs15.mkdtempSync(path13.join(os4.tmpdir(), "lotics-preview-"));
|
|
107778
108828
|
try {
|
|
107779
108829
|
const { path: localPath, filename } = await client2.downloadFileById(subcommand, tmpDir);
|
|
107780
108830
|
const output = flags.output ?? defaultPreviewOutputPath(filename, process.cwd());
|
|
@@ -108150,7 +109200,7 @@ Available workspaces:`);
|
|
|
108150
109200
|
return;
|
|
108151
109201
|
}
|
|
108152
109202
|
if (subcommand === "check") {
|
|
108153
|
-
await appCheck(client, {});
|
|
109203
|
+
await appCheck(client, { screens: flags.screens });
|
|
108154
109204
|
return;
|
|
108155
109205
|
}
|
|
108156
109206
|
if (subcommand === "workflow") {
|