@datarails-ai/dr-appspace-cli 0.12.1 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +738 -67
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { fileURLToPath as fileURLToPath6 } from "url";
|
|
|
13
13
|
// package.json
|
|
14
14
|
var package_default = {
|
|
15
15
|
name: "@datarails-ai/dr-appspace-cli",
|
|
16
|
-
version: "0.12.
|
|
16
|
+
version: "0.12.3",
|
|
17
17
|
description: "Datarails App Space CLI \u2014 build, try, push and publish App Space apps",
|
|
18
18
|
type: "module",
|
|
19
19
|
engines: {
|
|
@@ -57,7 +57,7 @@ var package_default = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
// src/buildInfo.ts
|
|
60
|
-
var BUILD_TIME = "2026-10-
|
|
60
|
+
var BUILD_TIME = "2026-10-08T11:11:55.601Z";
|
|
61
61
|
function formatBuildTime(iso) {
|
|
62
62
|
if (iso === "") return "";
|
|
63
63
|
const d = new Date(iso);
|
|
@@ -208,7 +208,17 @@ function decodeShape(value) {
|
|
|
208
208
|
|
|
209
209
|
// src/auth/keyring/types.ts
|
|
210
210
|
function isCookielessSession(s) {
|
|
211
|
-
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "" || s.cookieless === true;
|
|
211
|
+
return s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "" || s.cookieless === true || hasApiKey(s) || hasApiKeyEnvVar(s);
|
|
212
|
+
}
|
|
213
|
+
function hasApiKey(s) {
|
|
214
|
+
return s.apiKey !== void 0 && s.apiKey !== "";
|
|
215
|
+
}
|
|
216
|
+
function hasApiKeyEnvVar(s) {
|
|
217
|
+
return s.apiKeyEnvVar !== void 0 && s.apiKeyEnvVar !== "";
|
|
218
|
+
}
|
|
219
|
+
function usesApiKey(s) {
|
|
220
|
+
const impersonated = s.impersonatorEmail !== void 0 && s.impersonatorEmail !== "";
|
|
221
|
+
return (hasApiKey(s) || hasApiKeyEnvVar(s)) && !impersonated;
|
|
212
222
|
}
|
|
213
223
|
var KeychainError = class extends Error {
|
|
214
224
|
constructor(message, options) {
|
|
@@ -428,7 +438,9 @@ var KeyringStore = class _KeyringStore {
|
|
|
428
438
|
try {
|
|
429
439
|
parsed = JSON.parse(raw);
|
|
430
440
|
} catch (err) {
|
|
431
|
-
throw new KeychainError(`decode session for ${serverKey}:${email}`, {
|
|
441
|
+
throw new KeychainError(`decode session for ${serverKey}:${email}`, {
|
|
442
|
+
cause: err
|
|
443
|
+
});
|
|
432
444
|
}
|
|
433
445
|
const sessionId = typeof parsed.sessionId === "string" ? parsed.sessionId : "";
|
|
434
446
|
const csrfToken = typeof parsed.csrfToken === "string" ? parsed.csrfToken : "";
|
|
@@ -436,7 +448,10 @@ var KeyringStore = class _KeyringStore {
|
|
|
436
448
|
const impersonatorEmail = typeof parsed.impersonatorEmail === "string" ? parsed.impersonatorEmail : "";
|
|
437
449
|
const impersonated = impersonatorEmail !== "";
|
|
438
450
|
const cookielessFlag = parsed.cookieless === true;
|
|
439
|
-
const
|
|
451
|
+
const apiKey = typeof parsed.apiKey === "string" && parsed.apiKey !== "" ? parsed.apiKey : "";
|
|
452
|
+
const apiKeyEnvVar = typeof parsed.apiKeyEnvVar === "string" && parsed.apiKeyEnvVar !== "" ? parsed.apiKeyEnvVar : "";
|
|
453
|
+
const keyed = apiKey !== "" || apiKeyEnvVar !== "";
|
|
454
|
+
const cookieless = impersonated || cookielessFlag || keyed;
|
|
440
455
|
if (savedAtStr === "" || !cookieless && (sessionId === "" || csrfToken === "")) {
|
|
441
456
|
throw new KeychainError(`session for ${serverKey}:${email} is malformed`);
|
|
442
457
|
}
|
|
@@ -458,6 +473,18 @@ var KeyringStore = class _KeyringStore {
|
|
|
458
473
|
if (cookielessFlag) {
|
|
459
474
|
session.cookieless = true;
|
|
460
475
|
}
|
|
476
|
+
if (apiKey !== "") {
|
|
477
|
+
session.apiKey = apiKey;
|
|
478
|
+
}
|
|
479
|
+
if (apiKeyEnvVar !== "") {
|
|
480
|
+
session.apiKeyEnvVar = apiKeyEnvVar;
|
|
481
|
+
}
|
|
482
|
+
if (typeof parsed.apiKeyExpiresAt === "string" && parsed.apiKeyExpiresAt !== "") {
|
|
483
|
+
session.apiKeyExpiresAt = new Date(parsed.apiKeyExpiresAt);
|
|
484
|
+
}
|
|
485
|
+
if (typeof parsed.jwtApiKeyFingerprint === "string" && parsed.jwtApiKeyFingerprint !== "") {
|
|
486
|
+
session.jwtApiKeyFingerprint = parsed.jwtApiKeyFingerprint;
|
|
487
|
+
}
|
|
461
488
|
if (typeof parsed.jwtExpiresAt === "string" && parsed.jwtExpiresAt !== "") {
|
|
462
489
|
session.jwtExpiresAt = new Date(parsed.jwtExpiresAt);
|
|
463
490
|
}
|
|
@@ -499,7 +526,7 @@ var KeyringStore = class _KeyringStore {
|
|
|
499
526
|
const impersonated = session.impersonatorEmail !== void 0 && session.impersonatorEmail !== "";
|
|
500
527
|
const cookieless = session.cookieless === true;
|
|
501
528
|
if (isCookielessSession(session)) {
|
|
502
|
-
if (session.jwtAccess === void 0 || session.jwtAccess === "") {
|
|
529
|
+
if (!usesApiKey(session) && (session.jwtAccess === void 0 || session.jwtAccess === "")) {
|
|
503
530
|
throw new Error("saveSession: cookie-less session must have a jwtAccess token");
|
|
504
531
|
}
|
|
505
532
|
} else {
|
|
@@ -533,6 +560,18 @@ var KeyringStore = class _KeyringStore {
|
|
|
533
560
|
if (cookieless) {
|
|
534
561
|
metaBlob.cookieless = true;
|
|
535
562
|
}
|
|
563
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
564
|
+
metaBlob.apiKey = session.apiKey;
|
|
565
|
+
}
|
|
566
|
+
if (session.apiKeyEnvVar !== void 0 && session.apiKeyEnvVar !== "") {
|
|
567
|
+
metaBlob.apiKeyEnvVar = session.apiKeyEnvVar;
|
|
568
|
+
}
|
|
569
|
+
if (session.apiKeyExpiresAt !== void 0) {
|
|
570
|
+
metaBlob.apiKeyExpiresAt = session.apiKeyExpiresAt.toISOString();
|
|
571
|
+
}
|
|
572
|
+
if (session.jwtApiKeyFingerprint !== void 0 && session.jwtApiKeyFingerprint !== "") {
|
|
573
|
+
metaBlob.jwtApiKeyFingerprint = session.jwtApiKeyFingerprint;
|
|
574
|
+
}
|
|
536
575
|
await this.backend.set(SERVICE, sessionAccount, JSON.stringify(metaBlob));
|
|
537
576
|
if (session.jwtAccess !== void 0 && session.jwtAccess !== "") {
|
|
538
577
|
await this.backend.set(SERVICE, `${sessionAccount}:access`, session.jwtAccess);
|
|
@@ -564,7 +603,11 @@ var KeyringStore = class _KeyringStore {
|
|
|
564
603
|
blob.lastUsedAt = now.toISOString();
|
|
565
604
|
await this.backend.set(SERVICE, sessionAccount, JSON.stringify(blob));
|
|
566
605
|
const savedAt = parseDate(blob.savedAt) ?? now;
|
|
567
|
-
await this.tryUpsertLegacyIndex(serverKey, {
|
|
606
|
+
await this.tryUpsertLegacyIndex(serverKey, {
|
|
607
|
+
email,
|
|
608
|
+
savedAt,
|
|
609
|
+
lastUsedAt: now
|
|
610
|
+
});
|
|
568
611
|
}
|
|
569
612
|
/**
|
|
570
613
|
* Upserts one account into the legacy `__index__:<server>` entry via a
|
|
@@ -826,6 +869,9 @@ async function pollForToken(host, deviceCode, options = {}) {
|
|
|
826
869
|
}
|
|
827
870
|
}
|
|
828
871
|
|
|
872
|
+
// src/auth/jwt.ts
|
|
873
|
+
import { createHash } from "crypto";
|
|
874
|
+
|
|
829
875
|
// src/auth/types.ts
|
|
830
876
|
function jwtExpiringSoon(e, bufferMs) {
|
|
831
877
|
if (!e.jwtExpiresAt) return true;
|
|
@@ -1040,7 +1086,11 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1040
1086
|
if (typeof parsed["detail"] === "string") detail = parsed["detail"];
|
|
1041
1087
|
} catch {
|
|
1042
1088
|
}
|
|
1043
|
-
throw new LoginAsError(res.status, {
|
|
1089
|
+
throw new LoginAsError(res.status, {
|
|
1090
|
+
code,
|
|
1091
|
+
detail,
|
|
1092
|
+
body: truncate(text3, TRUNCATE)
|
|
1093
|
+
});
|
|
1044
1094
|
}
|
|
1045
1095
|
let raw;
|
|
1046
1096
|
try {
|
|
@@ -1065,6 +1115,93 @@ async function loginAs(host, operatorAccessToken, userId, options) {
|
|
|
1065
1115
|
}
|
|
1066
1116
|
return { access, refresh, expiresAt, user: obj4["user"] };
|
|
1067
1117
|
}
|
|
1118
|
+
async function exchangeApiKey(host, apiKey, options) {
|
|
1119
|
+
const key = apiKey.trim();
|
|
1120
|
+
if (key === "") {
|
|
1121
|
+
throw new Error("api-key exchange: the stored API key is empty");
|
|
1122
|
+
}
|
|
1123
|
+
if (options?.opaque === true) {
|
|
1124
|
+
if (/[\r\n\0]/.test(key) || !/^[\x00-\xff]+$/.test(key)) {
|
|
1125
|
+
throw new Error(
|
|
1126
|
+
"api-key exchange: the environment variable named by apiKeyEnvVar holds a value that cannot travel in an HTTP header (a line break, NUL, or a character above U+00FF)"
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
} else if (!/^[\x21-\x7e]+$/.test(key)) {
|
|
1130
|
+
throw new Error(
|
|
1131
|
+
"api-key exchange: only a printable-ASCII API key with no spaces is accepted; the stored one contains whitespace, control characters, or non-ASCII characters"
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
const base = trimTrailingSlash(host);
|
|
1135
|
+
const res = await fetch(`${base}/jwt/api/apikey/token/`, {
|
|
1136
|
+
method: "POST",
|
|
1137
|
+
headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
|
|
1138
|
+
signal: options?.signal
|
|
1139
|
+
});
|
|
1140
|
+
const text3 = await res.text();
|
|
1141
|
+
if (!res.ok) {
|
|
1142
|
+
let code;
|
|
1143
|
+
try {
|
|
1144
|
+
const value = JSON.parse(text3)["error"];
|
|
1145
|
+
if (value === "invalid_key" || value === "exchange_unavailable") code = value;
|
|
1146
|
+
} catch {
|
|
1147
|
+
}
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
`api-key exchange: HTTP ${res.status}${code !== void 0 ? ` (${code})` : ""}`
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
1152
|
+
let raw;
|
|
1153
|
+
try {
|
|
1154
|
+
raw = JSON.parse(text3);
|
|
1155
|
+
} catch {
|
|
1156
|
+
throw new Error("api-key exchange parse: the response is not JSON");
|
|
1157
|
+
}
|
|
1158
|
+
if (raw === null || typeof raw !== "object") {
|
|
1159
|
+
throw new Error("api-key exchange parse: expected object");
|
|
1160
|
+
}
|
|
1161
|
+
const access = raw["access_token"];
|
|
1162
|
+
if (typeof access !== "string" || access === "") {
|
|
1163
|
+
throw new Error("api-key exchange: empty access token");
|
|
1164
|
+
}
|
|
1165
|
+
let expiresAt;
|
|
1166
|
+
try {
|
|
1167
|
+
expiresAt = decodeJwtExpiration(access);
|
|
1168
|
+
} catch (err) {
|
|
1169
|
+
throw new Error(`api-key exchange: ${msgOf3(err)}`);
|
|
1170
|
+
}
|
|
1171
|
+
return { access, expiresAt };
|
|
1172
|
+
}
|
|
1173
|
+
function apiKeyFingerprint(apiKey) {
|
|
1174
|
+
return createHash("sha256").update(apiKey.trim()).digest("hex").slice(0, 32);
|
|
1175
|
+
}
|
|
1176
|
+
function resolveApiKeyMaterial(session) {
|
|
1177
|
+
if (session.apiKey !== void 0 && session.apiKey !== "") {
|
|
1178
|
+
return { key: session.apiKey, fromEnv: false };
|
|
1179
|
+
}
|
|
1180
|
+
const name = session.apiKeyEnvVar;
|
|
1181
|
+
if (name === void 0 || name === "") {
|
|
1182
|
+
throw new Error("api-key exchange: the session carries no API key");
|
|
1183
|
+
}
|
|
1184
|
+
const value = process.env[name];
|
|
1185
|
+
if (value === void 0 || value === "") {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`api-key exchange: the credential file names environment variable ${name} as the API-key carrier, but it is unset or empty in this environment`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
return { key: value, fromEnv: true };
|
|
1191
|
+
}
|
|
1192
|
+
function mintedByStoredApiKey(session) {
|
|
1193
|
+
if (session.jwtAccess === void 0 || session.jwtAccess === "") return false;
|
|
1194
|
+
if (session.jwtApiKeyFingerprint === void 0 || session.jwtApiKeyFingerprint === "") {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
let material;
|
|
1198
|
+
try {
|
|
1199
|
+
material = resolveApiKeyMaterial(session);
|
|
1200
|
+
} catch {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
return session.jwtApiKeyFingerprint === apiKeyFingerprint(material.key);
|
|
1204
|
+
}
|
|
1068
1205
|
async function ensureValidJwt(store, serverKey, email, options) {
|
|
1069
1206
|
return ensureJwt(store, serverKey, email, {
|
|
1070
1207
|
buffer: options?.buffer ?? 0,
|
|
@@ -1086,6 +1223,10 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1086
1223
|
if (session === void 0) {
|
|
1087
1224
|
throw new NotLoggedInError(`not logged in: ${serverKey}:${email}`);
|
|
1088
1225
|
}
|
|
1226
|
+
if (usesApiKey(session)) {
|
|
1227
|
+
const host2 = opts.host ?? resolve(serverKey).host;
|
|
1228
|
+
return exchangeKeyedSession(store, serverKey, email, session, host2, opts);
|
|
1229
|
+
}
|
|
1089
1230
|
if (!opts.force && session.jwtAccess !== void 0 && session.jwtAccess !== "" && !jwtExpiringSoon(session, opts.buffer)) {
|
|
1090
1231
|
return session;
|
|
1091
1232
|
}
|
|
@@ -1099,7 +1240,9 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1099
1240
|
if (session.jwtRefresh !== void 0 && session.jwtRefresh !== "" && host !== "") {
|
|
1100
1241
|
try {
|
|
1101
1242
|
const refresh = session.cookieless === true ? refreshExternalJwt : refreshJwt;
|
|
1102
|
-
const tok = await refresh(host, session.jwtRefresh, {
|
|
1243
|
+
const tok = await refresh(host, session.jwtRefresh, {
|
|
1244
|
+
signal: opts.signal
|
|
1245
|
+
});
|
|
1103
1246
|
refreshNext = {
|
|
1104
1247
|
...session,
|
|
1105
1248
|
jwtAccess: tok.access,
|
|
@@ -1157,10 +1300,47 @@ async function ensureJwt(store, serverKey, email, opts) {
|
|
|
1157
1300
|
const detail = reasons.length > 0 ? reasons.join("; ") : "no recovery path available";
|
|
1158
1301
|
throw new SessionExpiredError(`${serverKey}:${email} (${detail})`);
|
|
1159
1302
|
}
|
|
1303
|
+
async function exchangeKeyedSession(store, serverKey, email, session, host, opts) {
|
|
1304
|
+
if (!opts.force && mintedByStoredApiKey(session) && !jwtExpiringSoon(session, opts.buffer)) {
|
|
1305
|
+
return session;
|
|
1306
|
+
}
|
|
1307
|
+
if (host === "") {
|
|
1308
|
+
throw new SessionExpiredError(
|
|
1309
|
+
`${serverKey}:${email} (no host to exchange the API key against)`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
const material = resolveApiKeyMaterial(session);
|
|
1313
|
+
let tok;
|
|
1314
|
+
try {
|
|
1315
|
+
tok = await exchangeApiKey(host, material.key, {
|
|
1316
|
+
signal: opts.signal,
|
|
1317
|
+
opaque: material.fromEnv
|
|
1318
|
+
});
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
throw new SessionExpiredError(`${serverKey}:${email} (${msgOf3(err)})`);
|
|
1321
|
+
}
|
|
1322
|
+
const next = {
|
|
1323
|
+
...session,
|
|
1324
|
+
jwtAccess: tok.access,
|
|
1325
|
+
jwtExpiresAt: tok.expiresAt,
|
|
1326
|
+
jwtApiKeyFingerprint: apiKeyFingerprint(material.key),
|
|
1327
|
+
// An API-key identity has no refresh token; clear any stale one so the
|
|
1328
|
+
// refresh ladder is never tempted by it on a later non-keyed read.
|
|
1329
|
+
jwtRefresh: void 0
|
|
1330
|
+
};
|
|
1331
|
+
try {
|
|
1332
|
+
await store.saveSession(serverKey, next);
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
throw new Error(`save credentials: ${msgOf3(err)}`, { cause: err });
|
|
1335
|
+
}
|
|
1336
|
+
return next;
|
|
1337
|
+
}
|
|
1160
1338
|
async function recoverImpersonatedJwt(store, serverKey, email, session, host, opts) {
|
|
1161
1339
|
if (session.jwtRefresh !== void 0 && session.jwtRefresh !== "" && host !== "") {
|
|
1162
1340
|
try {
|
|
1163
|
-
const tok = await refreshExternalJwt(host, session.jwtRefresh, {
|
|
1341
|
+
const tok = await refreshExternalJwt(host, session.jwtRefresh, {
|
|
1342
|
+
signal: opts.signal
|
|
1343
|
+
});
|
|
1164
1344
|
const next2 = {
|
|
1165
1345
|
...session,
|
|
1166
1346
|
jwtAccess: tok.access,
|
|
@@ -1341,7 +1521,7 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
1341
1521
|
}
|
|
1342
1522
|
const cookieless = session.cookieless === true;
|
|
1343
1523
|
if (isCookielessSession(session)) {
|
|
1344
|
-
if (session.jwtAccess === void 0 || session.jwtAccess === "") {
|
|
1524
|
+
if (!usesApiKey(session) && (session.jwtAccess === void 0 || session.jwtAccess === "")) {
|
|
1345
1525
|
throw new NotLoggedInError(
|
|
1346
1526
|
cookieless ? `not logged in: ${serverKey}:${email} (cookie-less session has no JWT)` : `not logged in: ${serverKey}:${email} (impersonated session has no JWT; re-run dr-appspace login-as)`
|
|
1347
1527
|
);
|
|
@@ -1845,7 +2025,7 @@ var AuthorizedClient = class _AuthorizedClient {
|
|
|
1845
2025
|
async ensureFreshAuth(bufferMs, signal) {
|
|
1846
2026
|
const access = this._session.jwtAccess ?? "";
|
|
1847
2027
|
const refresh = this._session.jwtRefresh ?? "";
|
|
1848
|
-
if (access === "" && refresh === "") {
|
|
2028
|
+
if (access === "" && refresh === "" && !usesApiKey(this._session)) {
|
|
1849
2029
|
return;
|
|
1850
2030
|
}
|
|
1851
2031
|
try {
|
|
@@ -1894,8 +2074,27 @@ import { z } from "zod";
|
|
|
1894
2074
|
var CREDENTIALS_FILE_ENV = "DR_CREDENTIALS_FILE";
|
|
1895
2075
|
var DEFAULT_CREDENTIALS_FILE = join3(homedir3(), ".dr", "session.json");
|
|
1896
2076
|
var credentialsFileSchema = z.object({
|
|
1897
|
-
access: z.string().min(1),
|
|
1898
|
-
refresh: z.string().min(1),
|
|
2077
|
+
access: z.string().min(1).optional(),
|
|
2078
|
+
refresh: z.string().min(1).optional(),
|
|
2079
|
+
// A user-bound API key, exchanged for `access` on demand. A file may hold
|
|
2080
|
+
// only this; `access` then appears after the first exchange.
|
|
2081
|
+
apiKey: z.string().min(1).optional(),
|
|
2082
|
+
// DR-52657: the NAME of an environment variable holding the API key, for
|
|
2083
|
+
// provisioners that must not put the key on disk — an agent sandbox, where
|
|
2084
|
+
// the variable's in-sandbox value is an opaque vault placeholder, or a CI
|
|
2085
|
+
// runner keeping the key in its secret env. Resolved at exchange time and
|
|
2086
|
+
// passed verbatim. Mutually exclusive with `apiKey` (refinement below).
|
|
2087
|
+
apiKeyEnvVar: z.string().min(1).optional(),
|
|
2088
|
+
// Carried verbatim, judged where it is read. `z.string()` (not validated or
|
|
2089
|
+
// `.catch`): a rotation rewrites the whole file from the parsed object, so a
|
|
2090
|
+
// value this schema dropped would be deleted silently, and a top-level
|
|
2091
|
+
// rejection would fail the WHOLE file and lock the operator out of logout.
|
|
2092
|
+
apiKeyExpiresAt: z.string().optional(),
|
|
2093
|
+
// Which key minted `access`, as a non-secret fingerprint. Written by the
|
|
2094
|
+
// CLI, not a provisioner: a rotation replaces `apiKey` and leaves this
|
|
2095
|
+
// naming the old one, which is how the next command knows `access` is not
|
|
2096
|
+
// the new key's. `z.string()` for the same lock-out reason as above.
|
|
2097
|
+
accessApiKeyFingerprint: z.string().optional(),
|
|
1899
2098
|
server: z.string().min(1),
|
|
1900
2099
|
email: z.string().min(1),
|
|
1901
2100
|
// min(1): an empty string is how a hand-rolled producer spells "no cookie",
|
|
@@ -1911,7 +2110,19 @@ var credentialsFileSchema = z.object({
|
|
|
1911
2110
|
}).loose().refine((c) => c.sessionId === void 0 === (c.csrfToken === void 0), {
|
|
1912
2111
|
message: "sessionId and csrfToken must be provided together",
|
|
1913
2112
|
path: ["sessionId"]
|
|
1914
|
-
})
|
|
2113
|
+
}).refine((c) => !(c.apiKey !== void 0 && c.apiKeyEnvVar !== void 0), {
|
|
2114
|
+
message: "apiKey and apiKeyEnvVar are mutually exclusive",
|
|
2115
|
+
path: ["apiKeyEnvVar"]
|
|
2116
|
+
}).refine((c) => c.access !== void 0 || c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0, {
|
|
2117
|
+
message: "one of access, apiKey or apiKeyEnvVar is required",
|
|
2118
|
+
path: ["access"]
|
|
2119
|
+
}).refine(
|
|
2120
|
+
(c) => c.apiKey !== void 0 || c.apiKeyEnvVar !== void 0 || c.refresh !== void 0 || c.sessionId !== void 0,
|
|
2121
|
+
{
|
|
2122
|
+
message: "refresh is required unless an apiKey, apiKeyEnvVar or a session cookie is present",
|
|
2123
|
+
path: ["refresh"]
|
|
2124
|
+
}
|
|
2125
|
+
);
|
|
1915
2126
|
function credentialsFilePath() {
|
|
1916
2127
|
const override = process.env[CREDENTIALS_FILE_ENV];
|
|
1917
2128
|
return override !== void 0 && override !== "" ? override : DEFAULT_CREDENTIALS_FILE;
|
|
@@ -1924,8 +2135,14 @@ async function readCredentialsFile(path3) {
|
|
|
1924
2135
|
if (err.code === "ENOENT") return void 0;
|
|
1925
2136
|
throw err;
|
|
1926
2137
|
}
|
|
2138
|
+
let parsed;
|
|
2139
|
+
try {
|
|
2140
|
+
parsed = JSON.parse(raw);
|
|
2141
|
+
} catch {
|
|
2142
|
+
throw new Error(`credential file ${path3} is not valid JSON`);
|
|
2143
|
+
}
|
|
1927
2144
|
try {
|
|
1928
|
-
return credentialsFileSchema.parse(
|
|
2145
|
+
return credentialsFileSchema.parse(parsed);
|
|
1929
2146
|
} catch (err) {
|
|
1930
2147
|
const reason = err instanceof Error ? err.message : String(err);
|
|
1931
2148
|
throw new Error(`credential file ${path3} is malformed: ${reason}`, { cause: err });
|
|
@@ -1933,11 +2150,19 @@ async function readCredentialsFile(path3) {
|
|
|
1933
2150
|
}
|
|
1934
2151
|
async function writeCredentialsFile(path3, creds) {
|
|
1935
2152
|
await mkdir(dirname2(path3), { recursive: true, mode: 448 });
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2153
|
+
let next;
|
|
2154
|
+
try {
|
|
2155
|
+
next = credentialsFileSchema.parse({
|
|
2156
|
+
...creds,
|
|
2157
|
+
savedAt: creds.savedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2158
|
+
});
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2161
|
+
throw new Error(`refusing to write an unreadable credential file ${path3}: ${reason}`, {
|
|
2162
|
+
cause: err
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
const body = JSON.stringify(next, null, 2);
|
|
1941
2166
|
const tmp = `${path3}.${process.pid}.tmp`;
|
|
1942
2167
|
await writeFile(tmp, body, { mode: 384 });
|
|
1943
2168
|
await chmod(tmp, 384);
|
|
@@ -1966,9 +2191,11 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
1966
2191
|
const savedAt = creds.savedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1967
2192
|
const sessionId = creds.sessionId ?? "";
|
|
1968
2193
|
let jwtExpiresAt;
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2194
|
+
if (creds.access !== void 0 && creds.access !== "") {
|
|
2195
|
+
try {
|
|
2196
|
+
jwtExpiresAt = decodeJwtExpiration(creds.access).toISOString();
|
|
2197
|
+
} catch {
|
|
2198
|
+
}
|
|
1972
2199
|
}
|
|
1973
2200
|
return JSON.stringify({
|
|
1974
2201
|
sessionId,
|
|
@@ -1976,7 +2203,19 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
1976
2203
|
savedAt,
|
|
1977
2204
|
lastUsedAt: savedAt,
|
|
1978
2205
|
...sessionId === "" && { cookieless: true },
|
|
1979
|
-
...jwtExpiresAt !== void 0 && { jwtExpiresAt }
|
|
2206
|
+
...jwtExpiresAt !== void 0 && { jwtExpiresAt },
|
|
2207
|
+
// Carry the API-key carrier through so `KeyringStore.getSession` sees a
|
|
2208
|
+
// keyed identity and routes it to the exchange, plus the fingerprint of the
|
|
2209
|
+
// key that minted the stored `access` so a reload can tell the cached JWT
|
|
2210
|
+
// is still the current key's. Without it a file store would re-exchange on
|
|
2211
|
+
// EVERY request — `ensureFreshAuth` reloads the session each time, and
|
|
2212
|
+
// `mintedByStoredApiKey` can never match a fingerprint that was never
|
|
2213
|
+
// written. `set('meta')` persists it back below.
|
|
2214
|
+
...creds.apiKey !== void 0 && creds.apiKey !== "" && { apiKey: creds.apiKey },
|
|
2215
|
+
...creds.apiKeyEnvVar !== void 0 && creds.apiKeyEnvVar !== "" && { apiKeyEnvVar: creds.apiKeyEnvVar },
|
|
2216
|
+
...creds.accessApiKeyFingerprint !== void 0 && creds.accessApiKeyFingerprint !== "" && {
|
|
2217
|
+
jwtApiKeyFingerprint: creds.accessApiKeyFingerprint
|
|
2218
|
+
}
|
|
1980
2219
|
});
|
|
1981
2220
|
}
|
|
1982
2221
|
static slotOf(creds, account) {
|
|
@@ -2001,6 +2240,20 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2001
2240
|
...csrfToken !== "" && { csrfToken }
|
|
2002
2241
|
};
|
|
2003
2242
|
}
|
|
2243
|
+
/**
|
|
2244
|
+
* The key fingerprint out of a metadata blob (`KeyringStore` emits it as
|
|
2245
|
+
* `jwtApiKeyFingerprint`), empty when absent. Unlike the cookie it is NOT
|
|
2246
|
+
* derived, so it has to be persisted back to the file or every request
|
|
2247
|
+
* re-exchanges.
|
|
2248
|
+
*/
|
|
2249
|
+
static fingerprintOf(blob) {
|
|
2250
|
+
try {
|
|
2251
|
+
const parsed = JSON.parse(blob);
|
|
2252
|
+
return typeof parsed.jwtApiKeyFingerprint === "string" ? parsed.jwtApiKeyFingerprint : "";
|
|
2253
|
+
} catch {
|
|
2254
|
+
return "";
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2004
2257
|
async get(_service, account) {
|
|
2005
2258
|
const creds = await this.load();
|
|
2006
2259
|
if (creds === void 0) return void 0;
|
|
@@ -2026,10 +2279,14 @@ var FileKeyringBackend = class _FileKeyringBackend {
|
|
|
2026
2279
|
switch (_FileKeyringBackend.slotOf(creds, account)) {
|
|
2027
2280
|
case "meta": {
|
|
2028
2281
|
const cookie = _FileKeyringBackend.cookieOf(value);
|
|
2029
|
-
|
|
2282
|
+
const fingerprint = _FileKeyringBackend.fingerprintOf(value);
|
|
2283
|
+
if (cookie.sessionId === creds.sessionId && cookie.csrfToken === creds.csrfToken && fingerprint === (creds.accessApiKeyFingerprint ?? "")) {
|
|
2030
2284
|
return;
|
|
2031
2285
|
}
|
|
2032
|
-
|
|
2286
|
+
const next = { ...creds, ...cookie };
|
|
2287
|
+
if (fingerprint !== "") next.accessApiKeyFingerprint = fingerprint;
|
|
2288
|
+
else delete next.accessApiKeyFingerprint;
|
|
2289
|
+
await writeCredentialsFile(this.path, next);
|
|
2033
2290
|
return;
|
|
2034
2291
|
}
|
|
2035
2292
|
case "access":
|
|
@@ -6605,6 +6862,10 @@ function partCalls(element) {
|
|
|
6605
6862
|
for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
|
|
6606
6863
|
if (action && typeof action === "object" && named(action.call)) out.push(action.call);
|
|
6607
6864
|
}
|
|
6865
|
+
const bulk = typed.selection && typeof typed.selection === "object" ? typed.selection.bulk_actions : void 0;
|
|
6866
|
+
for (const action of Array.isArray(bulk) ? bulk : []) {
|
|
6867
|
+
if (action && typeof action === "object" && named(action.call)) out.push(action.call);
|
|
6868
|
+
}
|
|
6608
6869
|
for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
|
|
6609
6870
|
if (column && typeof column === "object" && column.cell === "toggle" && named(column.call)) out.push(column.call);
|
|
6610
6871
|
}
|
|
@@ -7313,9 +7574,7 @@ function backendSection(app, manifest, pages, elements) {
|
|
|
7313
7574
|
signature_ok: signatureOk(source),
|
|
7314
7575
|
reads_ctx_role: CTX_ROLE.test(source),
|
|
7315
7576
|
tables_touched: [...new Set([...source.matchAll(TABLE_CALL)].map((match) => match[2]))].sort(),
|
|
7316
|
-
verbs:
|
|
7317
|
-
(verb) => TABLE_ONLY_VERBS.has(verb) ? callsOnTable(source, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(source)
|
|
7318
|
-
),
|
|
7577
|
+
verbs: handlerVerbs(source),
|
|
7319
7578
|
referenced_by_page: isReferenced(handler.name, scriptText) || attributed.has(handler.name)
|
|
7320
7579
|
};
|
|
7321
7580
|
}
|
|
@@ -7334,6 +7593,10 @@ function backendSection(app, manifest, pages, elements) {
|
|
|
7334
7593
|
invariants: invariants(model, roles, handlers)
|
|
7335
7594
|
};
|
|
7336
7595
|
}
|
|
7596
|
+
function handlerVerbs(source) {
|
|
7597
|
+
const text3 = typeof source === "string" ? source : "";
|
|
7598
|
+
return DATA_VERBS.filter((verb) => TABLE_ONLY_VERBS.has(verb) ? callsOnTable(text3, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(text3));
|
|
7599
|
+
}
|
|
7337
7600
|
function callsOnTable(source, verb) {
|
|
7338
7601
|
const holders = [TABLE_INLINE];
|
|
7339
7602
|
for (const match of source.matchAll(TABLE_LOCAL)) holders.push(match[1]);
|
|
@@ -8810,7 +9073,7 @@ import { tmpdir as tmpdir2 } from "os";
|
|
|
8810
9073
|
import { dirname as dirname3, join as join10 } from "path";
|
|
8811
9074
|
|
|
8812
9075
|
// src/commands/appspace/generate/report.ts
|
|
8813
|
-
import { createHash } from "crypto";
|
|
9076
|
+
import { createHash as createHash2 } from "crypto";
|
|
8814
9077
|
import { mkdir as mkdir3, readFile as readFile7, writeFile as writeFile4 } from "fs/promises";
|
|
8815
9078
|
import { join as join9 } from "path";
|
|
8816
9079
|
function reportPath(dir) {
|
|
@@ -8858,7 +9121,7 @@ async function ownedFiles(dir) {
|
|
|
8858
9121
|
}
|
|
8859
9122
|
}
|
|
8860
9123
|
function digestOf(text3) {
|
|
8861
|
-
return
|
|
9124
|
+
return createHash2("sha256").update(text3, "utf8").digest("hex");
|
|
8862
9125
|
}
|
|
8863
9126
|
async function recordedDigests(dir) {
|
|
8864
9127
|
try {
|
|
@@ -9124,7 +9387,7 @@ function registerGetCommand(parent) {
|
|
|
9124
9387
|
|
|
9125
9388
|
// src/commands/appspace/generate/catalog.ts
|
|
9126
9389
|
import { readFile as readFile9 } from "fs/promises";
|
|
9127
|
-
import { createHash as
|
|
9390
|
+
import { createHash as createHash3 } from "crypto";
|
|
9128
9391
|
var NO_CATALOG = {
|
|
9129
9392
|
source: "none",
|
|
9130
9393
|
metrics: [],
|
|
@@ -11041,6 +11304,9 @@ function payloadProducers(contract, { moduleSource = () => void 0, pageSource =
|
|
|
11041
11304
|
for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
|
|
11042
11305
|
if (isPlainObject3(action) && named2(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
|
|
11043
11306
|
}
|
|
11307
|
+
for (const action of isPlainObject3(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
|
|
11308
|
+
if (isPlainObject3(action) && named2(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["ids"] });
|
|
11309
|
+
}
|
|
11044
11310
|
for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
|
|
11045
11311
|
if (isPlainObject3(column) && column.cell === "toggle" && named2(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
|
|
11046
11312
|
}
|
|
@@ -11835,6 +12101,11 @@ function stateCallers(contract, { moduleSource = () => void 0, handlerSource = (
|
|
|
11835
12101
|
const asks = action.confirm === true || named3(action.confirm);
|
|
11836
12102
|
add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
|
|
11837
12103
|
}
|
|
12104
|
+
for (const action of isPlainObject4(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
|
|
12105
|
+
if (!isPlainObject4(action) || !named3(action.call)) continue;
|
|
12106
|
+
const asks = action.confirm === true || named3(action.confirm);
|
|
12107
|
+
add(action.call, { page, uid, id, what: `bulk action "${action.label ?? action.call}"`, asks });
|
|
12108
|
+
}
|
|
11838
12109
|
break;
|
|
11839
12110
|
case "action":
|
|
11840
12111
|
case "detail": {
|
|
@@ -12164,13 +12435,18 @@ function isPlatformChartColour(n, value) {
|
|
|
12164
12435
|
var look_exports = {};
|
|
12165
12436
|
__export(look_exports, {
|
|
12166
12437
|
HEADER_ACTION_TYPES: () => HEADER_ACTION_TYPES,
|
|
12438
|
+
STATE_NAMES: () => STATE_NAMES,
|
|
12439
|
+
folderParams: () => folderParams,
|
|
12167
12440
|
headerActionProblems: () => headerActionProblems,
|
|
12168
12441
|
humanise: () => humanise,
|
|
12442
|
+
isAppContract: () => isAppContract,
|
|
12443
|
+
kpiStripWorkflowProblems: () => kpiStripWorkflowProblems,
|
|
12169
12444
|
layoutProblems: () => layoutProblems,
|
|
12170
12445
|
lookPages: () => lookPages,
|
|
12171
12446
|
navCountProblems: () => navCountProblems,
|
|
12172
12447
|
railReachProblems: () => railReachProblems,
|
|
12173
12448
|
rootsOf: () => rootsOf,
|
|
12449
|
+
shellExpectedProblems: () => shellExpectedProblems,
|
|
12174
12450
|
untitledCards: () => untitledCards
|
|
12175
12451
|
});
|
|
12176
12452
|
var isPlainObject5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -12478,6 +12754,181 @@ function railReachProblems(contract) {
|
|
|
12478
12754
|
}
|
|
12479
12755
|
return out;
|
|
12480
12756
|
}
|
|
12757
|
+
var STATE_NAMES = ["folder", "status", "stage", "state", "queue"];
|
|
12758
|
+
var CATCH_ALL = /* @__PURE__ */ new Set(["", "all", "any", "*", "everything"]);
|
|
12759
|
+
var PICK_KINDS = /* @__PURE__ */ new Set(["select", "multi"]);
|
|
12760
|
+
var ROW_TYPES = /* @__PURE__ */ new Set(["data_grid", "list", "queue"]);
|
|
12761
|
+
var NOT_COUNTS = ["sum", "avg", "avg_days", "count_events"];
|
|
12762
|
+
var VALUES_FIX = "state `app.shell` with `nav: rail` and one entry per value, counts from a `summary` handler; look.md rule 1";
|
|
12763
|
+
var PAGES_FIX = "state `app.shell` with `nav: rail`; with no `rail.entries` the rail lists the pages; look.md rule 1";
|
|
12764
|
+
var COUNTS_FIX = "state `app.shell` with `nav: rail` and one entry per state, each counting a measure of that summary (`pages[].nav.counts.source`); look.md rule 1";
|
|
12765
|
+
function isAppContract(contract) {
|
|
12766
|
+
const kind = contract?.app?.kind;
|
|
12767
|
+
return kind !== "dashboard" && (kind === "app" || isPlainObject5(contract?.data?.app));
|
|
12768
|
+
}
|
|
12769
|
+
function tablesOf(contract) {
|
|
12770
|
+
const tables = contract?.app?.data_model?.tables;
|
|
12771
|
+
return isPlainObject5(tables) ? tables : {};
|
|
12772
|
+
}
|
|
12773
|
+
function enumOf(column) {
|
|
12774
|
+
return Array.isArray(column?.enum) ? column.enum.filter((one) => typeof one === "string") : [];
|
|
12775
|
+
}
|
|
12776
|
+
function stateColumn(contract, table) {
|
|
12777
|
+
const columns = isPlainObject5(tablesOf(contract)[table]?.columns) ? tablesOf(contract)[table].columns : {};
|
|
12778
|
+
return Object.keys(columns).find((name) => STATE_NAMES.includes(name) && enumOf(columns[name]).length >= 3) ?? null;
|
|
12779
|
+
}
|
|
12780
|
+
function realValues(param) {
|
|
12781
|
+
return Array.isArray(param?.values) ? param.values.map(String).filter((value) => !CATCH_ALL.has(value.toLowerCase())) : [];
|
|
12782
|
+
}
|
|
12783
|
+
function ownPages(contract) {
|
|
12784
|
+
return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true && named4(page?.path));
|
|
12785
|
+
}
|
|
12786
|
+
function folderParams(contract) {
|
|
12787
|
+
const enums = [];
|
|
12788
|
+
for (const [table, spec] of Object.entries(tablesOf(contract))) {
|
|
12789
|
+
const columns = isPlainObject5(spec?.columns) ? spec.columns : {};
|
|
12790
|
+
for (const name of STATE_NAMES) if (enumOf(columns[name]).length >= 3) enums.push({ where: `${table}.${name}`, values: new Set(enumOf(columns[name])) });
|
|
12791
|
+
}
|
|
12792
|
+
const out = [];
|
|
12793
|
+
for (const page of ownPages(contract)) {
|
|
12794
|
+
for (const param of Array.isArray(page.params) ? page.params : []) {
|
|
12795
|
+
if (!PICK_KINDS.has(param?.kind) || !named4(param?.name)) continue;
|
|
12796
|
+
const values = realValues(param);
|
|
12797
|
+
if (STATE_NAMES.includes(param.name) && (values.length >= 3 || !Array.isArray(param.values))) {
|
|
12798
|
+
out.push({ page, param, values, why: `named "${param.name}"` });
|
|
12799
|
+
continue;
|
|
12800
|
+
}
|
|
12801
|
+
if (values.length < 3) continue;
|
|
12802
|
+
const match = enums.find((one) => values.every((value) => one.values.has(value)));
|
|
12803
|
+
if (match !== void 0) out.push({ page, param, values, why: `its values are the enum of ${match.where}` });
|
|
12804
|
+
}
|
|
12805
|
+
}
|
|
12806
|
+
return out;
|
|
12807
|
+
}
|
|
12808
|
+
function tableOf2(contract, element) {
|
|
12809
|
+
const props = isPlainObject5(element?.props) ? element.props : {};
|
|
12810
|
+
if (named4(props.entity)) return props.entity;
|
|
12811
|
+
const source = props.source;
|
|
12812
|
+
if (!named4(source)) return null;
|
|
12813
|
+
const handler = contract?.data?.app?.handlers?.[source];
|
|
12814
|
+
if (isPlainObject5(handler) && named4(handler.table)) return handler.table;
|
|
12815
|
+
const read = contract?.data?.sources?.[source];
|
|
12816
|
+
if (isPlainObject5(read?.from) && named4(read.from.table)) return read.from.table;
|
|
12817
|
+
return null;
|
|
12818
|
+
}
|
|
12819
|
+
function listingOn(contract, page, test) {
|
|
12820
|
+
const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
|
|
12821
|
+
for (const [uid, element] of Object.entries(elements)) {
|
|
12822
|
+
if (element?.page !== page.path || !ROW_TYPES.has(element.type)) continue;
|
|
12823
|
+
const table = tableOf2(contract, element);
|
|
12824
|
+
if (table !== null && test(table)) return [uid, element, table];
|
|
12825
|
+
}
|
|
12826
|
+
return null;
|
|
12827
|
+
}
|
|
12828
|
+
function stateCountColumn(spec) {
|
|
12829
|
+
if (!isPlainObject5(spec) || NOT_COUNTS.some((key) => spec[key] !== void 0)) return null;
|
|
12830
|
+
const where2 = spec.count === true ? isPlainObject5(spec.where) ? spec.where : {} : spec.count === void 0 ? spec : null;
|
|
12831
|
+
if (where2 === null) return null;
|
|
12832
|
+
return Object.keys(where2).map((key) => key.split("__")[0]).find((column) => STATE_NAMES.includes(column)) ?? null;
|
|
12833
|
+
}
|
|
12834
|
+
function stateCounts(handler) {
|
|
12835
|
+
const out = {};
|
|
12836
|
+
for (const [measure, spec] of Object.entries(isPlainObject5(handler?.measures) ? handler.measures : {})) {
|
|
12837
|
+
const column = stateCountColumn(spec);
|
|
12838
|
+
if (column !== null) out[measure] = column;
|
|
12839
|
+
}
|
|
12840
|
+
return out;
|
|
12841
|
+
}
|
|
12842
|
+
function pageReads(contract, page, name) {
|
|
12843
|
+
const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
|
|
12844
|
+
if (page?.nav?.counts?.source === name) return true;
|
|
12845
|
+
return Object.values(elements).some((element) => element?.page === page.path && element.props?.source === name);
|
|
12846
|
+
}
|
|
12847
|
+
function shellExpectedProblems(contract) {
|
|
12848
|
+
if (!isAppContract(contract)) return [];
|
|
12849
|
+
const shell = isPlainObject5(contract?.app?.shell) ? contract.app.shell : {};
|
|
12850
|
+
if (shell.nav === "rail") return [];
|
|
12851
|
+
const out = [];
|
|
12852
|
+
const say = (page, param, where2, message, fix) => out.push({ rule: "L8", code: "shell-expected", page, param, where: where2, message, fix });
|
|
12853
|
+
const has = shell.nav === void 0 || shell.nav === null ? "states no app.shell" : `states app.shell.nav ${JSON.stringify(shell.nav)}`;
|
|
12854
|
+
const pages = ownPages(contract);
|
|
12855
|
+
if (pages.length > 4) say(null, null, "pages", `the app has ${pages.length} pages and ${has}, so its screens are a row of tabs too long to scan; an app this size is a rail`, PAGES_FIX);
|
|
12856
|
+
const named_ = /* @__PURE__ */ new Set();
|
|
12857
|
+
for (const { page, param, values, why } of folderParams(contract)) {
|
|
12858
|
+
named_.add(page.path);
|
|
12859
|
+
const listed = values.length > 0 ? ` with ${values.length} values (${values.map((one) => JSON.stringify(one)).join(", ")})` : " (its values from elsewhere)";
|
|
12860
|
+
say(page.path, param.name, `${page.path} params.${param.name}`, `${page.path} declares ${param.kind} param "${param.name}" (${why})${listed} and the app ${has}, so its folders are a dropdown with no counts; they are the rail's entries`, VALUES_FIX);
|
|
12861
|
+
}
|
|
12862
|
+
const handlers = isPlainObject5(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
|
|
12863
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
12864
|
+
if (handler?.role !== "summary" || !named4(handler.table)) continue;
|
|
12865
|
+
const column = stateColumn(contract, handler.table);
|
|
12866
|
+
const counts = Object.fromEntries(Object.entries(stateCounts(handler)).filter(([, by]) => by === column));
|
|
12867
|
+
if (column === null || Object.keys(counts).length < 3) continue;
|
|
12868
|
+
for (const page of pages) {
|
|
12869
|
+
if (named_.has(page.path) || !pageReads(contract, page, name)) continue;
|
|
12870
|
+
const listing = listingOn(contract, page, (table) => table === handler.table);
|
|
12871
|
+
if (listing === null) continue;
|
|
12872
|
+
const columns = [column];
|
|
12873
|
+
say(
|
|
12874
|
+
page.path,
|
|
12875
|
+
null,
|
|
12876
|
+
`${page.path} ${name}`,
|
|
12877
|
+
`${page.path} reads the summary "${name}", whose measures count ${handler.table} by ${columns.join(", ")} (${Object.keys(counts).map((one) => JSON.stringify(one)).join(", ")}), beside ${nameOf3(listing[0], listing[1])} listing ${handler.table}, and the app ${has}: those counts are folders, and folders are the rail's entries`,
|
|
12878
|
+
COUNTS_FIX
|
|
12879
|
+
);
|
|
12880
|
+
}
|
|
12881
|
+
}
|
|
12882
|
+
return out;
|
|
12883
|
+
}
|
|
12884
|
+
function kpiStripWorkflowProblems(contract) {
|
|
12885
|
+
if (!isAppContract(contract)) return [];
|
|
12886
|
+
const elements = isPlainObject5(contract?.elements) ? contract.elements : {};
|
|
12887
|
+
const handlers = isPlainObject5(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
|
|
12888
|
+
const folderPages = /* @__PURE__ */ new Map();
|
|
12889
|
+
for (const { page, param } of folderParams(contract)) if (!folderPages.has(page.path)) folderPages.set(page.path, param.name);
|
|
12890
|
+
const out = [];
|
|
12891
|
+
for (const page of ownPages(contract)) {
|
|
12892
|
+
const listing = listingOn(contract, page, (table) => stateColumn(contract, table) !== null);
|
|
12893
|
+
const workflow = listing !== null ? `${nameOf3(listing[0], listing[1])} lists ${listing[2]}, whose ${stateColumn(contract, listing[2])} column is a set of states` : folderPages.has(page.path) ? `the page declares the folder-like param "${folderPages.get(page.path)}"` : null;
|
|
12894
|
+
if (workflow === null) continue;
|
|
12895
|
+
const banded = bandsOf(contract, page);
|
|
12896
|
+
const inBand = new Set(banded.flatMap((band) => band.roots));
|
|
12897
|
+
const loose = rootsOf(contract, page).filter((uid) => !inBand.has(uid));
|
|
12898
|
+
for (const { section, roots } of [...banded, { section: null, roots: loose }]) {
|
|
12899
|
+
const stats = roots.filter((uid) => elements[uid]?.type === "stat");
|
|
12900
|
+
if (stats.length < 2) continue;
|
|
12901
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
12902
|
+
for (const uid of stats) {
|
|
12903
|
+
const source = elements[uid]?.props?.source;
|
|
12904
|
+
if (!named4(source) || handlers[source]?.role !== "summary") continue;
|
|
12905
|
+
if (!bySource.has(source)) bySource.set(source, []);
|
|
12906
|
+
bySource.get(source).push(uid);
|
|
12907
|
+
}
|
|
12908
|
+
for (const [source, group] of bySource) {
|
|
12909
|
+
if (group.length < 2) continue;
|
|
12910
|
+
const measures = handlers[source].measures;
|
|
12911
|
+
const known = isPlainObject5(measures) && group.every((uid) => Object.prototype.hasOwnProperty.call(measures, elements[uid]?.props?.measure));
|
|
12912
|
+
const counts = stateCounts(handlers[source]);
|
|
12913
|
+
if (known ? !group.every((uid) => counts[elements[uid].props.measure] !== void 0) : group.length !== stats.length) continue;
|
|
12914
|
+
const where2 = section === null ? page.path : `band "${section.id}" on ${page.path}`;
|
|
12915
|
+
const shape = section?.layout === "kpi_strip" ? "is a kpi_strip of" : "holds a row of";
|
|
12916
|
+
const what = known ? `${group.length} counts of rows by ${[...new Set(group.map((uid) => counts[elements[uid].props.measure]))].join(", ")}` : `${group.length} counts`;
|
|
12917
|
+
out.push({
|
|
12918
|
+
rule: "L9",
|
|
12919
|
+
code: "kpi-strip-on-workflow",
|
|
12920
|
+
page: page.path,
|
|
12921
|
+
section: section?.id ?? null,
|
|
12922
|
+
where: where2,
|
|
12923
|
+
message: `${where2} ${shape} ${what} from the summary "${source}", and ${workflow}: folder or state counts belong on the rail, not in a KPI strip (workflow.md \xA78)`,
|
|
12924
|
+
fix: VALUES_FIX
|
|
12925
|
+
});
|
|
12926
|
+
break;
|
|
12927
|
+
}
|
|
12928
|
+
}
|
|
12929
|
+
}
|
|
12930
|
+
return out;
|
|
12931
|
+
}
|
|
12481
12932
|
|
|
12482
12933
|
// ../flows/insights-builder/kit/contract/platformBadges.mjs
|
|
12483
12934
|
var PLATFORM_BADGE_COLOURS = Object.freeze([
|
|
@@ -13629,8 +14080,9 @@ function compileApp(contract, { kit = null, runtime = RUNTIME, extra = {} } = {}
|
|
|
13629
14080
|
const warnings = [...sourceFindings, ...widgetFindings].filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one);
|
|
13630
14081
|
problems.push(...widgetFindings.filter((one) => !one.advisory));
|
|
13631
14082
|
if (tree.problems.length === 0) problems.push(...partProblems(declared, tree, handlers, sources, contract?.pages, model, { "dr-components": appComponents }));
|
|
13632
|
-
|
|
13633
|
-
|
|
14083
|
+
const backendFacts = inferredBackendHandlers(contract?.backend?.handlers, handlers, extra);
|
|
14084
|
+
if (tree.problems.length === 0) problems.push(...workflowProblems(declared, tree, handlers, sources, backendFacts, model));
|
|
14085
|
+
const selectedFindings = tree.problems.length === 0 ? selectedProblems(declared, tree, handlers, sources, backendFacts, contract?.pages) : [];
|
|
13634
14086
|
problems.push(...selectedFindings.filter((one) => !one.advisory));
|
|
13635
14087
|
warnings.push(...selectedFindings.filter((one) => one.advisory).map(({ advisory: _advisory, ...one }) => one));
|
|
13636
14088
|
if (problems.length > 0) return { files: {}, problems, contract, warnings };
|
|
@@ -14684,7 +15136,7 @@ function workflowProblems(elements, tree, handlers = {}, sources = {}, backendHa
|
|
|
14684
15136
|
const say = (code, pointer2, message) => problems.push({ code, pointer: pointer2, message: `${type} "${props.id ?? uid}" ${message}` });
|
|
14685
15137
|
const readsFrom = (source, pointer2, what) => {
|
|
14686
15138
|
if (!stated(source)) say("part_source_shape", pointer2, `names no source; a ${type} reads ${what} from a reader \u2014 a ${either3(WORKFLOW_READERS)} handler, or a data.sources entry`);
|
|
14687
|
-
else if (!reader(source)) say("part_source_shape", pointer2, `reads "${source}", which ${roleOf(source) === "custom" || roleOf(source) === void 0 ? `is no reader (a custom handler is one only when its backend.handlers
|
|
15139
|
+
else if (!reader(source)) say("part_source_shape", pointer2, `reads "${source}", which ${roleOf(source) === "custom" || roleOf(source) === void 0 ? `is no reader (a custom handler is one only when its verbs \u2014 stated in backend.handlers, else read off handlers/${source}.py, a guess you settle by stating them \u2014 are all query, get or aggregate${Array.isArray(verbsOf(source)) ? `; ${backendHandlers?.[source]?.inferred === true ? "its file reads as" : "it states"} ${JSON.stringify(verbsOf(source))}` : ""})` : `has role ${roleOf(source)}`}; a ${type} reads ${what} from a list, inbox, history or summary handler, a data.sources entry, or such a custom handler, and never calls a writer`);
|
|
14688
15140
|
};
|
|
14689
15141
|
if (type !== "split_pane" && typed.bind === "selected" && !bindReaches(uid)) {
|
|
14690
15142
|
say("part_bind", `${at}/props/props/bind`, `binds the selected row where no row is ever picked for it: put it at the page root, in a root split_pane or panel, or inside a form or detail with bind: "selected"`);
|
|
@@ -14767,6 +15219,21 @@ function workflowProblems(elements, tree, handlers = {}, sources = {}, backendHa
|
|
|
14767
15219
|
return problems;
|
|
14768
15220
|
}
|
|
14769
15221
|
var ROW_READERS = ["list", "inbox", "history", "summary"];
|
|
15222
|
+
function inferredBackendHandlers(stated2, handlers = {}, extra = {}) {
|
|
15223
|
+
const out = isPlainObject6(stated2) ? { ...stated2 } : {};
|
|
15224
|
+
for (const [name, row] of Object.entries(isPlainObject6(handlers) ? handlers : {})) {
|
|
15225
|
+
if (row?.role !== "custom") continue;
|
|
15226
|
+
if (isPlainObject6(out[name]) && Array.isArray(out[name].verbs)) continue;
|
|
15227
|
+
const source = extra?.[`handlers/${name}.py`];
|
|
15228
|
+
if (typeof source !== "string") continue;
|
|
15229
|
+
const verbs = handlerVerbs(source);
|
|
15230
|
+
const hidden = !verbs.some((verb) => INFER_WRITES.includes(verb)) && WRITE_ANYWHERE.test(source);
|
|
15231
|
+
out[name] = { ...isPlainObject6(out[name]) ? out[name] : {}, verbs: hidden ? [...verbs, "update"] : verbs, inferred: true };
|
|
15232
|
+
}
|
|
15233
|
+
return out;
|
|
15234
|
+
}
|
|
15235
|
+
var INFER_WRITES = ["insert", "update", "delete"];
|
|
15236
|
+
var WRITE_ANYWHERE = /\.\s*(?:insert|update|delete)\s*\(/;
|
|
14770
15237
|
var READ_VERBS = ["query", "get", "aggregate"];
|
|
14771
15238
|
var ROOT_HOLDERS = ["panel", "split_pane"];
|
|
14772
15239
|
var BOUND_TYPES = ["form", "detail"];
|
|
@@ -14858,7 +15325,7 @@ function selectedProblems(elements, tree, handlers = {}, sources = {}, backendHa
|
|
|
14858
15325
|
"bind_source",
|
|
14859
15326
|
uid,
|
|
14860
15327
|
"/props/source",
|
|
14861
|
-
`${element.type} "${nameOf5(uid)}" binds the selected row and reads it from "${source}", ${role !== void 0 && role !== "custom" ? `a ${role} handler` :
|
|
15328
|
+
`${element.type} "${nameOf5(uid)}" binds the selected row and reads it from "${source}", ${role !== void 0 && role !== "custom" ? `a ${role} handler` : `a custom handler whose verbs (${backendHandlers?.[source]?.inferred === true ? `read off handlers/${source}.py \u2014 state them in backend.handlers if that guess is wrong` : "stated in backend.handlers"}) are not all reads`}; a pick would call a writer, so the runtime never calls it and draws an error \u2014 read through a list, inbox, history or summary handler, a data.sources entry, or a custom handler whose verbs are all query, get or aggregate`
|
|
14862
15329
|
);
|
|
14863
15330
|
} else if (named5(props.entity) && named5(tableOfSource(source, handlers, sources)) && tableOfSource(source, handlers, sources) !== props.entity) {
|
|
14864
15331
|
say("bind_source", uid, "/props/source", `${element.type} "${nameOf5(uid)}" is over "${props.entity}" and reads the selected row from "${source}", whose rows are "${tableOfSource(source, handlers, sources)}"; it would show (and a Save would post) another table's row \u2014 read the row from a reader over "${props.entity}"`);
|
|
@@ -19145,7 +19612,7 @@ function interpret(status, body, path3, contract) {
|
|
|
19145
19612
|
}
|
|
19146
19613
|
function fromRefusal(status, detail, contract) {
|
|
19147
19614
|
const refused = `the push would be refused (${String(status)})`;
|
|
19148
|
-
const tables =
|
|
19615
|
+
const tables = tablesOf2(contract);
|
|
19149
19616
|
const conflicts = conflictTables(detail);
|
|
19150
19617
|
if (conflicts !== void 0) {
|
|
19151
19618
|
return conflicts.map(([table, owners]) => {
|
|
@@ -19229,7 +19696,7 @@ async function stage(dir, scratch, files) {
|
|
|
19229
19696
|
}
|
|
19230
19697
|
return null;
|
|
19231
19698
|
}
|
|
19232
|
-
function
|
|
19699
|
+
function tablesOf2(contract) {
|
|
19233
19700
|
const out = /* @__PURE__ */ new Map();
|
|
19234
19701
|
const app = isObj3(contract) && isObj3(contract["app"]) ? contract["app"] : {};
|
|
19235
19702
|
const model = isObj3(app["data_model"]) ? app["data_model"] : {};
|
|
@@ -22619,6 +23086,10 @@ function partCalls2(element) {
|
|
|
22619
23086
|
for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
|
|
22620
23087
|
if (action && typeof action === "object" && named6(action.call)) out.push(action.call);
|
|
22621
23088
|
}
|
|
23089
|
+
const bulk = typed.selection && typeof typed.selection === "object" ? typed.selection.bulk_actions : void 0;
|
|
23090
|
+
for (const action of Array.isArray(bulk) ? bulk : []) {
|
|
23091
|
+
if (action && typeof action === "object" && named6(action.call)) out.push(action.call);
|
|
23092
|
+
}
|
|
22622
23093
|
for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
|
|
22623
23094
|
if (column && typeof column === "object" && column.cell === "toggle" && named6(column.call)) out.push(column.call);
|
|
22624
23095
|
}
|
|
@@ -23327,9 +23798,7 @@ function backendSection2(app, manifest, pages, elements) {
|
|
|
23327
23798
|
signature_ok: signatureOk2(source),
|
|
23328
23799
|
reads_ctx_role: CTX_ROLE2.test(source),
|
|
23329
23800
|
tables_touched: [...new Set([...source.matchAll(TABLE_CALL2)].map((match) => match[2]))].sort(),
|
|
23330
|
-
verbs:
|
|
23331
|
-
(verb) => TABLE_ONLY_VERBS2.has(verb) ? callsOnTable2(source, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(source)
|
|
23332
|
-
),
|
|
23801
|
+
verbs: handlerVerbs2(source),
|
|
23333
23802
|
referenced_by_page: isReferenced3(handler.name, scriptText) || attributed.has(handler.name)
|
|
23334
23803
|
};
|
|
23335
23804
|
}
|
|
@@ -23348,6 +23817,10 @@ function backendSection2(app, manifest, pages, elements) {
|
|
|
23348
23817
|
invariants: invariants2(model, roles, handlers)
|
|
23349
23818
|
};
|
|
23350
23819
|
}
|
|
23820
|
+
function handlerVerbs2(source) {
|
|
23821
|
+
const text3 = typeof source === "string" ? source : "";
|
|
23822
|
+
return DATA_VERBS2.filter((verb) => TABLE_ONLY_VERBS2.has(verb) ? callsOnTable2(text3, verb) : new RegExp(`\\.\\s*${verb}\\s*\\(`).test(text3));
|
|
23823
|
+
}
|
|
23351
23824
|
function callsOnTable2(source, verb) {
|
|
23352
23825
|
const holders = [TABLE_INLINE2];
|
|
23353
23826
|
for (const match of source.matchAll(TABLE_LOCAL2)) holders.push(match[1]);
|
|
@@ -24099,6 +24572,9 @@ function payloadProducers2(contract, { moduleSource = () => void 0, pageSource =
|
|
|
24099
24572
|
for (const action of Array.isArray(typed.row_actions) ? typed.row_actions : []) {
|
|
24100
24573
|
if (isPlainObject11(action) && named7(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["id", "row"] });
|
|
24101
24574
|
}
|
|
24575
|
+
for (const action of isPlainObject11(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
|
|
24576
|
+
if (isPlainObject11(action) && named7(action.call)) add(action.call, { page, uid, what: "data_grid", keys: ["ids"] });
|
|
24577
|
+
}
|
|
24102
24578
|
for (const column of Array.isArray(typed.columns) ? typed.columns : []) {
|
|
24103
24579
|
if (isPlainObject11(column) && column.cell === "toggle" && named7(column.call)) add(column.call, { page, uid, what: "data_grid", keys: ["id", "row", "field", "value"] });
|
|
24104
24580
|
}
|
|
@@ -25194,6 +25670,181 @@ function railReachProblems2(contract) {
|
|
|
25194
25670
|
}
|
|
25195
25671
|
return out;
|
|
25196
25672
|
}
|
|
25673
|
+
var STATE_NAMES2 = ["folder", "status", "stage", "state", "queue"];
|
|
25674
|
+
var CATCH_ALL2 = /* @__PURE__ */ new Set(["", "all", "any", "*", "everything"]);
|
|
25675
|
+
var PICK_KINDS2 = /* @__PURE__ */ new Set(["select", "multi"]);
|
|
25676
|
+
var ROW_TYPES2 = /* @__PURE__ */ new Set(["data_grid", "list", "queue"]);
|
|
25677
|
+
var NOT_COUNTS2 = ["sum", "avg", "avg_days", "count_events"];
|
|
25678
|
+
var VALUES_FIX2 = "state `app.shell` with `nav: rail` and one entry per value, counts from a `summary` handler; look.md rule 1";
|
|
25679
|
+
var PAGES_FIX2 = "state `app.shell` with `nav: rail`; with no `rail.entries` the rail lists the pages; look.md rule 1";
|
|
25680
|
+
var COUNTS_FIX2 = "state `app.shell` with `nav: rail` and one entry per state, each counting a measure of that summary (`pages[].nav.counts.source`); look.md rule 1";
|
|
25681
|
+
function isAppContract2(contract) {
|
|
25682
|
+
const kind = contract?.app?.kind;
|
|
25683
|
+
return kind !== "dashboard" && (kind === "app" || isPlainObject13(contract?.data?.app));
|
|
25684
|
+
}
|
|
25685
|
+
function tablesOf3(contract) {
|
|
25686
|
+
const tables = contract?.app?.data_model?.tables;
|
|
25687
|
+
return isPlainObject13(tables) ? tables : {};
|
|
25688
|
+
}
|
|
25689
|
+
function enumOf2(column) {
|
|
25690
|
+
return Array.isArray(column?.enum) ? column.enum.filter((one) => typeof one === "string") : [];
|
|
25691
|
+
}
|
|
25692
|
+
function stateColumn2(contract, table) {
|
|
25693
|
+
const columns = isPlainObject13(tablesOf3(contract)[table]?.columns) ? tablesOf3(contract)[table].columns : {};
|
|
25694
|
+
return Object.keys(columns).find((name) => STATE_NAMES2.includes(name) && enumOf2(columns[name]).length >= 3) ?? null;
|
|
25695
|
+
}
|
|
25696
|
+
function realValues2(param) {
|
|
25697
|
+
return Array.isArray(param?.values) ? param.values.map(String).filter((value) => !CATCH_ALL2.has(value.toLowerCase())) : [];
|
|
25698
|
+
}
|
|
25699
|
+
function ownPages2(contract) {
|
|
25700
|
+
return (Array.isArray(contract?.pages) ? contract.pages : []).filter((page) => page?.owned !== true && named9(page?.path));
|
|
25701
|
+
}
|
|
25702
|
+
function folderParams2(contract) {
|
|
25703
|
+
const enums = [];
|
|
25704
|
+
for (const [table, spec] of Object.entries(tablesOf3(contract))) {
|
|
25705
|
+
const columns = isPlainObject13(spec?.columns) ? spec.columns : {};
|
|
25706
|
+
for (const name of STATE_NAMES2) if (enumOf2(columns[name]).length >= 3) enums.push({ where: `${table}.${name}`, values: new Set(enumOf2(columns[name])) });
|
|
25707
|
+
}
|
|
25708
|
+
const out = [];
|
|
25709
|
+
for (const page of ownPages2(contract)) {
|
|
25710
|
+
for (const param of Array.isArray(page.params) ? page.params : []) {
|
|
25711
|
+
if (!PICK_KINDS2.has(param?.kind) || !named9(param?.name)) continue;
|
|
25712
|
+
const values = realValues2(param);
|
|
25713
|
+
if (STATE_NAMES2.includes(param.name) && (values.length >= 3 || !Array.isArray(param.values))) {
|
|
25714
|
+
out.push({ page, param, values, why: `named "${param.name}"` });
|
|
25715
|
+
continue;
|
|
25716
|
+
}
|
|
25717
|
+
if (values.length < 3) continue;
|
|
25718
|
+
const match = enums.find((one) => values.every((value) => one.values.has(value)));
|
|
25719
|
+
if (match !== void 0) out.push({ page, param, values, why: `its values are the enum of ${match.where}` });
|
|
25720
|
+
}
|
|
25721
|
+
}
|
|
25722
|
+
return out;
|
|
25723
|
+
}
|
|
25724
|
+
function tableOf3(contract, element) {
|
|
25725
|
+
const props = isPlainObject13(element?.props) ? element.props : {};
|
|
25726
|
+
if (named9(props.entity)) return props.entity;
|
|
25727
|
+
const source = props.source;
|
|
25728
|
+
if (!named9(source)) return null;
|
|
25729
|
+
const handler = contract?.data?.app?.handlers?.[source];
|
|
25730
|
+
if (isPlainObject13(handler) && named9(handler.table)) return handler.table;
|
|
25731
|
+
const read = contract?.data?.sources?.[source];
|
|
25732
|
+
if (isPlainObject13(read?.from) && named9(read.from.table)) return read.from.table;
|
|
25733
|
+
return null;
|
|
25734
|
+
}
|
|
25735
|
+
function listingOn2(contract, page, test) {
|
|
25736
|
+
const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
|
|
25737
|
+
for (const [uid, element] of Object.entries(elements)) {
|
|
25738
|
+
if (element?.page !== page.path || !ROW_TYPES2.has(element.type)) continue;
|
|
25739
|
+
const table = tableOf3(contract, element);
|
|
25740
|
+
if (table !== null && test(table)) return [uid, element, table];
|
|
25741
|
+
}
|
|
25742
|
+
return null;
|
|
25743
|
+
}
|
|
25744
|
+
function stateCountColumn2(spec) {
|
|
25745
|
+
if (!isPlainObject13(spec) || NOT_COUNTS2.some((key) => spec[key] !== void 0)) return null;
|
|
25746
|
+
const where2 = spec.count === true ? isPlainObject13(spec.where) ? spec.where : {} : spec.count === void 0 ? spec : null;
|
|
25747
|
+
if (where2 === null) return null;
|
|
25748
|
+
return Object.keys(where2).map((key) => key.split("__")[0]).find((column) => STATE_NAMES2.includes(column)) ?? null;
|
|
25749
|
+
}
|
|
25750
|
+
function stateCounts2(handler) {
|
|
25751
|
+
const out = {};
|
|
25752
|
+
for (const [measure, spec] of Object.entries(isPlainObject13(handler?.measures) ? handler.measures : {})) {
|
|
25753
|
+
const column = stateCountColumn2(spec);
|
|
25754
|
+
if (column !== null) out[measure] = column;
|
|
25755
|
+
}
|
|
25756
|
+
return out;
|
|
25757
|
+
}
|
|
25758
|
+
function pageReads2(contract, page, name) {
|
|
25759
|
+
const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
|
|
25760
|
+
if (page?.nav?.counts?.source === name) return true;
|
|
25761
|
+
return Object.values(elements).some((element) => element?.page === page.path && element.props?.source === name);
|
|
25762
|
+
}
|
|
25763
|
+
function shellExpectedProblems2(contract) {
|
|
25764
|
+
if (!isAppContract2(contract)) return [];
|
|
25765
|
+
const shell = isPlainObject13(contract?.app?.shell) ? contract.app.shell : {};
|
|
25766
|
+
if (shell.nav === "rail") return [];
|
|
25767
|
+
const out = [];
|
|
25768
|
+
const say = (page, param, where2, message, fix) => out.push({ rule: "L8", code: "shell-expected", page, param, where: where2, message, fix });
|
|
25769
|
+
const has = shell.nav === void 0 || shell.nav === null ? "states no app.shell" : `states app.shell.nav ${JSON.stringify(shell.nav)}`;
|
|
25770
|
+
const pages = ownPages2(contract);
|
|
25771
|
+
if (pages.length > 4) say(null, null, "pages", `the app has ${pages.length} pages and ${has}, so its screens are a row of tabs too long to scan; an app this size is a rail`, PAGES_FIX2);
|
|
25772
|
+
const named_ = /* @__PURE__ */ new Set();
|
|
25773
|
+
for (const { page, param, values, why } of folderParams2(contract)) {
|
|
25774
|
+
named_.add(page.path);
|
|
25775
|
+
const listed = values.length > 0 ? ` with ${values.length} values (${values.map((one) => JSON.stringify(one)).join(", ")})` : " (its values from elsewhere)";
|
|
25776
|
+
say(page.path, param.name, `${page.path} params.${param.name}`, `${page.path} declares ${param.kind} param "${param.name}" (${why})${listed} and the app ${has}, so its folders are a dropdown with no counts; they are the rail's entries`, VALUES_FIX2);
|
|
25777
|
+
}
|
|
25778
|
+
const handlers = isPlainObject13(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
|
|
25779
|
+
for (const [name, handler] of Object.entries(handlers)) {
|
|
25780
|
+
if (handler?.role !== "summary" || !named9(handler.table)) continue;
|
|
25781
|
+
const column = stateColumn2(contract, handler.table);
|
|
25782
|
+
const counts = Object.fromEntries(Object.entries(stateCounts2(handler)).filter(([, by]) => by === column));
|
|
25783
|
+
if (column === null || Object.keys(counts).length < 3) continue;
|
|
25784
|
+
for (const page of pages) {
|
|
25785
|
+
if (named_.has(page.path) || !pageReads2(contract, page, name)) continue;
|
|
25786
|
+
const listing = listingOn2(contract, page, (table) => table === handler.table);
|
|
25787
|
+
if (listing === null) continue;
|
|
25788
|
+
const columns = [column];
|
|
25789
|
+
say(
|
|
25790
|
+
page.path,
|
|
25791
|
+
null,
|
|
25792
|
+
`${page.path} ${name}`,
|
|
25793
|
+
`${page.path} reads the summary "${name}", whose measures count ${handler.table} by ${columns.join(", ")} (${Object.keys(counts).map((one) => JSON.stringify(one)).join(", ")}), beside ${nameOf4(listing[0], listing[1])} listing ${handler.table}, and the app ${has}: those counts are folders, and folders are the rail's entries`,
|
|
25794
|
+
COUNTS_FIX2
|
|
25795
|
+
);
|
|
25796
|
+
}
|
|
25797
|
+
}
|
|
25798
|
+
return out;
|
|
25799
|
+
}
|
|
25800
|
+
function kpiStripWorkflowProblems2(contract) {
|
|
25801
|
+
if (!isAppContract2(contract)) return [];
|
|
25802
|
+
const elements = isPlainObject13(contract?.elements) ? contract.elements : {};
|
|
25803
|
+
const handlers = isPlainObject13(contract?.data?.app?.handlers) ? contract.data.app.handlers : {};
|
|
25804
|
+
const folderPages = /* @__PURE__ */ new Map();
|
|
25805
|
+
for (const { page, param } of folderParams2(contract)) if (!folderPages.has(page.path)) folderPages.set(page.path, param.name);
|
|
25806
|
+
const out = [];
|
|
25807
|
+
for (const page of ownPages2(contract)) {
|
|
25808
|
+
const listing = listingOn2(contract, page, (table) => stateColumn2(contract, table) !== null);
|
|
25809
|
+
const workflow = listing !== null ? `${nameOf4(listing[0], listing[1])} lists ${listing[2]}, whose ${stateColumn2(contract, listing[2])} column is a set of states` : folderPages.has(page.path) ? `the page declares the folder-like param "${folderPages.get(page.path)}"` : null;
|
|
25810
|
+
if (workflow === null) continue;
|
|
25811
|
+
const banded = bandsOf3(contract, page);
|
|
25812
|
+
const inBand = new Set(banded.flatMap((band) => band.roots));
|
|
25813
|
+
const loose = rootsOf3(contract, page).filter((uid) => !inBand.has(uid));
|
|
25814
|
+
for (const { section, roots } of [...banded, { section: null, roots: loose }]) {
|
|
25815
|
+
const stats = roots.filter((uid) => elements[uid]?.type === "stat");
|
|
25816
|
+
if (stats.length < 2) continue;
|
|
25817
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
25818
|
+
for (const uid of stats) {
|
|
25819
|
+
const source = elements[uid]?.props?.source;
|
|
25820
|
+
if (!named9(source) || handlers[source]?.role !== "summary") continue;
|
|
25821
|
+
if (!bySource.has(source)) bySource.set(source, []);
|
|
25822
|
+
bySource.get(source).push(uid);
|
|
25823
|
+
}
|
|
25824
|
+
for (const [source, group] of bySource) {
|
|
25825
|
+
if (group.length < 2) continue;
|
|
25826
|
+
const measures = handlers[source].measures;
|
|
25827
|
+
const known = isPlainObject13(measures) && group.every((uid) => Object.prototype.hasOwnProperty.call(measures, elements[uid]?.props?.measure));
|
|
25828
|
+
const counts = stateCounts2(handlers[source]);
|
|
25829
|
+
if (known ? !group.every((uid) => counts[elements[uid].props.measure] !== void 0) : group.length !== stats.length) continue;
|
|
25830
|
+
const where2 = section === null ? page.path : `band "${section.id}" on ${page.path}`;
|
|
25831
|
+
const shape = section?.layout === "kpi_strip" ? "is a kpi_strip of" : "holds a row of";
|
|
25832
|
+
const what = known ? `${group.length} counts of rows by ${[...new Set(group.map((uid) => counts[elements[uid].props.measure]))].join(", ")}` : `${group.length} counts`;
|
|
25833
|
+
out.push({
|
|
25834
|
+
rule: "L9",
|
|
25835
|
+
code: "kpi-strip-on-workflow",
|
|
25836
|
+
page: page.path,
|
|
25837
|
+
section: section?.id ?? null,
|
|
25838
|
+
where: where2,
|
|
25839
|
+
message: `${where2} ${shape} ${what} from the summary "${source}", and ${workflow}: folder or state counts belong on the rail, not in a KPI strip (workflow.md \xA78)`,
|
|
25840
|
+
fix: VALUES_FIX2
|
|
25841
|
+
});
|
|
25842
|
+
break;
|
|
25843
|
+
}
|
|
25844
|
+
}
|
|
25845
|
+
}
|
|
25846
|
+
return out;
|
|
25847
|
+
}
|
|
25197
25848
|
|
|
25198
25849
|
// ../flows/appspace-builder/kit/contract/platformPalette.mjs
|
|
25199
25850
|
var PLATFORM_CHART2 = Object.freeze([
|
|
@@ -25415,7 +26066,7 @@ function rgbText2(parts) {
|
|
|
25415
26066
|
|
|
25416
26067
|
// ../flows/appspace-builder/kit/checks/rules/look.mjs
|
|
25417
26068
|
var THEME_FILE8 = "ui/theme.css";
|
|
25418
|
-
var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7"];
|
|
26069
|
+
var RULES3 = ["L1", "L2", "L3", "L4", "L5", "L6", "L7", "L8", "L9"];
|
|
25419
26070
|
function checkLook(app, contract, contractError) {
|
|
25420
26071
|
const view = {
|
|
25421
26072
|
...app,
|
|
@@ -25444,6 +26095,9 @@ function checkLook(app, contract, contractError) {
|
|
|
25444
26095
|
for (const found of railReachProblems2(contract)) {
|
|
25445
26096
|
violations.push({ rule: "L7", where: found.where, message: found.message });
|
|
25446
26097
|
}
|
|
26098
|
+
for (const found of [...shellExpectedProblems2(contract), ...kpiStripWorkflowProblems2(contract)]) {
|
|
26099
|
+
violations.push({ rule: found.rule, where: found.where, message: `${found.code}: ${found.message}; ${found.fix}` });
|
|
26100
|
+
}
|
|
25447
26101
|
return { violations, skipped: [] };
|
|
25448
26102
|
}
|
|
25449
26103
|
|
|
@@ -25628,7 +26282,7 @@ function checkUx(app, contract, contractError) {
|
|
|
25628
26282
|
return out;
|
|
25629
26283
|
};
|
|
25630
26284
|
const nameOf5 = (uid) => said(elements[uid]?.props?.id) ?? said(elements[uid]?.id) ?? uid;
|
|
25631
|
-
const
|
|
26285
|
+
const tableOf4 = (el) => said(el?.props?.entity) ?? said(handlers[el?.props?.source]?.table);
|
|
25632
26286
|
const violations = [];
|
|
25633
26287
|
for (const uid of uids) {
|
|
25634
26288
|
const el = elements[uid];
|
|
@@ -25638,7 +26292,7 @@ function checkUx(app, contract, contractError) {
|
|
|
25638
26292
|
if (!table || handlers[el.props?.action]?.role === "save") continue;
|
|
25639
26293
|
const listing = uids.find((other) => {
|
|
25640
26294
|
const o = elements[other];
|
|
25641
|
-
return o?.page === el.page && ROWS.has(o.type) &&
|
|
26295
|
+
return o?.page === el.page && ROWS.has(o.type) && tableOf4(o) === table && !ancestors(other).some((up) => up.type === "detail");
|
|
25642
26296
|
});
|
|
25643
26297
|
if (!listing) continue;
|
|
25644
26298
|
const label = said(el.props?.label) ?? "New";
|
|
@@ -25655,7 +26309,7 @@ function checkUx(app, contract, contractError) {
|
|
|
25655
26309
|
const el = elements[uid];
|
|
25656
26310
|
if (!ROWS.has(el?.type)) continue;
|
|
25657
26311
|
if (ancestors(uid).some((up) => up.type === "detail")) continue;
|
|
25658
|
-
const table =
|
|
26312
|
+
const table = tableOf4(el);
|
|
25659
26313
|
if (!table || table.startsWith("_") || !isPlainObject14(tables[table]?.columns) || tables[table].singleton === true) continue;
|
|
25660
26314
|
const columns = tables[table].columns;
|
|
25661
26315
|
const pk = Object.keys(columns).find((c) => isPlainObject14(columns[c]) && columns[c].pk === true) ?? "id";
|
|
@@ -25696,7 +26350,7 @@ function checkUx(app, contract, contractError) {
|
|
|
25696
26350
|
for (const uid of uids) {
|
|
25697
26351
|
const el = elements[uid];
|
|
25698
26352
|
if (!ROWS.has(el?.type) || el.type === "data_grid") continue;
|
|
25699
|
-
const table =
|
|
26353
|
+
const table = tableOf4(el);
|
|
25700
26354
|
const columns = isPlainObject14(tables[table]?.columns) ? tables[table].columns : {};
|
|
25701
26355
|
const state = STATE_COLUMNS.find((c) => isPlainObject14(columns[c]) && Array.isArray(columns[c].enum));
|
|
25702
26356
|
const seen = /* @__PURE__ */ new Map();
|
|
@@ -26012,6 +26666,11 @@ function stateCallers2(contract, { moduleSource = () => void 0, handlerSource =
|
|
|
26012
26666
|
const asks = action.confirm === true || named10(action.confirm);
|
|
26013
26667
|
add(action.call, { page, uid, id, what: `row action "${action.label ?? action.call}"`, asks });
|
|
26014
26668
|
}
|
|
26669
|
+
for (const action of isPlainObject15(typed.selection) && Array.isArray(typed.selection.bulk_actions) ? typed.selection.bulk_actions : []) {
|
|
26670
|
+
if (!isPlainObject15(action) || !named10(action.call)) continue;
|
|
26671
|
+
const asks = action.confirm === true || named10(action.confirm);
|
|
26672
|
+
add(action.call, { page, uid, id, what: `bulk action "${action.label ?? action.call}"`, asks });
|
|
26673
|
+
}
|
|
26015
26674
|
break;
|
|
26016
26675
|
case "action":
|
|
26017
26676
|
case "detail": {
|
|
@@ -26305,7 +26964,7 @@ function kitFromDisk2() {
|
|
|
26305
26964
|
}
|
|
26306
26965
|
return kitOnDisk2;
|
|
26307
26966
|
}
|
|
26308
|
-
var ADVISORY = /* @__PURE__ */ new Set(["L6", "L7", "A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25", "A3.26", "A3.27", "A3.28", "A3.29"]);
|
|
26967
|
+
var ADVISORY = /* @__PURE__ */ new Set(["L6", "L7", "L8", "L9", "A1.5", "A1.6", "A2.5", "A2.6", "A3.15", "A3.17", "A3.19", "A3.20", "A3.21", "A3.22", "A3.23", "A3.24", "A3.25", "A3.26", "A3.27", "A3.28", "A3.29"]);
|
|
26309
26968
|
function verify2({ app: appRef }, { extractContract: extractContract2 = extract2, kit } = {}) {
|
|
26310
26969
|
const KIT = kit ?? kitFromDisk2();
|
|
26311
26970
|
const app = loadApp2(resolve12(appRef.dir));
|
|
@@ -30634,7 +31293,7 @@ function auditedOf(model) {
|
|
|
30634
31293
|
if (typeof tables !== "object" || tables === null) return [];
|
|
30635
31294
|
return Object.keys(tables).filter((name) => tables[name]?.audit === true);
|
|
30636
31295
|
}
|
|
30637
|
-
function
|
|
31296
|
+
function tablesOf4(model) {
|
|
30638
31297
|
const tables = model?.tables;
|
|
30639
31298
|
if (typeof tables !== "object" || tables === null) return {};
|
|
30640
31299
|
const out = {};
|
|
@@ -30672,7 +31331,7 @@ function toRemove(rows2, { stage: stage2, cleanUp, keep, wrote = true }) {
|
|
|
30672
31331
|
return rows2.filter((row) => row.recent);
|
|
30673
31332
|
}
|
|
30674
31333
|
async function sweepTestRows(client, appId, opts) {
|
|
30675
|
-
const tables =
|
|
31334
|
+
const tables = tablesOf4(opts.model);
|
|
30676
31335
|
if (Object.keys(tables).length === 0) {
|
|
30677
31336
|
return { checked: false, why: "the app has no tables", left: [], removed: [], errors: [], truncated: [] };
|
|
30678
31337
|
}
|
|
@@ -30930,7 +31589,7 @@ function pushedContract(bundle, pages) {
|
|
|
30930
31589
|
}
|
|
30931
31590
|
return null;
|
|
30932
31591
|
}
|
|
30933
|
-
function
|
|
31592
|
+
function isAppContract3(contract) {
|
|
30934
31593
|
return contract.app?.kind !== "dashboard" && typeof contract.data?.app === "object";
|
|
30935
31594
|
}
|
|
30936
31595
|
var COMPONENTS2 = /* @__PURE__ */ new Set(["form", "list", "detail", "queue", "action", "filter", "stat", "chart", "custom", "widget"]);
|
|
@@ -30959,7 +31618,7 @@ function sectionOf3(element) {
|
|
|
30959
31618
|
return typeof raw === "string" && raw !== "" ? raw : void 0;
|
|
30960
31619
|
}
|
|
30961
31620
|
function contractFindings(contract) {
|
|
30962
|
-
if (!
|
|
31621
|
+
if (!isAppContract3(contract)) return [];
|
|
30963
31622
|
const pages = contract.pages ?? [];
|
|
30964
31623
|
const elements = contract.elements ?? {};
|
|
30965
31624
|
const out = [];
|
|
@@ -31024,7 +31683,7 @@ function contractFindings(contract) {
|
|
|
31024
31683
|
return out;
|
|
31025
31684
|
}
|
|
31026
31685
|
function auditFindings(contract) {
|
|
31027
|
-
if (!
|
|
31686
|
+
if (!isAppContract3(contract)) return [];
|
|
31028
31687
|
const tables = contract.app?.data_model?.tables ?? {};
|
|
31029
31688
|
const handlers = contract.data?.app?.handlers ?? {};
|
|
31030
31689
|
const elements = contract.elements ?? {};
|
|
@@ -31153,7 +31812,7 @@ function renderFindings(runs, contract, seed) {
|
|
|
31153
31812
|
return out;
|
|
31154
31813
|
}
|
|
31155
31814
|
function chartFindings(runs, contract, seed) {
|
|
31156
|
-
if (contract === null || !
|
|
31815
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31157
31816
|
const handlers = contract.data?.app?.handlers ?? {};
|
|
31158
31817
|
const out = [];
|
|
31159
31818
|
const said2 = /* @__PURE__ */ new Set();
|
|
@@ -31249,7 +31908,7 @@ function mapDecisions(handler) {
|
|
|
31249
31908
|
return Object.keys(decision).sort();
|
|
31250
31909
|
}
|
|
31251
31910
|
function refusalFindings(runs, contract) {
|
|
31252
|
-
if (contract === null || !
|
|
31911
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31253
31912
|
const handlers = contract.data?.app?.handlers ?? {};
|
|
31254
31913
|
const byButton = /* @__PURE__ */ new Map();
|
|
31255
31914
|
for (const run of runs) {
|
|
@@ -31645,7 +32304,7 @@ function partsSummary(contract, bundle, handlerCode) {
|
|
|
31645
32304
|
custom_handler_lines: customHandlerLines,
|
|
31646
32305
|
custom_share: components === 0 ? 0 : Math.round(customUi / components * 100)
|
|
31647
32306
|
};
|
|
31648
|
-
if (
|
|
32307
|
+
if (isAppContract3(contract)) {
|
|
31649
32308
|
const files = transformFiles2(contract, handlerCode);
|
|
31650
32309
|
let lines = 0;
|
|
31651
32310
|
for (const text3 of files.values()) if (text3 !== void 0) lines += nonBlankLinesOf(text3);
|
|
@@ -31671,7 +32330,7 @@ function pushedFiles(bundle, handlerCode) {
|
|
|
31671
32330
|
};
|
|
31672
32331
|
}
|
|
31673
32332
|
function payloadFindings(contract, bundle, handlerCode) {
|
|
31674
|
-
if (contract === null || !
|
|
32333
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31675
32334
|
return DECLARES.unproducedPayloadKeys(contract, pushedFiles(bundle, handlerCode)).map((found) => ({
|
|
31676
32335
|
code: "payload-key-unproduced",
|
|
31677
32336
|
where: `${found.pages.join(", ")} \u2192 ${found.handler}`,
|
|
@@ -31681,7 +32340,7 @@ function payloadFindings(contract, bundle, handlerCode) {
|
|
|
31681
32340
|
}));
|
|
31682
32341
|
}
|
|
31683
32342
|
function leverFindings(contract, bundle, handlerCode) {
|
|
31684
|
-
if (contract === null || !
|
|
32343
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31685
32344
|
const files = pushedFiles(bundle, handlerCode);
|
|
31686
32345
|
const finding = (code) => (found) => ({
|
|
31687
32346
|
code,
|
|
@@ -31696,7 +32355,7 @@ function leverFindings(contract, bundle, handlerCode) {
|
|
|
31696
32355
|
];
|
|
31697
32356
|
}
|
|
31698
32357
|
function workflowFindings(contract, bundle, handlerCode) {
|
|
31699
|
-
if (contract === null || !
|
|
32358
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31700
32359
|
const files = pushedFiles(bundle, handlerCode);
|
|
31701
32360
|
return [
|
|
31702
32361
|
// A role that opens the app on a 404: counted, like the unreachable page
|
|
@@ -31755,7 +32414,7 @@ function shown(value) {
|
|
|
31755
32414
|
return typeof value === "number" || typeof value === "boolean" || typeof value === "string" ? String(value) : JSON.stringify(value);
|
|
31756
32415
|
}
|
|
31757
32416
|
function leverStaticFindings(runs, contract, unread = /* @__PURE__ */ new Set()) {
|
|
31758
|
-
if (contract === null || !
|
|
32417
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31759
32418
|
const sources = contract.data?.sources ?? {};
|
|
31760
32419
|
const out = [];
|
|
31761
32420
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -31853,7 +32512,7 @@ function declaresOf(element) {
|
|
|
31853
32512
|
return { calls, shows };
|
|
31854
32513
|
}
|
|
31855
32514
|
function customDeclaresFindings(contract, bundle, runs) {
|
|
31856
|
-
if (contract === null || !
|
|
32515
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31857
32516
|
const out = [];
|
|
31858
32517
|
const said2 = /* @__PURE__ */ new Set();
|
|
31859
32518
|
const undeclared = (uid, page, module, handler, problem5, fix) => {
|
|
@@ -31906,7 +32565,7 @@ function customDeclaresFindings(contract, bundle, runs) {
|
|
|
31906
32565
|
return out;
|
|
31907
32566
|
}
|
|
31908
32567
|
function roleFindings(contract, held) {
|
|
31909
|
-
if (held.own === null || !
|
|
32568
|
+
if (held.own === null || !isAppContract3(contract)) return [];
|
|
31910
32569
|
const elements = contract.elements ?? {};
|
|
31911
32570
|
const parentOf2 = /* @__PURE__ */ new Map();
|
|
31912
32571
|
for (const [uid, element] of Object.entries(elements)) {
|
|
@@ -31951,7 +32610,7 @@ function roleFindings(contract, held) {
|
|
|
31951
32610
|
];
|
|
31952
32611
|
}
|
|
31953
32612
|
function cardTitleFindings(contract) {
|
|
31954
|
-
if (!
|
|
32613
|
+
if (!isAppContract3(contract)) return [];
|
|
31955
32614
|
return LOOK.untitledCards(contract).map((found) => ({
|
|
31956
32615
|
code: "card-untitled",
|
|
31957
32616
|
where: `${found.page} \u2192 ${found.uid}`,
|
|
@@ -31960,8 +32619,19 @@ function cardTitleFindings(contract) {
|
|
|
31960
32619
|
see: SEE.labels
|
|
31961
32620
|
}));
|
|
31962
32621
|
}
|
|
32622
|
+
function shellFindings(contract) {
|
|
32623
|
+
if (contract === null) return [];
|
|
32624
|
+
return [...LOOK.shellExpectedProblems(contract), ...LOOK.kpiStripWorkflowProblems(contract)].map((found) => ({
|
|
32625
|
+
code: found.code,
|
|
32626
|
+
where: found.where,
|
|
32627
|
+
message: found.message,
|
|
32628
|
+
fix: `${found.fix.charAt(0).toUpperCase()}${found.fix.slice(1)}.`,
|
|
32629
|
+
see: `App kit ${found.rule} (contract/look.mjs)`,
|
|
32630
|
+
severity: "warning"
|
|
32631
|
+
}));
|
|
32632
|
+
}
|
|
31963
32633
|
function uxFindings(contract) {
|
|
31964
|
-
if (contract === null || !
|
|
32634
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
31965
32635
|
const codes = {
|
|
31966
32636
|
"A3.23": "create-form-inline",
|
|
31967
32637
|
"A3.24": "row-no-detail",
|
|
@@ -32006,7 +32676,8 @@ function collectFindings(input) {
|
|
|
32006
32676
|
...input.declared === null ? [] : unreachedFindings(input.declared, input.cases, input.contract, input.runs),
|
|
32007
32677
|
// Last: said and never counted, so they never push a counted finding down the list.
|
|
32008
32678
|
...workflow.filter((one) => one.severity === "warning"),
|
|
32009
|
-
...uxFindings(input.contract)
|
|
32679
|
+
...uxFindings(input.contract),
|
|
32680
|
+
...shellFindings(input.contract)
|
|
32010
32681
|
];
|
|
32011
32682
|
}
|
|
32012
32683
|
function formatFindings(findings) {
|
|
@@ -32426,7 +33097,7 @@ function shownValue(value) {
|
|
|
32426
33097
|
return typeof value === "number" || typeof value === "boolean" || typeof value === "string" ? String(value) : JSON.stringify(value);
|
|
32427
33098
|
}
|
|
32428
33099
|
function paramsOf3(contract, page) {
|
|
32429
|
-
if (contract === null || !
|
|
33100
|
+
if (contract === null || !isAppContract3(contract)) return [];
|
|
32430
33101
|
const found = (contract?.pages ?? []).find((one) => one.path === page && one.owned !== true);
|
|
32431
33102
|
return Array.isArray(found?.params) ? found.params : [];
|
|
32432
33103
|
}
|
|
@@ -32478,7 +33149,7 @@ function leversNotChecked(skipUi, runs, contract) {
|
|
|
32478
33149
|
}
|
|
32479
33150
|
}
|
|
32480
33151
|
const loaded = new Set(runs.map((run) => `ui/${run.entry}`));
|
|
32481
|
-
if (contract === null || !
|
|
33152
|
+
if (contract === null || !isAppContract3(contract)) return out;
|
|
32482
33153
|
for (const page of contract.pages ?? []) {
|
|
32483
33154
|
if (page.owned === true || !Array.isArray(page.params) || page.params.length === 0) continue;
|
|
32484
33155
|
if (judged.has(page.path) || !loaded.has(page.path)) continue;
|