@objectstack/lint 17.0.0-rc.4 → 17.0.0-rc.6
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/CHANGELOG.md +1428 -0
- package/dist/index.cjs +1787 -624
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +324 -123
- package/dist/index.d.ts +324 -123
- package/dist/index.js +1765 -610
- package/dist/index.js.map +1 -1
- package/dist/{runtime-Cs64ShwN.d.cts → runtime-B50yywI_.d.cts} +49 -4
- package/dist/{runtime-Cs64ShwN.d.ts → runtime-B50yywI_.d.ts} +49 -4
- package/dist/runtime.cjs +1214 -428
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1204 -407
- package/dist/runtime.js.map +1 -1
- package/package.json +6 -5
package/dist/runtime.cjs
CHANGED
|
@@ -400,6 +400,38 @@ function validateStackExpressions(stack) {
|
|
|
400
400
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
401
401
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
402
402
|
};
|
|
403
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
404
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
405
|
+
const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
|
|
406
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
407
|
+
visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
|
|
408
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
|
|
409
|
+
requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
|
|
410
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
411
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
412
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
413
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
414
|
+
// one, not a fabricated fourth cell (#6716).
|
|
415
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
416
|
+
};
|
|
417
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
418
|
+
const source = celSourceOf(raw);
|
|
419
|
+
if (!source) return;
|
|
420
|
+
const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
|
|
421
|
+
if (!roots.ok) return;
|
|
422
|
+
const kept = import_formula2.SCOPE_ROOTS.filter(
|
|
423
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
424
|
+
);
|
|
425
|
+
if (kept.length === 0) return;
|
|
426
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
427
|
+
const prescription = FIELD_RULE_USER_ROOTS.includes(root) ? `To gate the CHOICES of a select by user, move the predicate to the option's own \`visibleWhen\` (\`options: [{ \u2026, visibleWhen: \u2026 }]\`) \u2014 per-option is the one \`*When\` surface that binds \`current_user\` and its ADR-0068 aliases. To hide the FIELD by role, declare field-level security on a permission set (\`fields: { '<object>.<field>': { readable: false } }\`), which the server enforces. To gate on record state, rewrite the predicate against \`record\`.` : root === "data" ? `\`data\` is the root of a METADATA form (a \`*.form\` module \u2014 the metadata row being edited); this is an OBJECT field, whose runtime form binds the row as \`record\` \u2014 one key name, two form kinds, two roots. Rewrite \`data.<key>\` as \`record.<field>\`.` : `\`${root}\` is declared platform-wide and bound at OTHER evaluation sites (flow, automation, screen and action predicates), never at the field level. Rewrite the predicate against \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item), or move the decision to a surface that binds \`${root}\`.`;
|
|
428
|
+
issues.push({
|
|
429
|
+
where,
|
|
430
|
+
message: `\`${slot}\` reads \`${root}\`, but a field-level conditional rule binds only \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item) \u2014 \`${root}\` is unbound here, so ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}. ` + prescription,
|
|
431
|
+
source,
|
|
432
|
+
severity: "error"
|
|
433
|
+
});
|
|
434
|
+
};
|
|
403
435
|
const checkDeclaredPredicate = (where, raw) => {
|
|
404
436
|
if (raw == null) return;
|
|
405
437
|
const res = (0, import_formula2.validateExpression)("predicate", raw);
|
|
@@ -432,7 +464,14 @@ function validateStackExpressions(stack) {
|
|
|
432
464
|
if (retired.length > 0) {
|
|
433
465
|
issues.push({
|
|
434
466
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
435
|
-
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) +
|
|
467
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
|
|
468
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
469
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
470
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
471
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
472
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
473
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
474
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
436
475
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
437
476
|
});
|
|
438
477
|
} else if (!fn) {
|
|
@@ -466,13 +505,27 @@ function validateStackExpressions(stack) {
|
|
|
466
505
|
for (const [fname, f] of fieldList) {
|
|
467
506
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
468
507
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
508
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
469
509
|
}
|
|
470
|
-
const
|
|
471
|
-
|
|
510
|
+
for (const [oi, opt] of asArray(f.options).entries()) {
|
|
511
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
512
|
+
check(
|
|
513
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
514
|
+
opt.visibleWhen,
|
|
515
|
+
objectName,
|
|
516
|
+
"record"
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
for (const [slot, raw, consequence2] of [
|
|
520
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
521
|
+
["requiredWhen", f.requiredWhen, `the requirement would never be enforced \u2014 the predicate faults, the server logs and skips it, and the field stays optional in the database`]
|
|
522
|
+
]) {
|
|
523
|
+
const source = celSourceOf(raw);
|
|
524
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
472
525
|
issues.push({
|
|
473
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
474
|
-
message:
|
|
475
|
-
source
|
|
526
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
527
|
+
message: `\`${slot}\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and ${consequence2}. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
|
|
528
|
+
source,
|
|
476
529
|
severity: "error"
|
|
477
530
|
});
|
|
478
531
|
}
|
|
@@ -1196,7 +1249,8 @@ function validateDashboardActionRefs(stack) {
|
|
|
1196
1249
|
|
|
1197
1250
|
// src/validate-filter-tokens.ts
|
|
1198
1251
|
var import_data3 = require("@objectstack/spec/data");
|
|
1199
|
-
|
|
1252
|
+
|
|
1253
|
+
// src/filter-walk.ts
|
|
1200
1254
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1201
1255
|
function asArray5(v) {
|
|
1202
1256
|
if (Array.isArray(v)) return v;
|
|
@@ -1208,7 +1262,62 @@ function asArray5(v) {
|
|
|
1208
1262
|
function label(v, fallback) {
|
|
1209
1263
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
1210
1264
|
}
|
|
1265
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
1266
|
+
if (!node || typeof node !== "object") return;
|
|
1267
|
+
if (seen.has(node)) return;
|
|
1268
|
+
seen.add(node);
|
|
1269
|
+
if (Array.isArray(node)) {
|
|
1270
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1274
|
+
const childPath = `${path}.${k}`;
|
|
1275
|
+
if (FILTER_KEYS.has(k)) {
|
|
1276
|
+
visit({ value: v, path: childPath, where });
|
|
1277
|
+
continue;
|
|
1278
|
+
}
|
|
1279
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
1283
|
+
if (!stack || typeof stack !== "object") return;
|
|
1284
|
+
for (const { key, kind } of surfaces) {
|
|
1285
|
+
const items = asArray5(stack[key]);
|
|
1286
|
+
items.forEach((item, i) => {
|
|
1287
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
1288
|
+
if (kind === "dashboard") {
|
|
1289
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1290
|
+
widgets.forEach((w, wi) => {
|
|
1291
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1292
|
+
scanForFilters(
|
|
1293
|
+
w,
|
|
1294
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
1295
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1296
|
+
visit,
|
|
1297
|
+
/* @__PURE__ */ new Set()
|
|
1298
|
+
);
|
|
1299
|
+
});
|
|
1300
|
+
const { widgets: _skip, ...rest } = item;
|
|
1301
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// src/validate-filter-tokens.ts
|
|
1310
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
1211
1311
|
var KNOWN_LIST = import_data3.CONTEXT_TOKENS.join("}, {");
|
|
1312
|
+
var TOKEN_FILTER_SURFACES = [
|
|
1313
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1314
|
+
{ key: "objects", kind: "object" },
|
|
1315
|
+
{ key: "views", kind: "view" },
|
|
1316
|
+
{ key: "reports", kind: "report" },
|
|
1317
|
+
{ key: "datasets", kind: "dataset" },
|
|
1318
|
+
{ key: "pages", kind: "page" },
|
|
1319
|
+
{ key: "apps", kind: "app" }
|
|
1320
|
+
];
|
|
1212
1321
|
function walkFilterValues(node, path, where, out, seen) {
|
|
1213
1322
|
if (node === null || node === void 0) return;
|
|
1214
1323
|
if (typeof node === "string") {
|
|
@@ -1237,58 +1346,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
1237
1346
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
1238
1347
|
}
|
|
1239
1348
|
}
|
|
1240
|
-
function
|
|
1241
|
-
if (!
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1349
|
+
function validateFilterTokens(stack) {
|
|
1350
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1351
|
+
const out = [];
|
|
1352
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
1353
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
1354
|
+
});
|
|
1355
|
+
return out;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// src/validate-empty-combinators.ts
|
|
1359
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
1360
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1361
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1362
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
1363
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1364
|
+
{ key: "objects", kind: "object" },
|
|
1365
|
+
{ key: "views", kind: "view" },
|
|
1366
|
+
{ key: "reports", kind: "report" },
|
|
1367
|
+
{ key: "datasets", kind: "dataset" },
|
|
1368
|
+
{ key: "pages", kind: "page" },
|
|
1369
|
+
{ key: "apps", kind: "app" },
|
|
1370
|
+
{ key: "flows", kind: "flow" }
|
|
1371
|
+
];
|
|
1372
|
+
function isFilterNode(value) {
|
|
1373
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1374
|
+
const proto = Object.getPrototypeOf(value);
|
|
1375
|
+
return proto === Object.prototype || proto === null;
|
|
1376
|
+
}
|
|
1377
|
+
var VERDICT_OF = {
|
|
1378
|
+
$and: (0, import_data4.reduceFilterVerdict)({ $and: [] }),
|
|
1379
|
+
$or: (0, import_data4.reduceFilterVerdict)({ $or: [] }),
|
|
1380
|
+
$not: (0, import_data4.reduceFilterVerdict)({ $not: {} }),
|
|
1381
|
+
node: (0, import_data4.reduceFilterVerdict)({}),
|
|
1382
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1383
|
+
orWithEmptyBranch: (0, import_data4.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
1384
|
+
};
|
|
1385
|
+
function rows(verdict) {
|
|
1386
|
+
if (verdict === "true") return "matches EVERY row";
|
|
1387
|
+
if (verdict === "false") return "matches NO row";
|
|
1388
|
+
return "carries a real predicate";
|
|
1389
|
+
}
|
|
1390
|
+
var MATCH_NONE_SPELLING = "If you really do want a predicate that selects nothing, `{ <field>: { $in: [] } }` is the declared spelling for it (an empty `$in` list matches nothing, on every backend) \u2014 it says so where an empty combinator only implies it.";
|
|
1391
|
+
var OMIT_THE_KEY = 'To express "no filter", DELETE the key \u2014 an absent `filter` and a filter that reduces to TRUE run identically, and only the absent key says so to the next reader (and to the next AI author that copies this metadata).';
|
|
1392
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
1393
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
1394
|
+
const message = key === "$and" ? `\`$and: []\` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ${rows(VERDICT_OF.$and)} \u2014 the key is authored, and it constrains nothing, so this surface reads as filtered and is not.` : key === "$or" ? `\`$or: []\` is a disjunction of ZERO branches. Under the #5322 identity ruling it ${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides every row (fail-closed by design \u2014 #5134).` : `\`$not: {}\` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ${rows(VERDICT_OF.$not)} \u2014 the opposite of the "no filter" an empty operand looks like.`;
|
|
1395
|
+
const hint = key === "$and" ? `${OMIT_THE_KEY} To express a constraint, put the conditions in the array. ${MATCH_NONE_SPELLING}` : key === "$or" ? `If you meant "no filter", this is its OPPOSITE: emptying the array does not relax the filter, it closes it. ${OMIT_THE_KEY} If you meant to offer alternatives, put the branches in the array. ${MATCH_NONE_SPELLING}` : `Put the condition you are negating inside \`$not\` (\`{ $not: { status: 'closed' } }\`). ${OMIT_THE_KEY} ${MATCH_NONE_SPELLING}`;
|
|
1396
|
+
ctx.out.push({
|
|
1397
|
+
severity: "error",
|
|
1398
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
1399
|
+
where: ctx.where,
|
|
1400
|
+
path,
|
|
1401
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
1402
|
+
hint: `${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity unchanged \u2014 this rule judges only what is written in the metadata.`
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function emitEmptyNode(position, path, ctx) {
|
|
1406
|
+
if (position === "root") {
|
|
1407
|
+
ctx.out.push({
|
|
1408
|
+
severity: "error",
|
|
1409
|
+
rule: FILTER_EMPTY_NODE,
|
|
1410
|
+
where: ctx.where,
|
|
1411
|
+
path,
|
|
1412
|
+
message: `An EMPTY filter node (\`{}\`) is authored here. Under the #5322 identity ruling an empty node is TRUE \u2014 it ${rows(VERDICT_OF.node)}, exactly as if the key were absent \u2014 so a filter is declared and enforces nothing.`,
|
|
1413
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
1414
|
+
});
|
|
1246
1415
|
return;
|
|
1247
1416
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1417
|
+
if (position === "or-branch") {
|
|
1418
|
+
ctx.out.push({
|
|
1419
|
+
severity: "error",
|
|
1420
|
+
rule: FILTER_EMPTY_NODE,
|
|
1421
|
+
where: ctx.where,
|
|
1422
|
+
path,
|
|
1423
|
+
message: `An EMPTY branch (\`{}\`) of a \`$or\`. An empty node is TRUE, and one TRUE disjunct ABSORBS the whole disjunction (\`{ $or: [{ status: 'open' }, {}] }\` ${rows(VERDICT_OF.orWithEmptyBranch)}), so every branch you wrote beside it is dead.`,
|
|
1424
|
+
hint: "Delete the empty branch \u2014 the `$or` then means what it looks like. If it was meant to carry a condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW the scope to the surviving branches, which is why the runtime absorbs rather than filters \u2014 #5297.)"
|
|
1425
|
+
});
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
ctx.out.push({
|
|
1429
|
+
severity: "error",
|
|
1430
|
+
rule: FILTER_EMPTY_NODE,
|
|
1431
|
+
where: ctx.where,
|
|
1432
|
+
path,
|
|
1433
|
+
message: "An EMPTY branch (`{}`) of a `$and`. An empty node is TRUE \u2014 the AND identity \u2014 so the branch contributes no condition and the conjunction means whatever its other branches mean.",
|
|
1434
|
+
hint: "Delete the empty branch, or write the condition it was meant to carry. A branch that constrains nothing is indistinguishable from one whose condition was lost in an edit."
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function scanNodeKeys(node, path, ctx) {
|
|
1438
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1439
|
+
if (key === "$and" || key === "$or") {
|
|
1440
|
+
if (!Array.isArray(value)) continue;
|
|
1441
|
+
if (value.length === 0) {
|
|
1442
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
value.forEach((element, index) => {
|
|
1446
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
1447
|
+
});
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
if (key === "$not") {
|
|
1451
|
+
if (!isFilterNode(value)) continue;
|
|
1452
|
+
if (Object.keys(value).length === 0) {
|
|
1453
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
1252
1457
|
continue;
|
|
1253
1458
|
}
|
|
1254
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
1255
1459
|
}
|
|
1256
1460
|
}
|
|
1257
|
-
function
|
|
1461
|
+
function scanBranch(value, path, position, ctx) {
|
|
1462
|
+
if (!isFilterNode(value)) return;
|
|
1463
|
+
if (Object.keys(value).length === 0) {
|
|
1464
|
+
emitEmptyNode(position, path, ctx);
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
scanNodeKeys(value, path, ctx);
|
|
1468
|
+
}
|
|
1469
|
+
function validateEmptyCombinators(stack) {
|
|
1258
1470
|
if (!stack || typeof stack !== "object") return [];
|
|
1259
1471
|
const out = [];
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
["views", "view"],
|
|
1264
|
-
["reports", "report"],
|
|
1265
|
-
["datasets", "dataset"],
|
|
1266
|
-
["pages", "page"],
|
|
1267
|
-
["apps", "app"]
|
|
1268
|
-
];
|
|
1269
|
-
for (const [key, kind] of surfaces) {
|
|
1270
|
-
const items = asArray5(stack[key]);
|
|
1271
|
-
items.forEach((item, i) => {
|
|
1272
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
1273
|
-
if (kind === "dashboard") {
|
|
1274
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1275
|
-
widgets.forEach((w, wi) => {
|
|
1276
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1277
|
-
scanForFilters(
|
|
1278
|
-
w,
|
|
1279
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
1280
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1281
|
-
out,
|
|
1282
|
-
/* @__PURE__ */ new Set()
|
|
1283
|
-
);
|
|
1284
|
-
});
|
|
1285
|
-
const { widgets: _skip, ...rest } = item;
|
|
1286
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1287
|
-
return;
|
|
1288
|
-
}
|
|
1289
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1290
|
-
});
|
|
1291
|
-
}
|
|
1472
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
1473
|
+
scanBranch(value, path, "root", { where, out });
|
|
1474
|
+
});
|
|
1292
1475
|
return out;
|
|
1293
1476
|
}
|
|
1294
1477
|
|
|
@@ -1482,7 +1665,7 @@ function validateObjectReferences(stack) {
|
|
|
1482
1665
|
}
|
|
1483
1666
|
|
|
1484
1667
|
// src/validate-searchable-fields.ts
|
|
1485
|
-
var
|
|
1668
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
1486
1669
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1487
1670
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1488
1671
|
function asArray7(v) {
|
|
@@ -1530,7 +1713,7 @@ function resolveAllowedSet(target) {
|
|
|
1530
1713
|
fields = { ...fields };
|
|
1531
1714
|
for (const f of systemDeclared) fields[f] = {};
|
|
1532
1715
|
}
|
|
1533
|
-
const { allowed, source } = (0,
|
|
1716
|
+
const { allowed, source } = (0, import_data5.resolveSearchFieldResolution)({
|
|
1534
1717
|
fields,
|
|
1535
1718
|
searchableFields: target.searchableFields,
|
|
1536
1719
|
displayField: target.displayField
|
|
@@ -1596,7 +1779,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1596
1779
|
where,
|
|
1597
1780
|
path: `${path}[${i}]`,
|
|
1598
1781
|
message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest3(name, known)),
|
|
1599
|
-
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a
|
|
1782
|
+
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a stored text field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
1783
|
+
});
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
if ((0, import_data5.isVirtualSearchField)(target.fields[name])) {
|
|
1787
|
+
const vtype = target.fields[name]?.type;
|
|
1788
|
+
findings.push({
|
|
1789
|
+
severity: "error",
|
|
1790
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1791
|
+
where,
|
|
1792
|
+
path: `${path}[${i}]`,
|
|
1793
|
+
message: `${subject} entry "${name}" on object "${objectName}" is a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materializes a column for 'search' to scan and the entry can never match. It reads as search coverage and delivers none \u2014 the runtime used to admit it verbatim because the declaration named it (#6674).`,
|
|
1794
|
+
hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
|
|
1600
1795
|
});
|
|
1601
1796
|
continue;
|
|
1602
1797
|
}
|
|
@@ -1616,7 +1811,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1616
1811
|
}
|
|
1617
1812
|
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
1618
1813
|
let why;
|
|
1619
|
-
if (
|
|
1814
|
+
if (import_data5.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
1620
1815
|
why = "a system/audit column, which the auto-default set never includes";
|
|
1621
1816
|
} else if (meta?.hidden) {
|
|
1622
1817
|
why = "hidden";
|
|
@@ -1630,8 +1825,8 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1630
1825
|
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1631
1826
|
where,
|
|
1632
1827
|
path: `${path}[${i}]`,
|
|
1633
|
-
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...
|
|
1634
|
-
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text
|
|
1828
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data5.SEARCHABLE_TEXTUAL_TYPES, ...import_data5.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
1829
|
+
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
|
|
1635
1830
|
});
|
|
1636
1831
|
}
|
|
1637
1832
|
return findings;
|
|
@@ -1950,6 +2145,15 @@ function validateActionNameRefs(stack) {
|
|
|
1950
2145
|
"Navigation action item"
|
|
1951
2146
|
);
|
|
1952
2147
|
}
|
|
2148
|
+
const runAction = strName5(nav.runAction);
|
|
2149
|
+
if (nav.type === "object" && runAction) {
|
|
2150
|
+
check(
|
|
2151
|
+
runAction,
|
|
2152
|
+
`app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
|
|
2153
|
+
`${navPath}.runAction`,
|
|
2154
|
+
"Navigation deep-link auto-run"
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
1953
2157
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
1954
2158
|
}
|
|
1955
2159
|
};
|
|
@@ -2015,8 +2219,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2015
2219
|
"element:number": { props: ["field"] },
|
|
2016
2220
|
"element:filter": { props: ["fields"] },
|
|
2017
2221
|
"element:form": { props: ["fields"] },
|
|
2018
|
-
//
|
|
2019
|
-
|
|
2222
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2223
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
2224
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
2225
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
2226
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
2227
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
2228
|
+
//
|
|
2229
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
2230
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
2231
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
2232
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
2233
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
2234
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
2235
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
2236
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2020
2237
|
};
|
|
2021
2238
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2022
2239
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2585,18 +2802,57 @@ function validateNavTargetRefs(stack) {
|
|
|
2585
2802
|
}
|
|
2586
2803
|
|
|
2587
2804
|
// src/validate-translation-references.ts
|
|
2805
|
+
var import_spec = require("@objectstack/spec");
|
|
2588
2806
|
var import_system4 = require("@objectstack/spec/system");
|
|
2807
|
+
|
|
2808
|
+
// src/view-walk.ts
|
|
2809
|
+
function isRec7(v) {
|
|
2810
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2811
|
+
}
|
|
2812
|
+
function strName10(v) {
|
|
2813
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2814
|
+
}
|
|
2815
|
+
function viewObjectName(view) {
|
|
2816
|
+
return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
|
|
2817
|
+
}
|
|
2818
|
+
function viewContainerSites(view, basePath) {
|
|
2819
|
+
if (!isRec7(view)) return [];
|
|
2820
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
2821
|
+
if (isRec7(view.form)) {
|
|
2822
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
2823
|
+
}
|
|
2824
|
+
for (const key of ["listViews", "formViews"]) {
|
|
2825
|
+
const container = view[key];
|
|
2826
|
+
if (!isRec7(container)) continue;
|
|
2827
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
2828
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
2829
|
+
if (!isRec7(sub)) continue;
|
|
2830
|
+
sites.push({
|
|
2831
|
+
view: sub,
|
|
2832
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
2833
|
+
surface: `${key}.${subKey}`,
|
|
2834
|
+
kind
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
return sites;
|
|
2839
|
+
}
|
|
2840
|
+
function formViewSites(view, basePath) {
|
|
2841
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/validate-translation-references.ts
|
|
2589
2845
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
2590
2846
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
2591
|
-
function
|
|
2847
|
+
function isRec8(v) {
|
|
2592
2848
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2593
2849
|
}
|
|
2594
2850
|
function asArray14(v) {
|
|
2595
2851
|
if (Array.isArray(v)) return v;
|
|
2596
|
-
if (
|
|
2852
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec8(def) ? def : {} }));
|
|
2597
2853
|
return [];
|
|
2598
2854
|
}
|
|
2599
|
-
function
|
|
2855
|
+
function strName11(v) {
|
|
2600
2856
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2601
2857
|
}
|
|
2602
2858
|
function distance5(a, b) {
|
|
@@ -2657,29 +2913,51 @@ function collectViewRecord(view, factsFor) {
|
|
|
2657
2913
|
const addSections = (container, binding) => {
|
|
2658
2914
|
if (!binding) return;
|
|
2659
2915
|
for (const section of asArray14(container.sections)) {
|
|
2660
|
-
const sectionName =
|
|
2916
|
+
const sectionName = strName11(section.name);
|
|
2661
2917
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2662
2918
|
}
|
|
2663
2919
|
};
|
|
2664
|
-
const listBinding =
|
|
2665
|
-
if (
|
|
2666
|
-
addView(recordObject ?? listBinding,
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2920
|
+
const listBinding = isRec8(view.list) ? bindingOf(view.list) : void 0;
|
|
2921
|
+
if (isRec8(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
2922
|
+
addView(recordObject ?? listBinding, strName11(view.name));
|
|
2923
|
+
const named = namedViewKeys(view);
|
|
2924
|
+
for (const family of ["listViews", "formViews"]) {
|
|
2925
|
+
const container = view[family];
|
|
2926
|
+
if (!isRec8(container)) continue;
|
|
2927
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
2928
|
+
let at = 0;
|
|
2929
|
+
for (const sub of Object.values(container)) {
|
|
2930
|
+
if (!sub || typeof sub !== "object") continue;
|
|
2931
|
+
const registryKey = registryKeys[at++];
|
|
2932
|
+
if (!isRec8(sub)) continue;
|
|
2672
2933
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2673
|
-
addView(binding,
|
|
2674
|
-
addView(binding, strName10(sub.name));
|
|
2934
|
+
addView(binding, registryKey);
|
|
2675
2935
|
addSections(sub, binding);
|
|
2676
2936
|
}
|
|
2677
2937
|
}
|
|
2678
|
-
if (
|
|
2938
|
+
if (isRec8(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2679
2939
|
addSections(view, recordObject ?? listBinding);
|
|
2680
2940
|
}
|
|
2681
|
-
function
|
|
2682
|
-
|
|
2941
|
+
function defaultListViewKey(object, container) {
|
|
2942
|
+
if (!object || !isRec8(container.list)) return void 0;
|
|
2943
|
+
const item = (0, import_spec.expandViewContainer)(object, container).find(
|
|
2944
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
2945
|
+
);
|
|
2946
|
+
if (!item) return void 0;
|
|
2947
|
+
const prefix = `${object}.`;
|
|
2948
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
2949
|
+
}
|
|
2950
|
+
function namedViewKeys(container) {
|
|
2951
|
+
const object = "probe";
|
|
2952
|
+
const prefix = `${object}.`;
|
|
2953
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
2954
|
+
const countEntries = (v) => isRec8(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
2955
|
+
const listCount = countEntries(container.listViews);
|
|
2956
|
+
const formCount = countEntries(container.formViews);
|
|
2957
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
2958
|
+
const items = (0, import_spec.expandViewContainer)(object, container);
|
|
2959
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
2960
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
2683
2961
|
}
|
|
2684
2962
|
function readOptions(field) {
|
|
2685
2963
|
const raw = field.options;
|
|
@@ -2691,14 +2969,14 @@ function readOptions(field) {
|
|
|
2691
2969
|
values.add(opt);
|
|
2692
2970
|
continue;
|
|
2693
2971
|
}
|
|
2694
|
-
if (!
|
|
2695
|
-
const value =
|
|
2972
|
+
if (!isRec8(opt)) continue;
|
|
2973
|
+
const value = strName11(opt.value);
|
|
2696
2974
|
if (!value) continue;
|
|
2697
2975
|
values.add(value);
|
|
2698
|
-
const label2 =
|
|
2976
|
+
const label2 = strName11(opt.label);
|
|
2699
2977
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2700
2978
|
}
|
|
2701
|
-
} else if (
|
|
2979
|
+
} else if (isRec8(raw)) {
|
|
2702
2980
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2703
2981
|
values.add(value);
|
|
2704
2982
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2719,23 +2997,23 @@ function buildUniverse(stack) {
|
|
|
2719
2997
|
return facts;
|
|
2720
2998
|
};
|
|
2721
2999
|
for (const obj of asArray14(stack.objects)) {
|
|
2722
|
-
const objectName =
|
|
3000
|
+
const objectName = strName11(obj.name);
|
|
2723
3001
|
if (!objectName) continue;
|
|
2724
3002
|
const facts = factsFor(objectName);
|
|
2725
3003
|
for (const field of asArray14(obj.fields)) {
|
|
2726
|
-
const fieldName =
|
|
3004
|
+
const fieldName = strName11(field.name);
|
|
2727
3005
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
2728
3006
|
}
|
|
2729
3007
|
for (const action of asArray14(obj.actions)) {
|
|
2730
|
-
const actionName =
|
|
3008
|
+
const actionName = strName11(action.name);
|
|
2731
3009
|
if (actionName) facts.actions.set(actionName, action);
|
|
2732
3010
|
}
|
|
2733
3011
|
for (const view of asArray14(obj.views)) {
|
|
2734
|
-
collectViewRecord({ ...view, object:
|
|
3012
|
+
collectViewRecord({ ...view, object: strName11(view.object) ?? objectName }, factsFor);
|
|
2735
3013
|
}
|
|
2736
3014
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
2737
3015
|
for (const group of asArray14(obj.fieldGroups)) {
|
|
2738
|
-
const key =
|
|
3016
|
+
const key = strName11(group.key) ?? strName11(group.name);
|
|
2739
3017
|
if (key) facts.sections.add(key);
|
|
2740
3018
|
}
|
|
2741
3019
|
}
|
|
@@ -2746,10 +3024,10 @@ function buildUniverse(stack) {
|
|
|
2746
3024
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2747
3025
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
2748
3026
|
if (!walked.objectName) continue;
|
|
2749
|
-
const props =
|
|
3027
|
+
const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
|
|
2750
3028
|
if (!props) continue;
|
|
2751
3029
|
for (const section of asArray14(props.sections)) {
|
|
2752
|
-
const sectionName =
|
|
3030
|
+
const sectionName = strName11(section.name);
|
|
2753
3031
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
2754
3032
|
}
|
|
2755
3033
|
}
|
|
@@ -2757,9 +3035,9 @@ function buildUniverse(stack) {
|
|
|
2757
3035
|
const globalActions = /* @__PURE__ */ new Map();
|
|
2758
3036
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
2759
3037
|
for (const action of asArray14(stack.actions)) {
|
|
2760
|
-
const actionName =
|
|
3038
|
+
const actionName = strName11(action.name);
|
|
2761
3039
|
if (!actionName) continue;
|
|
2762
|
-
const owner =
|
|
3040
|
+
const owner = strName11(action.objectName) ?? strName11(action.object);
|
|
2763
3041
|
if (owner) {
|
|
2764
3042
|
factsFor(owner).actions.set(actionName, action);
|
|
2765
3043
|
actionOwners.set(actionName, owner);
|
|
@@ -2774,19 +3052,19 @@ function buildUniverse(stack) {
|
|
|
2774
3052
|
}
|
|
2775
3053
|
const apps = /* @__PURE__ */ new Map();
|
|
2776
3054
|
for (const app of asArray14(stack.apps)) {
|
|
2777
|
-
const appName =
|
|
3055
|
+
const appName = strName11(app.name);
|
|
2778
3056
|
if (!appName) continue;
|
|
2779
3057
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
2780
3058
|
const walkNav = (items) => {
|
|
2781
3059
|
for (const item of asArray14(items)) {
|
|
2782
|
-
const id =
|
|
3060
|
+
const id = strName11(item.id);
|
|
2783
3061
|
if (id) navIds.add(id);
|
|
2784
3062
|
if (item.children) walkNav(item.children);
|
|
2785
3063
|
}
|
|
2786
3064
|
};
|
|
2787
3065
|
walkNav(app.navigation);
|
|
2788
3066
|
for (const area of asArray14(app.areas)) {
|
|
2789
|
-
const areaId =
|
|
3067
|
+
const areaId = strName11(area.id);
|
|
2790
3068
|
if (areaId) navIds.add(areaId);
|
|
2791
3069
|
walkNav(area.navigation);
|
|
2792
3070
|
}
|
|
@@ -2794,20 +3072,20 @@ function buildUniverse(stack) {
|
|
|
2794
3072
|
}
|
|
2795
3073
|
const dashboards = /* @__PURE__ */ new Map();
|
|
2796
3074
|
for (const dash of asArray14(stack.dashboards)) {
|
|
2797
|
-
const dashName =
|
|
3075
|
+
const dashName = strName11(dash.name);
|
|
2798
3076
|
if (!dashName) continue;
|
|
2799
3077
|
const widgets = /* @__PURE__ */ new Set();
|
|
2800
3078
|
for (const widget of asArray14(dash.widgets)) {
|
|
2801
|
-
const id =
|
|
3079
|
+
const id = strName11(widget.id) ?? strName11(widget.name);
|
|
2802
3080
|
if (id) widgets.add(id);
|
|
2803
3081
|
}
|
|
2804
3082
|
const actions = /* @__PURE__ */ new Set();
|
|
2805
3083
|
const headerActions = [
|
|
2806
|
-
...asArray14(
|
|
3084
|
+
...asArray14(isRec8(dash.header) ? dash.header.actions : void 0),
|
|
2807
3085
|
...asArray14(dash.actions)
|
|
2808
3086
|
];
|
|
2809
3087
|
for (const action of headerActions) {
|
|
2810
|
-
const key =
|
|
3088
|
+
const key = strName11(action.actionUrl) ?? strName11(action.url) ?? strName11(action.name);
|
|
2811
3089
|
if (key) actions.add(key);
|
|
2812
3090
|
}
|
|
2813
3091
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -2819,7 +3097,7 @@ function localePath(bundleIndex, locale) {
|
|
|
2819
3097
|
}
|
|
2820
3098
|
function validateTranslationReferences(stack) {
|
|
2821
3099
|
const findings = [];
|
|
2822
|
-
if (!
|
|
3100
|
+
if (!isRec8(stack)) return findings;
|
|
2823
3101
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
2824
3102
|
if (bundles.length === 0) return findings;
|
|
2825
3103
|
const universe = buildUniverse(stack);
|
|
@@ -2828,13 +3106,13 @@ function validateTranslationReferences(stack) {
|
|
|
2828
3106
|
};
|
|
2829
3107
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
2830
3108
|
const bundle = bundles[bi];
|
|
2831
|
-
if (!
|
|
3109
|
+
if (!isRec8(bundle)) continue;
|
|
2832
3110
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
2833
|
-
if (!
|
|
3111
|
+
if (!isRec8(rawData)) continue;
|
|
2834
3112
|
const base = localePath(bi, locale);
|
|
2835
3113
|
const inLocale = `locale "${locale}"`;
|
|
2836
3114
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
2837
|
-
if (!
|
|
3115
|
+
if (!isRec8(rawNode)) continue;
|
|
2838
3116
|
const objPath = `${base}.objects.${objectName}`;
|
|
2839
3117
|
const facts = universe.objects.get(objectName);
|
|
2840
3118
|
if (!facts) {
|
|
@@ -2860,7 +3138,7 @@ function validateTranslationReferences(stack) {
|
|
|
2860
3138
|
);
|
|
2861
3139
|
continue;
|
|
2862
3140
|
}
|
|
2863
|
-
if (!
|
|
3141
|
+
if (!isRec8(rawField)) continue;
|
|
2864
3142
|
checkOptionKeys(findings, {
|
|
2865
3143
|
optionMap: rawField.options,
|
|
2866
3144
|
field,
|
|
@@ -2942,7 +3220,7 @@ function validateTranslationReferences(stack) {
|
|
|
2942
3220
|
);
|
|
2943
3221
|
continue;
|
|
2944
3222
|
}
|
|
2945
|
-
if (!
|
|
3223
|
+
if (!isRec8(rawApp)) continue;
|
|
2946
3224
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
2947
3225
|
if (navIds.has(navId)) continue;
|
|
2948
3226
|
orphan(
|
|
@@ -2965,7 +3243,7 @@ function validateTranslationReferences(stack) {
|
|
|
2965
3243
|
);
|
|
2966
3244
|
continue;
|
|
2967
3245
|
}
|
|
2968
|
-
if (!
|
|
3246
|
+
if (!isRec8(rawDash)) continue;
|
|
2969
3247
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
2970
3248
|
if (dash.widgets.has(widgetId)) continue;
|
|
2971
3249
|
orphan(
|
|
@@ -2990,7 +3268,7 @@ function validateTranslationReferences(stack) {
|
|
|
2990
3268
|
return findings;
|
|
2991
3269
|
}
|
|
2992
3270
|
function asRecord(v) {
|
|
2993
|
-
return
|
|
3271
|
+
return isRec8(v) ? v : {};
|
|
2994
3272
|
}
|
|
2995
3273
|
function checkOptionKeys(findings, ctx) {
|
|
2996
3274
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3002,7 +3280,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3002
3280
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3003
3281
|
where: ctx.where,
|
|
3004
3282
|
path: ctx.path,
|
|
3005
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3283
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName11(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
3006
3284
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3007
3285
|
});
|
|
3008
3286
|
return;
|
|
@@ -3021,11 +3299,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3021
3299
|
}
|
|
3022
3300
|
}
|
|
3023
3301
|
function checkActionParams(findings, ctx) {
|
|
3024
|
-
const rawParams = Object.keys(asRecord(
|
|
3302
|
+
const rawParams = Object.keys(asRecord(isRec8(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3025
3303
|
if (rawParams.length === 0) return;
|
|
3026
3304
|
const declared = /* @__PURE__ */ new Set();
|
|
3027
3305
|
for (const param of asArray14(ctx.action.params)) {
|
|
3028
|
-
const name =
|
|
3306
|
+
const name = strName11(param.name) ?? strName11(param.field);
|
|
3029
3307
|
if (name) declared.add(name);
|
|
3030
3308
|
}
|
|
3031
3309
|
for (const paramName of rawParams) {
|
|
@@ -3041,78 +3319,60 @@ function checkActionParams(findings, ctx) {
|
|
|
3041
3319
|
}
|
|
3042
3320
|
}
|
|
3043
3321
|
|
|
3044
|
-
// src/
|
|
3045
|
-
|
|
3046
|
-
function isRec8(v) {
|
|
3322
|
+
// src/collection-entries.ts
|
|
3323
|
+
function isRec9(v) {
|
|
3047
3324
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3048
3325
|
}
|
|
3049
|
-
function strName11(v) {
|
|
3050
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3051
|
-
}
|
|
3052
|
-
function viewObjectName2(view) {
|
|
3053
|
-
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
|
|
3054
|
-
}
|
|
3055
3326
|
function collectionEntries(v, base) {
|
|
3056
3327
|
if (Array.isArray(v)) {
|
|
3057
3328
|
const out = [];
|
|
3058
3329
|
for (let i = 0; i < v.length; i++) {
|
|
3059
|
-
if (
|
|
3330
|
+
if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3060
3331
|
}
|
|
3061
3332
|
return out;
|
|
3062
3333
|
}
|
|
3063
|
-
if (
|
|
3064
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3334
|
+
if (isRec9(v)) {
|
|
3335
|
+
return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3065
3336
|
}
|
|
3066
3337
|
return [];
|
|
3067
3338
|
}
|
|
3339
|
+
|
|
3340
|
+
// src/validate-translatable-sections.ts
|
|
3341
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3342
|
+
function isRec10(v) {
|
|
3343
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3344
|
+
}
|
|
3345
|
+
function strName12(v) {
|
|
3346
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3347
|
+
}
|
|
3068
3348
|
function viewLabel(view) {
|
|
3069
|
-
const name =
|
|
3349
|
+
const name = strName12(view.name);
|
|
3070
3350
|
return name ? `view "${name}"` : "";
|
|
3071
3351
|
}
|
|
3072
3352
|
function joinWhere(...parts) {
|
|
3073
3353
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
3074
3354
|
}
|
|
3075
3355
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3076
|
-
const recordObject =
|
|
3077
|
-
const listBinding =
|
|
3078
|
-
const
|
|
3079
|
-
sites.push({
|
|
3080
|
-
path: `${basePath}.sections`,
|
|
3081
|
-
surface: label2,
|
|
3082
|
-
objectName: recordObject ?? listBinding,
|
|
3083
|
-
sections: view.sections
|
|
3084
|
-
});
|
|
3085
|
-
if (isRec8(view.form)) {
|
|
3356
|
+
const recordObject = viewObjectName(view);
|
|
3357
|
+
const listBinding = isRec10(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3358
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
3086
3359
|
sites.push({
|
|
3087
|
-
path: `${
|
|
3088
|
-
surface: joinWhere(label2,
|
|
3089
|
-
objectName:
|
|
3090
|
-
sections: view.
|
|
3360
|
+
path: `${site.path}.sections`,
|
|
3361
|
+
surface: joinWhere(label2, site.surface),
|
|
3362
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
3363
|
+
sections: site.view.sections
|
|
3091
3364
|
});
|
|
3092
3365
|
}
|
|
3093
|
-
for (const key of ["listViews", "formViews"]) {
|
|
3094
|
-
const container = view[key];
|
|
3095
|
-
if (!isRec8(container)) continue;
|
|
3096
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
3097
|
-
if (!isRec8(sub)) continue;
|
|
3098
|
-
sites.push({
|
|
3099
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
3100
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
3101
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
3102
|
-
sections: sub.sections
|
|
3103
|
-
});
|
|
3104
|
-
}
|
|
3105
|
-
}
|
|
3106
3366
|
}
|
|
3107
3367
|
function translatedObjectNames(stack) {
|
|
3108
3368
|
const out = /* @__PURE__ */ new Set();
|
|
3109
3369
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3110
3370
|
for (const bundle of bundles) {
|
|
3111
|
-
if (!
|
|
3371
|
+
if (!isRec10(bundle)) continue;
|
|
3112
3372
|
for (const data of Object.values(bundle)) {
|
|
3113
|
-
if (!
|
|
3373
|
+
if (!isRec10(data) || !isRec10(data.objects)) continue;
|
|
3114
3374
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3115
|
-
if (
|
|
3375
|
+
if (isRec10(node)) out.add(objectName);
|
|
3116
3376
|
}
|
|
3117
3377
|
}
|
|
3118
3378
|
}
|
|
@@ -3124,22 +3384,22 @@ function suggestedName(label2) {
|
|
|
3124
3384
|
}
|
|
3125
3385
|
function validateTranslatableSections(stack) {
|
|
3126
3386
|
const findings = [];
|
|
3127
|
-
if (!
|
|
3387
|
+
if (!isRec10(stack)) return findings;
|
|
3128
3388
|
const translated = translatedObjectNames(stack);
|
|
3129
3389
|
if (translated.size === 0) return findings;
|
|
3130
3390
|
const sites = [];
|
|
3131
3391
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3132
|
-
const objectName =
|
|
3392
|
+
const objectName = strName12(obj.name);
|
|
3133
3393
|
if (!objectName) continue;
|
|
3134
3394
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3135
3395
|
collectViewSites(
|
|
3136
|
-
{ ...view, object:
|
|
3396
|
+
{ ...view, object: strName12(view.object) ?? objectName },
|
|
3137
3397
|
path,
|
|
3138
3398
|
viewLabel(view),
|
|
3139
3399
|
sites
|
|
3140
3400
|
);
|
|
3141
3401
|
}
|
|
3142
|
-
if (
|
|
3402
|
+
if (isRec10(obj.listViews)) {
|
|
3143
3403
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3144
3404
|
}
|
|
3145
3405
|
}
|
|
@@ -3147,13 +3407,13 @@ function validateTranslatableSections(stack) {
|
|
|
3147
3407
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3148
3408
|
}
|
|
3149
3409
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3150
|
-
const pageName =
|
|
3410
|
+
const pageName = strName12(page.name);
|
|
3151
3411
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3152
3412
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3153
3413
|
if (!walked.objectName) continue;
|
|
3154
|
-
const props =
|
|
3414
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
3155
3415
|
if (!props) continue;
|
|
3156
|
-
const type =
|
|
3416
|
+
const type = strName12(walked.component.type) ?? "component";
|
|
3157
3417
|
sites.push({
|
|
3158
3418
|
path: `${walked.path}.properties.sections`,
|
|
3159
3419
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3168,9 +3428,9 @@ function validateTranslatableSections(stack) {
|
|
|
3168
3428
|
if (!Array.isArray(site.sections)) continue;
|
|
3169
3429
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3170
3430
|
const section = site.sections[i];
|
|
3171
|
-
if (!
|
|
3172
|
-
if (
|
|
3173
|
-
const heading =
|
|
3431
|
+
if (!isRec10(section)) continue;
|
|
3432
|
+
if (strName12(section.name)) continue;
|
|
3433
|
+
const heading = strName12(section.label);
|
|
3174
3434
|
if (!heading) continue;
|
|
3175
3435
|
const slug = suggestedName(heading);
|
|
3176
3436
|
findings.push({
|
|
@@ -3188,10 +3448,10 @@ function validateTranslatableSections(stack) {
|
|
|
3188
3448
|
|
|
3189
3449
|
// src/flow-walk.ts
|
|
3190
3450
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
3191
|
-
function
|
|
3451
|
+
function isRec11(v) {
|
|
3192
3452
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3193
3453
|
}
|
|
3194
|
-
function
|
|
3454
|
+
function strName13(v) {
|
|
3195
3455
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3196
3456
|
}
|
|
3197
3457
|
var REGION_SLOTS = new Map(
|
|
@@ -3200,10 +3460,10 @@ var REGION_SLOTS = new Map(
|
|
|
3200
3460
|
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
3201
3461
|
var MAX_REGION_DEPTH = 16;
|
|
3202
3462
|
function flowNodeLabel(node, index) {
|
|
3203
|
-
return
|
|
3463
|
+
return strName13(node.label) ?? strName13(node.id) ?? `#${index}`;
|
|
3204
3464
|
}
|
|
3205
3465
|
function stripRegions(config) {
|
|
3206
|
-
if (!
|
|
3466
|
+
if (!isRec11(config)) return void 0;
|
|
3207
3467
|
let out;
|
|
3208
3468
|
for (const key of Object.keys(config)) {
|
|
3209
3469
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3214,11 +3474,11 @@ function stripRegions(config) {
|
|
|
3214
3474
|
}
|
|
3215
3475
|
function walkFlowNodes(flow, flowPath) {
|
|
3216
3476
|
const out = [];
|
|
3217
|
-
if (!
|
|
3477
|
+
if (!isRec11(flow)) return out;
|
|
3218
3478
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3219
3479
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3220
3480
|
nodes.forEach((raw, index) => {
|
|
3221
|
-
if (!
|
|
3481
|
+
if (!isRec11(raw)) return;
|
|
3222
3482
|
const path = `${basePath}[${index}]`;
|
|
3223
3483
|
out.push({
|
|
3224
3484
|
node: raw,
|
|
@@ -3227,9 +3487,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3227
3487
|
regionTrail: trail,
|
|
3228
3488
|
depth
|
|
3229
3489
|
});
|
|
3230
|
-
const type =
|
|
3490
|
+
const type = strName13(raw.type);
|
|
3231
3491
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3232
|
-
if (!slots || !
|
|
3492
|
+
if (!slots || !isRec11(raw.config)) return;
|
|
3233
3493
|
const config = raw.config;
|
|
3234
3494
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3235
3495
|
for (const slot of slots) {
|
|
@@ -3237,8 +3497,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3237
3497
|
if (slot === "branches") {
|
|
3238
3498
|
if (!Array.isArray(value)) continue;
|
|
3239
3499
|
value.forEach((branch, b) => {
|
|
3240
|
-
if (!
|
|
3241
|
-
const branchName =
|
|
3500
|
+
if (!isRec11(branch)) return;
|
|
3501
|
+
const branchName = strName13(branch.name) ?? `#${b}`;
|
|
3242
3502
|
visitList(
|
|
3243
3503
|
branch.nodes,
|
|
3244
3504
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3248,7 +3508,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3248
3508
|
});
|
|
3249
3509
|
continue;
|
|
3250
3510
|
}
|
|
3251
|
-
if (!
|
|
3511
|
+
if (!isRec11(value)) continue;
|
|
3252
3512
|
visitList(
|
|
3253
3513
|
value.nodes,
|
|
3254
3514
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3473,7 +3733,7 @@ function asArray16(v) {
|
|
|
3473
3733
|
}
|
|
3474
3734
|
return [];
|
|
3475
3735
|
}
|
|
3476
|
-
function
|
|
3736
|
+
function strName14(v) {
|
|
3477
3737
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3478
3738
|
}
|
|
3479
3739
|
function surfaceOf(v) {
|
|
@@ -3484,17 +3744,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3484
3744
|
if (!stack || typeof stack !== "object") return findings;
|
|
3485
3745
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3486
3746
|
for (const skill of asArray16(stack.skills)) {
|
|
3487
|
-
const n =
|
|
3747
|
+
const n = strName14(skill.name);
|
|
3488
3748
|
if (n) skillsByName.set(n, skill);
|
|
3489
3749
|
}
|
|
3490
3750
|
const agents = asArray16(stack.agents);
|
|
3491
3751
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3492
3752
|
const agent = agents[ai];
|
|
3493
|
-
const agentName =
|
|
3753
|
+
const agentName = strName14(agent.name) ?? `#${ai}`;
|
|
3494
3754
|
const agentSurface = surfaceOf(agent.surface);
|
|
3495
3755
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3496
3756
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3497
|
-
const ref =
|
|
3757
|
+
const ref = strName14(skillRefs[si]);
|
|
3498
3758
|
if (!ref) continue;
|
|
3499
3759
|
const skill = skillsByName.get(ref);
|
|
3500
3760
|
if (!skill) continue;
|
|
@@ -3523,7 +3783,7 @@ function asArray17(v) {
|
|
|
3523
3783
|
}
|
|
3524
3784
|
return [];
|
|
3525
3785
|
}
|
|
3526
|
-
function
|
|
3786
|
+
function strName15(v) {
|
|
3527
3787
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3528
3788
|
}
|
|
3529
3789
|
function distance6(a, b) {
|
|
@@ -3564,8 +3824,8 @@ function materialisesAsTool(action) {
|
|
|
3564
3824
|
if (!ai || typeof ai !== "object") return false;
|
|
3565
3825
|
const aiRec = ai;
|
|
3566
3826
|
if (aiRec.exposed !== true) return false;
|
|
3567
|
-
if (!
|
|
3568
|
-
const type =
|
|
3827
|
+
if (!strName15(aiRec.description)) return false;
|
|
3828
|
+
const type = strName15(action.type);
|
|
3569
3829
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3570
3830
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3571
3831
|
return Boolean(action.target);
|
|
@@ -3573,12 +3833,12 @@ function materialisesAsTool(action) {
|
|
|
3573
3833
|
function collectToolUniverse(stack) {
|
|
3574
3834
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3575
3835
|
for (const tool of asArray17(stack.tools)) {
|
|
3576
|
-
const n =
|
|
3836
|
+
const n = strName15(tool.name);
|
|
3577
3837
|
if (n) universe.add(n);
|
|
3578
3838
|
}
|
|
3579
3839
|
const addActionFamily = (actions) => {
|
|
3580
3840
|
for (const action of asArray17(actions)) {
|
|
3581
|
-
const n =
|
|
3841
|
+
const n = strName15(action.name);
|
|
3582
3842
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3583
3843
|
}
|
|
3584
3844
|
};
|
|
@@ -3592,7 +3852,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
3592
3852
|
const names = /* @__PURE__ */ new Set();
|
|
3593
3853
|
const scan = (actions) => {
|
|
3594
3854
|
for (const action of asArray17(actions)) {
|
|
3595
|
-
const n =
|
|
3855
|
+
const n = strName15(action.name);
|
|
3596
3856
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3597
3857
|
}
|
|
3598
3858
|
};
|
|
@@ -3618,10 +3878,10 @@ function validateAiToolReferences(stack) {
|
|
|
3618
3878
|
const skills = asArray17(stack.skills);
|
|
3619
3879
|
for (let si = 0; si < skills.length; si++) {
|
|
3620
3880
|
const skill = skills[si];
|
|
3621
|
-
const skillName =
|
|
3881
|
+
const skillName = strName15(skill.name) ?? `#${si}`;
|
|
3622
3882
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3623
3883
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3624
|
-
const ref =
|
|
3884
|
+
const ref = strName15(refs[ti]);
|
|
3625
3885
|
if (!ref || resolves(ref)) continue;
|
|
3626
3886
|
const isPattern = ref.endsWith("*");
|
|
3627
3887
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3640,6 +3900,7 @@ function validateAiToolReferences(stack) {
|
|
|
3640
3900
|
|
|
3641
3901
|
// src/validate-ai-agent-authoring.ts
|
|
3642
3902
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3903
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
3643
3904
|
function asArray18(v) {
|
|
3644
3905
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3645
3906
|
if (v && typeof v === "object") {
|
|
@@ -3647,7 +3908,7 @@ function asArray18(v) {
|
|
|
3647
3908
|
}
|
|
3648
3909
|
return [];
|
|
3649
3910
|
}
|
|
3650
|
-
function
|
|
3911
|
+
function strName16(v) {
|
|
3651
3912
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3652
3913
|
}
|
|
3653
3914
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -3657,7 +3918,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3657
3918
|
const agents = asArray18(stack.agents);
|
|
3658
3919
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3659
3920
|
const agent = agents[ai];
|
|
3660
|
-
const name =
|
|
3921
|
+
const name = strName16(agent.name) ?? `#${ai}`;
|
|
3661
3922
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3662
3923
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3663
3924
|
findings.push({
|
|
@@ -3669,6 +3930,22 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3669
3930
|
hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
|
|
3670
3931
|
});
|
|
3671
3932
|
}
|
|
3933
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
3934
|
+
const apps = asArray18(stack.apps);
|
|
3935
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
3936
|
+
const app = apps[appIdx];
|
|
3937
|
+
const defaultAgent = strName16(app.defaultAgent);
|
|
3938
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
3939
|
+
const appName = strName16(app.name) ?? `#${appIdx}`;
|
|
3940
|
+
findings.push({
|
|
3941
|
+
severity: "warning",
|
|
3942
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
3943
|
+
where: `app "${appName}".defaultAgent`,
|
|
3944
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
3945
|
+
message: `app "${appName}" pins \`defaultAgent\` to "${defaultAgent}", which is not in the platform agent roster (${roster}). The kernel ships exactly two agents (ADR-0063 \xA72) and resolves this key against them and their legacy aliases only \u2014 an unrecognized name is not rejected, it silently falls back to the platform default at runtime, so the pin has no effect and the value drifts from what actually serves the app.`,
|
|
3946
|
+
hint: `Set \`defaultAgent\` to one of the platform agent names: ${roster}. If the goal is a dedicated persona or capability, express it as skills instead \u2014 they attach to "ask" / "build" by surface affinity, not as a custom \`defaultAgent\` value.`
|
|
3947
|
+
});
|
|
3948
|
+
}
|
|
3672
3949
|
return findings;
|
|
3673
3950
|
}
|
|
3674
3951
|
|
|
@@ -3756,13 +4033,13 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
3756
4033
|
"owner",
|
|
3757
4034
|
"record_type"
|
|
3758
4035
|
]);
|
|
3759
|
-
var
|
|
4036
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3760
4037
|
function asArray19(v) {
|
|
3761
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3762
|
-
if (
|
|
4038
|
+
if (Array.isArray(v)) return v.filter((x) => isRec12(x));
|
|
4039
|
+
if (isRec12(v)) {
|
|
3763
4040
|
return Object.entries(v).map(([name, def]) => ({
|
|
3764
4041
|
name,
|
|
3765
|
-
...
|
|
4042
|
+
...isRec12(def) ? def : {}
|
|
3766
4043
|
}));
|
|
3767
4044
|
}
|
|
3768
4045
|
return [];
|
|
@@ -3908,7 +4185,7 @@ function validateHookBodyWrites(stack) {
|
|
|
3908
4185
|
let objectFields = null;
|
|
3909
4186
|
hooks.forEach((hook, hookIndex) => {
|
|
3910
4187
|
const body = hook.body;
|
|
3911
|
-
if (!
|
|
4188
|
+
if (!isRec12(body) || body.language !== "js") return;
|
|
3912
4189
|
const source = body.source;
|
|
3913
4190
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3914
4191
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -3979,13 +4256,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
3979
4256
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
3980
4257
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
3981
4258
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
3982
|
-
var
|
|
4259
|
+
var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3983
4260
|
function asArray20(v) {
|
|
3984
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3985
|
-
if (
|
|
4261
|
+
if (Array.isArray(v)) return v.filter((x) => isRec13(x));
|
|
4262
|
+
if (isRec13(v)) {
|
|
3986
4263
|
return Object.entries(v).map(([name, def]) => ({
|
|
3987
4264
|
name,
|
|
3988
|
-
...
|
|
4265
|
+
...isRec13(def) ? def : {}
|
|
3989
4266
|
}));
|
|
3990
4267
|
}
|
|
3991
4268
|
return [];
|
|
@@ -4003,7 +4280,7 @@ function collectActionBodies(stack) {
|
|
|
4003
4280
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4004
4281
|
if (type !== "script") return;
|
|
4005
4282
|
const body = action.body;
|
|
4006
|
-
if (!
|
|
4283
|
+
if (!isRec13(body) || body.language !== "js") return;
|
|
4007
4284
|
const source = body.source;
|
|
4008
4285
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4009
4286
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4022,7 +4299,7 @@ function collectActionBodies(stack) {
|
|
|
4022
4299
|
}
|
|
4023
4300
|
function validateActionBodyWrites(stack) {
|
|
4024
4301
|
const findings = [];
|
|
4025
|
-
if (!
|
|
4302
|
+
if (!isRec13(stack)) return findings;
|
|
4026
4303
|
const sites = collectActionBodies(stack);
|
|
4027
4304
|
if (sites.length === 0) return findings;
|
|
4028
4305
|
let objectFields = null;
|
|
@@ -4080,13 +4357,13 @@ function fixHint2(field, declared) {
|
|
|
4080
4357
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
4081
4358
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4082
4359
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4083
|
-
var
|
|
4360
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4084
4361
|
function asArray21(v) {
|
|
4085
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4086
|
-
if (
|
|
4362
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4363
|
+
if (isRec14(v)) {
|
|
4087
4364
|
return Object.entries(v).map(([name, def]) => ({
|
|
4088
4365
|
name,
|
|
4089
|
-
...
|
|
4366
|
+
...isRec14(def) ? def : {}
|
|
4090
4367
|
}));
|
|
4091
4368
|
}
|
|
4092
4369
|
return [];
|
|
@@ -4099,7 +4376,7 @@ function readLiteralObjectName(config) {
|
|
|
4099
4376
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4100
4377
|
function validateFlowNodeWrites(stack) {
|
|
4101
4378
|
const findings = [];
|
|
4102
|
-
if (!
|
|
4379
|
+
if (!isRec14(stack)) return findings;
|
|
4103
4380
|
const flows = asArray21(stack.flows);
|
|
4104
4381
|
if (flows.length === 0) return findings;
|
|
4105
4382
|
let objectFields = null;
|
|
@@ -4108,10 +4385,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4108
4385
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4109
4386
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4110
4387
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4111
|
-
const config =
|
|
4388
|
+
const config = isRec14(node.config) ? node.config : void 0;
|
|
4112
4389
|
if (!config) return;
|
|
4113
4390
|
const fields = config.fields;
|
|
4114
|
-
if (!
|
|
4391
|
+
if (!isRec14(fields)) return;
|
|
4115
4392
|
const written = Object.keys(fields);
|
|
4116
4393
|
if (written.length === 0) return;
|
|
4117
4394
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4236,14 +4513,14 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
4236
4513
|
// src/validate-react-page-props.ts
|
|
4237
4514
|
var import_node_module2 = require("module");
|
|
4238
4515
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
4239
|
-
var
|
|
4516
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
4240
4517
|
|
|
4241
4518
|
// src/zod-issue-format.ts
|
|
4242
|
-
var
|
|
4519
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4243
4520
|
var valueAtPath = (root, path) => {
|
|
4244
4521
|
let cur = root;
|
|
4245
4522
|
for (const key of path) {
|
|
4246
|
-
if (!
|
|
4523
|
+
if (!isRec15(cur) && !Array.isArray(cur)) return void 0;
|
|
4247
4524
|
cur = cur[key];
|
|
4248
4525
|
}
|
|
4249
4526
|
return cur;
|
|
@@ -4382,7 +4659,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4382
4659
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4383
4660
|
function checkChartDrillDown(raw, push2) {
|
|
4384
4661
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4385
|
-
if (!
|
|
4662
|
+
if (!isRec16(raw)) {
|
|
4386
4663
|
push2(
|
|
4387
4664
|
"error",
|
|
4388
4665
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4405,7 +4682,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4405
4682
|
}
|
|
4406
4683
|
function checkChartAggregate(raw, push2) {
|
|
4407
4684
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4408
|
-
if (!
|
|
4685
|
+
if (!isRec16(raw)) {
|
|
4409
4686
|
push2(
|
|
4410
4687
|
"error",
|
|
4411
4688
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4420,7 +4697,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4420
4697
|
"warning",
|
|
4421
4698
|
REACT_CHART_AGGREGATE_INVALID,
|
|
4422
4699
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
4423
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
4700
|
+
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
|
|
4424
4701
|
);
|
|
4425
4702
|
}
|
|
4426
4703
|
const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
|
|
@@ -4436,7 +4713,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4436
4713
|
);
|
|
4437
4714
|
}
|
|
4438
4715
|
}
|
|
4439
|
-
var
|
|
4716
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4440
4717
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4441
4718
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
4442
4719
|
const { values, where, path } = attrs;
|
|
@@ -4446,11 +4723,11 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4446
4723
|
const aggregate = values.get("aggregate");
|
|
4447
4724
|
checkChartAggregate(aggregate, push2);
|
|
4448
4725
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4449
|
-
if (!
|
|
4726
|
+
if (!isRec16(aggregate)) return;
|
|
4450
4727
|
const fn = strOf(aggregate.function);
|
|
4451
4728
|
const field = strOf(aggregate.field);
|
|
4452
4729
|
const groupBy = aggregate.groupBy;
|
|
4453
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4730
|
+
const groupByField = strOf(groupBy) ?? (isRec16(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4454
4731
|
const objectName = strOf(values.get("objectName"));
|
|
4455
4732
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4456
4733
|
if (objectName && known) {
|
|
@@ -4483,18 +4760,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4483
4760
|
);
|
|
4484
4761
|
};
|
|
4485
4762
|
const xAxisRaw = values.get("xAxis");
|
|
4486
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
4763
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec16(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4487
4764
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4488
4765
|
axisRef(categoryAxis, categoryProp);
|
|
4489
4766
|
const yAxisRaw = values.get("yAxis");
|
|
4490
4767
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4491
4768
|
for (const a of yAxisList) {
|
|
4492
|
-
axisRef(strOf(a) ?? (
|
|
4769
|
+
axisRef(strOf(a) ?? (isRec16(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4493
4770
|
}
|
|
4494
4771
|
const series = values.get("series");
|
|
4495
4772
|
if (Array.isArray(series)) {
|
|
4496
4773
|
for (const s of series) {
|
|
4497
|
-
if (!
|
|
4774
|
+
if (!isRec16(s)) continue;
|
|
4498
4775
|
const dataKey = strOf(s.dataKey);
|
|
4499
4776
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4500
4777
|
}
|
|
@@ -4550,7 +4827,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4550
4827
|
if (!Array.isArray(value)) return { child, parent };
|
|
4551
4828
|
for (let i = 0; i < value.length; i++) {
|
|
4552
4829
|
const sub = value[i];
|
|
4553
|
-
if (!
|
|
4830
|
+
if (!isRec16(sub)) continue;
|
|
4554
4831
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4555
4832
|
child.push({
|
|
4556
4833
|
objectName: strOf(sub.childObject),
|
|
@@ -4575,7 +4852,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
4575
4852
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
4576
4853
|
return;
|
|
4577
4854
|
}
|
|
4578
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
4855
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data6.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
4579
4856
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
4580
4857
|
}
|
|
4581
4858
|
}
|
|
@@ -4595,20 +4872,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4595
4872
|
}
|
|
4596
4873
|
for (const key of spec.nestedFields ?? []) {
|
|
4597
4874
|
const v = readable(key);
|
|
4598
|
-
if (
|
|
4875
|
+
if (isRec16(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4599
4876
|
}
|
|
4600
4877
|
for (const key of spec.sections ?? []) {
|
|
4601
4878
|
const v = readable(key);
|
|
4602
4879
|
if (!Array.isArray(v)) continue;
|
|
4603
4880
|
for (let i = 0; i < v.length; i++) {
|
|
4604
4881
|
const section = v[i];
|
|
4605
|
-
if (!
|
|
4882
|
+
if (!isRec16(section)) continue;
|
|
4606
4883
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4607
4884
|
}
|
|
4608
4885
|
}
|
|
4609
4886
|
for (const key of spec.keyedByField ?? []) {
|
|
4610
4887
|
const v = readable(key);
|
|
4611
|
-
if (!
|
|
4888
|
+
if (!isRec16(v)) continue;
|
|
4612
4889
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4613
4890
|
}
|
|
4614
4891
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4892,13 +5169,13 @@ function validateReferenceIntegrity(stack) {
|
|
|
4892
5169
|
|
|
4893
5170
|
// src/validate-component-props.ts
|
|
4894
5171
|
var import_ui3 = require("@objectstack/spec/ui");
|
|
4895
|
-
var
|
|
5172
|
+
var import_spec2 = require("@objectstack/spec");
|
|
4896
5173
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
4897
5174
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
4898
|
-
function
|
|
5175
|
+
function isRec17(v) {
|
|
4899
5176
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4900
5177
|
}
|
|
4901
|
-
function
|
|
5178
|
+
function strName17(v) {
|
|
4902
5179
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4903
5180
|
}
|
|
4904
5181
|
function asArray24(v) {
|
|
@@ -4912,27 +5189,27 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
|
4912
5189
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
4913
5190
|
function suppliedByDataSource(issue, component) {
|
|
4914
5191
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
4915
|
-
const dataSource =
|
|
4916
|
-
return
|
|
5192
|
+
const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
|
|
5193
|
+
return strName17(dataSource?.object) !== void 0;
|
|
4917
5194
|
}
|
|
4918
5195
|
function validateComponentProps(stack) {
|
|
4919
5196
|
const findings = [];
|
|
4920
|
-
if (!
|
|
5197
|
+
if (!isRec17(stack)) return findings;
|
|
4921
5198
|
const pages = asArray24(stack.pages);
|
|
4922
5199
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4923
5200
|
const page = pages[pi];
|
|
4924
|
-
if (!
|
|
4925
|
-
const pageName =
|
|
5201
|
+
if (!isRec17(page)) continue;
|
|
5202
|
+
const pageName = strName17(page.name) ?? `#${pi}`;
|
|
4926
5203
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4927
|
-
const type =
|
|
5204
|
+
const type = strName17(component.type);
|
|
4928
5205
|
if (!type) continue;
|
|
4929
5206
|
const schema = PROPS_SCHEMAS[type];
|
|
4930
5207
|
if (!schema) continue;
|
|
4931
|
-
const props =
|
|
5208
|
+
const props = isRec17(component.properties) ? component.properties : void 0;
|
|
4932
5209
|
if (!props) continue;
|
|
4933
5210
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
4934
5211
|
const base = `${path}.properties`;
|
|
4935
|
-
for (const f of (0,
|
|
5212
|
+
for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
|
|
4936
5213
|
findings.push({
|
|
4937
5214
|
severity: "warning",
|
|
4938
5215
|
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
@@ -5522,6 +5799,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
5522
5799
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
5523
5800
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
5524
5801
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
5802
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5525
5803
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5526
5804
|
function asArray30(v) {
|
|
5527
5805
|
if (Array.isArray(v)) return v;
|
|
@@ -5539,6 +5817,11 @@ function renderNonObject(v) {
|
|
|
5539
5817
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
5540
5818
|
return `a ${t}`;
|
|
5541
5819
|
}
|
|
5820
|
+
function renderTriggerToken(v) {
|
|
5821
|
+
if (typeof v === "string") return `'${v}'`;
|
|
5822
|
+
const json = JSON.stringify(v);
|
|
5823
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
5824
|
+
}
|
|
5542
5825
|
function startNodeOf(flow) {
|
|
5543
5826
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
5544
5827
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -5658,6 +5941,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5658
5941
|
hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
|
|
5659
5942
|
});
|
|
5660
5943
|
}
|
|
5944
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
5945
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
5946
|
+
const hasTriggerType = config.triggerType != null;
|
|
5947
|
+
findings.push({
|
|
5948
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
5949
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
5950
|
+
// with no registry lookup in them — so no installed package can make
|
|
5951
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
5952
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
5953
|
+
// record-change trigger itself, and it still would not help, because
|
|
5954
|
+
// the flow never reaches the point of asking for one.
|
|
5955
|
+
severity: "error",
|
|
5956
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
5957
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
5958
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
5959
|
+
message: `declares type: 'record_change' but ` + (hasTriggerType ? `its start node's triggerType is ${renderTriggerToken(config.triggerType)}, which the engine routes to NO trigger` : `its start node has no triggerType at all, so there is nothing for the engine to route`) + ` \u2014 it binds a record-change flow only for a token starting with 'record-', so this flow is demoted to a manual one and never fires. Nothing NAMES it: the unbound-flow audit resolves the same binding and skips the flow as "manual \u2014 nothing to bind", so neither the boot warning nor the startup summary lists it; the only trace is the banner's flow count being one higher than its bound count.`,
|
|
5960
|
+
hint: `Use record-{before,after}-{create,update,delete,write} ('write' is create OR update in one flow, #3427; create/insert are synonyms). If the flow really is launched by hand or from a screen, declare type: 'autolaunched' or 'screen' instead of 'record_change' \u2014 those types have no trigger to be missing.`
|
|
5961
|
+
});
|
|
5962
|
+
}
|
|
5661
5963
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
5662
5964
|
findings.push({
|
|
5663
5965
|
severity: "warning",
|
|
@@ -5674,7 +5976,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5674
5976
|
|
|
5675
5977
|
// src/validate-approval-approvers.ts
|
|
5676
5978
|
var import_automation4 = require("@objectstack/spec/automation");
|
|
5677
|
-
var
|
|
5979
|
+
var import_spec3 = require("@objectstack/spec");
|
|
5678
5980
|
var import_formula3 = require("@objectstack/formula");
|
|
5679
5981
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
5680
5982
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
@@ -5689,8 +5991,8 @@ var APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED = "approval-approver-cross-org-unsup
|
|
|
5689
5991
|
var EXPRESSION_ROOTS = /* @__PURE__ */ new Set(["current", "trigger", "vars"]);
|
|
5690
5992
|
var RESERVED_OUTPUT_KEYS = /* @__PURE__ */ new Set(["decision", "requestId"]);
|
|
5691
5993
|
var GROUP_ROUTED_TYPES = /* @__PURE__ */ new Set(["position", "team", "department"]);
|
|
5692
|
-
var MEMBERSHIP_TIERS = new Set(
|
|
5693
|
-
var MEMBERSHIP_TIER_LIST =
|
|
5994
|
+
var MEMBERSHIP_TIERS = new Set(import_spec3.BUILTIN_MEMBERSHIP_ROLES);
|
|
5995
|
+
var MEMBERSHIP_TIER_LIST = import_spec3.BUILTIN_MEMBERSHIP_ROLES.join("/");
|
|
5694
5996
|
var TYPE_FIX = {
|
|
5695
5997
|
business_unit: "department",
|
|
5696
5998
|
bu: "department"
|
|
@@ -5885,7 +6187,7 @@ function validateApprovalApprovers(stack) {
|
|
|
5885
6187
|
}
|
|
5886
6188
|
|
|
5887
6189
|
// src/validate-record-title.ts
|
|
5888
|
-
var
|
|
6190
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
5889
6191
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
5890
6192
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
5891
6193
|
function asArray32(v) {
|
|
@@ -5913,7 +6215,7 @@ function validateRecordTitle(stack) {
|
|
|
5913
6215
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
5914
6216
|
});
|
|
5915
6217
|
}
|
|
5916
|
-
const completeness = (0,
|
|
6218
|
+
const completeness = (0, import_data7.objectTitleCompleteness)(obj);
|
|
5917
6219
|
if (completeness.status === "none") {
|
|
5918
6220
|
findings.push({
|
|
5919
6221
|
severity: "warning",
|
|
@@ -6009,7 +6311,7 @@ function validateSemanticRoles(stack) {
|
|
|
6009
6311
|
(h) => typeof h === "string" && h.length > 0
|
|
6010
6312
|
);
|
|
6011
6313
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
6012
|
-
const declaredTitle = [obj.nameField, obj.
|
|
6314
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
6013
6315
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
6014
6316
|
const stripSet = new Set(
|
|
6015
6317
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -6044,6 +6346,12 @@ function asArray34(v) {
|
|
|
6044
6346
|
}
|
|
6045
6347
|
return [];
|
|
6046
6348
|
}
|
|
6349
|
+
function isRec18(v) {
|
|
6350
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6351
|
+
}
|
|
6352
|
+
function strName18(v) {
|
|
6353
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6354
|
+
}
|
|
6047
6355
|
function fieldNameOf(entry) {
|
|
6048
6356
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
6049
6357
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -6052,13 +6360,6 @@ function fieldNameOf(entry) {
|
|
|
6052
6360
|
}
|
|
6053
6361
|
return null;
|
|
6054
6362
|
}
|
|
6055
|
-
function boundObject(view) {
|
|
6056
|
-
const data = view.data;
|
|
6057
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
6058
|
-
return data.object;
|
|
6059
|
-
}
|
|
6060
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
6061
|
-
}
|
|
6062
6363
|
function validateFormLayout(stack) {
|
|
6063
6364
|
const findings = [];
|
|
6064
6365
|
const objectFields = /* @__PURE__ */ new Map();
|
|
@@ -6068,44 +6369,44 @@ function validateFormLayout(stack) {
|
|
|
6068
6369
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6069
6370
|
objectFields.set(name, new Set(fields));
|
|
6070
6371
|
}
|
|
6071
|
-
const
|
|
6072
|
-
|
|
6073
|
-
const
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
}
|
|
6372
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6373
|
+
const viewName = strName18(view.name) ?? strName18(view.object) ?? viewPath;
|
|
6374
|
+
const containerObject = viewObjectName(view);
|
|
6375
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6376
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
6377
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
6378
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6379
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6380
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6381
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6382
|
+
const sec = sections[s];
|
|
6383
|
+
const secFields = isRec18(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6384
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6385
|
+
const entry = secFields[f];
|
|
6386
|
+
const fname = fieldNameOf(entry);
|
|
6387
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
6388
|
+
if (fname && known && !known.has(fname)) {
|
|
6389
|
+
findings.push({
|
|
6390
|
+
severity: "warning",
|
|
6391
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
6392
|
+
where,
|
|
6393
|
+
path: fpath,
|
|
6394
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
6395
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6396
|
+
});
|
|
6397
|
+
}
|
|
6398
|
+
const colSpan = isRec18(entry) ? entry.colSpan : void 0;
|
|
6399
|
+
if (colSpan != null) {
|
|
6400
|
+
findings.push({
|
|
6401
|
+
severity: "warning",
|
|
6402
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
6403
|
+
where,
|
|
6404
|
+
path: `${fpath}.colSpan`,
|
|
6405
|
+
message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
|
|
6406
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
6407
|
+
});
|
|
6408
|
+
}
|
|
6409
|
+
}
|
|
6109
6410
|
}
|
|
6110
6411
|
}
|
|
6111
6412
|
}
|
|
@@ -6204,17 +6505,12 @@ function validateSeedStateMachine(stack) {
|
|
|
6204
6505
|
}
|
|
6205
6506
|
|
|
6206
6507
|
// src/validate-visibility-predicates.ts
|
|
6207
|
-
var
|
|
6508
|
+
var import_formula4 = require("@objectstack/formula");
|
|
6208
6509
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6510
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6511
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
6512
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
6209
6513
|
var CANONICAL = "visibleWhen";
|
|
6210
|
-
var ALIASES = ["visibleOn", "visibility"];
|
|
6211
|
-
function asArray35(v) {
|
|
6212
|
-
if (Array.isArray(v)) return v;
|
|
6213
|
-
if (v && typeof v === "object") {
|
|
6214
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6215
|
-
}
|
|
6216
|
-
return [];
|
|
6217
|
-
}
|
|
6218
6514
|
function predicateSource(v) {
|
|
6219
6515
|
if (typeof v === "string") return v;
|
|
6220
6516
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
@@ -6225,6 +6521,67 @@ function predicateSource(v) {
|
|
|
6225
6521
|
function usesRoot(source, root) {
|
|
6226
6522
|
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
6227
6523
|
}
|
|
6524
|
+
function withoutStringLiterals(source) {
|
|
6525
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
6526
|
+
}
|
|
6527
|
+
var NON_CEL_SPELLINGS = [
|
|
6528
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
6529
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
6530
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
6531
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
6532
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
6533
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
6534
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
6535
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
6536
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
6537
|
+
];
|
|
6538
|
+
function quoteSource(source) {
|
|
6539
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
6540
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
6541
|
+
}
|
|
6542
|
+
function celRefusal(source) {
|
|
6543
|
+
if (!source.trim()) return null;
|
|
6544
|
+
const parsed = (0, import_formula4.parseCelToAstWithReason)(source);
|
|
6545
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
6546
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
6547
|
+
const identifiers = (0, import_formula4.collectCelRootIdentifiers)(source);
|
|
6548
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
6549
|
+
const scannable = withoutStringLiterals(source);
|
|
6550
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
6551
|
+
}
|
|
6552
|
+
function boundName(overrun) {
|
|
6553
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6554
|
+
}
|
|
6555
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6556
|
+
function isNode2(v) {
|
|
6557
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6558
|
+
}
|
|
6559
|
+
function namespaceRoots(node, out) {
|
|
6560
|
+
if (Array.isArray(node)) {
|
|
6561
|
+
for (const child of node) namespaceRoots(child, out);
|
|
6562
|
+
return;
|
|
6563
|
+
}
|
|
6564
|
+
if (!isNode2(node)) return;
|
|
6565
|
+
const args = node.args;
|
|
6566
|
+
if (Array.isArray(args)) {
|
|
6567
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6568
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6569
|
+
out.add(receiver.args);
|
|
6570
|
+
}
|
|
6571
|
+
}
|
|
6572
|
+
namespaceRoots(args, out);
|
|
6573
|
+
}
|
|
6574
|
+
function firstBareIdentifier(source) {
|
|
6575
|
+
const ast = (0, import_formula4.parseCelToAst)(source);
|
|
6576
|
+
if (!ast) return null;
|
|
6577
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
6578
|
+
namespaceRoots(ast, rooted);
|
|
6579
|
+
return (0, import_formula4.firstUndeclaredReference)(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
|
|
6580
|
+
}
|
|
6581
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
6582
|
+
runtime: "record",
|
|
6583
|
+
metadata: "data"
|
|
6584
|
+
};
|
|
6228
6585
|
var MISLAYER_BY_LAYER = {
|
|
6229
6586
|
runtime: {
|
|
6230
6587
|
forbiddenRoot: "data",
|
|
@@ -6238,18 +6595,6 @@ var MISLAYER_BY_LAYER = {
|
|
|
6238
6595
|
}
|
|
6239
6596
|
};
|
|
6240
6597
|
function checkElement(el, where, path, layer, findings) {
|
|
6241
|
-
for (const alias of ALIASES) {
|
|
6242
|
-
if (el[alias] !== void 0) {
|
|
6243
|
-
findings.push({
|
|
6244
|
-
severity: "warning",
|
|
6245
|
-
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
6246
|
-
where,
|
|
6247
|
-
path: `${path}.${alias}`,
|
|
6248
|
-
message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
|
|
6249
|
-
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
6250
|
-
});
|
|
6251
|
-
}
|
|
6252
|
-
}
|
|
6253
6598
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6254
6599
|
const source = predicateSource(raw);
|
|
6255
6600
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -6263,6 +6608,43 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6263
6608
|
hint: rule.hint
|
|
6264
6609
|
});
|
|
6265
6610
|
}
|
|
6611
|
+
const refusal = source ? celRefusal(source) : null;
|
|
6612
|
+
if (source && refusal?.kind === "bounds") {
|
|
6613
|
+
const bound = boundName(refusal.overrun);
|
|
6614
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6615
|
+
findings.push({
|
|
6616
|
+
severity: "error",
|
|
6617
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
6618
|
+
where,
|
|
6619
|
+
path,
|
|
6620
|
+
message: `visibility predicate is syntactically valid CEL but overruns ${bound} (${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front end refuses it, so it can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
6621
|
+
hint: `There is no syntax or dialect error to correct here \u2014 this is a SIZE fault, not a dialect mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work off the predicate: (1) collapse a long \`${root}.f == 'a' || ${root}.f == 'b' || \u2026\` chain into a single \`${root}.f in ['a', 'b', \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2); (2) precompute the heavy part into a formula/rollup field on the object and test that one field instead. Logic genuinely this large is not element visibility \u2014 compute it once on the record rather than re-deriving it in every predicate that needs it.`
|
|
6622
|
+
});
|
|
6623
|
+
}
|
|
6624
|
+
if (source && refusal?.kind === "syntax") {
|
|
6625
|
+
findings.push({
|
|
6626
|
+
severity: "error",
|
|
6627
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
6628
|
+
where,
|
|
6629
|
+
path,
|
|
6630
|
+
message: `visibility predicate is not valid CEL \u2014 ${refusal.detail} (predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
6631
|
+
hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator \u2014 CEL spells it \`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with \`${refusal.token.cel}\`, e.g. \`${refusal.token.example}\`.` : `Visibility predicates are bare CEL, e.g. \`record.status == 'open'\`. Spellings from other languages do not parse: write \`==\` (not \`===\`), \`!=\` (not \`!==\` or \`<>\`), \`&&\` (not \`and\`), \`||\` (not \`or\`), \`!\` (not \`not\`).`
|
|
6632
|
+
});
|
|
6633
|
+
}
|
|
6634
|
+
if (source && !refusal) {
|
|
6635
|
+
const bare = firstBareIdentifier(source);
|
|
6636
|
+
if (bare) {
|
|
6637
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6638
|
+
findings.push({
|
|
6639
|
+
severity: "error",
|
|
6640
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
6641
|
+
where,
|
|
6642
|
+
path,
|
|
6643
|
+
message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
6644
|
+
hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a `*.form.ts` metadata-editing form binds the row under edit as `data`).")
|
|
6645
|
+
});
|
|
6646
|
+
}
|
|
6647
|
+
}
|
|
6266
6648
|
}
|
|
6267
6649
|
function isFieldObject(entry) {
|
|
6268
6650
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
@@ -6270,43 +6652,304 @@ function isFieldObject(entry) {
|
|
|
6270
6652
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
6271
6653
|
const layer = opts.layer ?? "runtime";
|
|
6272
6654
|
const findings = [];
|
|
6273
|
-
const
|
|
6274
|
-
|
|
6275
|
-
const view
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
6655
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6656
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6657
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6658
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6659
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6660
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6661
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6662
|
+
const sec = sections[s];
|
|
6663
|
+
if (!sec || typeof sec !== "object") continue;
|
|
6664
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
6665
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
6666
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
6667
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6668
|
+
const entry = secFields[f];
|
|
6669
|
+
if (isFieldObject(entry)) {
|
|
6670
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
6671
|
+
}
|
|
6291
6672
|
}
|
|
6292
6673
|
}
|
|
6293
6674
|
}
|
|
6294
6675
|
}
|
|
6295
6676
|
}
|
|
6296
|
-
const
|
|
6297
|
-
|
|
6298
|
-
const
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6677
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6678
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
6679
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
6680
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6681
|
+
checkElement(walked.component, where, walked.path, layer, findings);
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
return findings;
|
|
6685
|
+
}
|
|
6686
|
+
|
|
6687
|
+
// src/validate-predicate-path-refs.ts
|
|
6688
|
+
var import_formula5 = require("@objectstack/formula");
|
|
6689
|
+
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
6690
|
+
var import_spec4 = require("@objectstack/spec");
|
|
6691
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
6692
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
6693
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
6694
|
+
var ROOT = "data";
|
|
6695
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6696
|
+
function defOf(schema) {
|
|
6697
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
6698
|
+
const s = schema;
|
|
6699
|
+
return s.def ?? s._def;
|
|
6700
|
+
}
|
|
6701
|
+
function peel(schema, depth = 0) {
|
|
6702
|
+
if (!schema || depth > 25) return schema;
|
|
6703
|
+
const d = defOf(schema);
|
|
6704
|
+
if (!d) return schema;
|
|
6705
|
+
switch (d.type) {
|
|
6706
|
+
case "optional":
|
|
6707
|
+
case "nullable":
|
|
6708
|
+
case "default":
|
|
6709
|
+
case "prefault":
|
|
6710
|
+
case "readonly":
|
|
6711
|
+
case "catch":
|
|
6712
|
+
case "nonoptional":
|
|
6713
|
+
return peel(d.innerType, depth + 1);
|
|
6714
|
+
case "lazy":
|
|
6715
|
+
return peel(d.getter(), depth + 1);
|
|
6716
|
+
case "pipe": {
|
|
6717
|
+
const inner = peel(d.in, depth + 1);
|
|
6718
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
6719
|
+
}
|
|
6720
|
+
default:
|
|
6721
|
+
return schema;
|
|
6722
|
+
}
|
|
6723
|
+
}
|
|
6724
|
+
function optionsOf(d) {
|
|
6725
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
6726
|
+
}
|
|
6727
|
+
function keysOf(schema, depth = 0) {
|
|
6728
|
+
if (depth > 25) return null;
|
|
6729
|
+
const u = peel(schema);
|
|
6730
|
+
const d = defOf(u);
|
|
6731
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
6732
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
6733
|
+
const all = /* @__PURE__ */ new Set();
|
|
6734
|
+
let keyBearing = false;
|
|
6735
|
+
for (const option of optionsOf(d)) {
|
|
6736
|
+
const k = keysOf(option, depth + 1);
|
|
6737
|
+
if (!k) continue;
|
|
6738
|
+
keyBearing = true;
|
|
6739
|
+
for (const key of k) all.add(key);
|
|
6740
|
+
}
|
|
6741
|
+
return keyBearing ? [...all] : null;
|
|
6742
|
+
}
|
|
6743
|
+
if (d?.type === "intersection") {
|
|
6744
|
+
const left = keysOf(d.left, depth + 1);
|
|
6745
|
+
const right = keysOf(d.right, depth + 1);
|
|
6746
|
+
if (!left && !right) return null;
|
|
6747
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
6748
|
+
}
|
|
6749
|
+
return null;
|
|
6750
|
+
}
|
|
6751
|
+
function propertyOf(schema, key, depth = 0) {
|
|
6752
|
+
if (depth > 25) return void 0;
|
|
6753
|
+
const u = peel(schema);
|
|
6754
|
+
const d = defOf(u);
|
|
6755
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
6756
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
6757
|
+
for (const option of optionsOf(d)) {
|
|
6758
|
+
const found = propertyOf(option, key, depth + 1);
|
|
6759
|
+
if (found !== void 0) return found;
|
|
6760
|
+
}
|
|
6761
|
+
}
|
|
6762
|
+
if (d?.type === "intersection") {
|
|
6763
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
6764
|
+
}
|
|
6765
|
+
return void 0;
|
|
6766
|
+
}
|
|
6767
|
+
function rowScopeOf(scope, key) {
|
|
6768
|
+
const prop = propertyOf(scope, key);
|
|
6769
|
+
if (prop === void 0) return void 0;
|
|
6770
|
+
let node = peel(prop);
|
|
6771
|
+
for (let i = 0; i < 25; i++) {
|
|
6772
|
+
const d = defOf(node);
|
|
6773
|
+
if (d?.type === "array") node = peel(d.element);
|
|
6774
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
6775
|
+
else return node;
|
|
6776
|
+
}
|
|
6777
|
+
return node;
|
|
6778
|
+
}
|
|
6779
|
+
function stepInto(scope, segment) {
|
|
6780
|
+
const u = peel(scope);
|
|
6781
|
+
const d = defOf(u);
|
|
6782
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
6783
|
+
const declared = keysOf(u);
|
|
6784
|
+
if (declared === null) return { kind: "opaque" };
|
|
6785
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
6786
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
6787
|
+
}
|
|
6788
|
+
function isNode3(v) {
|
|
6789
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6790
|
+
}
|
|
6791
|
+
function memberChain(node) {
|
|
6792
|
+
if (!isNode3(node)) return null;
|
|
6793
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
6794
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
6795
|
+
const head = memberChain(node.args[0]);
|
|
6796
|
+
return head ? [...head, node.args[1]] : null;
|
|
6797
|
+
}
|
|
6798
|
+
return null;
|
|
6799
|
+
}
|
|
6800
|
+
function rootedPaths(node, out) {
|
|
6801
|
+
if (Array.isArray(node)) {
|
|
6802
|
+
for (const child of node) rootedPaths(child, out);
|
|
6803
|
+
return;
|
|
6804
|
+
}
|
|
6805
|
+
if (!isNode3(node)) return;
|
|
6806
|
+
if (node.op === ".") {
|
|
6807
|
+
const chain = memberChain(node);
|
|
6808
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
6809
|
+
out.push(chain.slice(1));
|
|
6810
|
+
return;
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
rootedPaths(node.args, out);
|
|
6814
|
+
}
|
|
6815
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
6816
|
+
if (Array.isArray(node)) {
|
|
6817
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
6818
|
+
return;
|
|
6819
|
+
}
|
|
6820
|
+
if (!isNode3(node)) return;
|
|
6821
|
+
const args = node.args;
|
|
6822
|
+
if (Array.isArray(args)) {
|
|
6823
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6824
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6825
|
+
excluded.add(receiver.args);
|
|
6826
|
+
}
|
|
6827
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6828
|
+
const macroArgs = args[2];
|
|
6829
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
6830
|
+
const bound = macroArgs[0];
|
|
6831
|
+
if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
6832
|
+
excluded.add(bound.args);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
}
|
|
6837
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
6838
|
+
values.add(node.args);
|
|
6839
|
+
return;
|
|
6840
|
+
}
|
|
6841
|
+
classifyIdentifiers(args, values, excluded);
|
|
6842
|
+
}
|
|
6843
|
+
function predicateSource2(v) {
|
|
6844
|
+
if (typeof v === "string") return v;
|
|
6845
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
6846
|
+
return v.source;
|
|
6847
|
+
}
|
|
6848
|
+
return void 0;
|
|
6849
|
+
}
|
|
6850
|
+
function isRec19(v) {
|
|
6851
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6852
|
+
}
|
|
6853
|
+
function schemaIdOf(view) {
|
|
6854
|
+
const data = view.data;
|
|
6855
|
+
if (!isRec19(data)) return void 0;
|
|
6856
|
+
if (data.provider !== "schema") return void 0;
|
|
6857
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6858
|
+
}
|
|
6859
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
6860
|
+
const ast = (0, import_formula5.parseCelToAst)(source);
|
|
6861
|
+
if (!ast) return;
|
|
6862
|
+
const paths = [];
|
|
6863
|
+
rootedPaths(ast, paths);
|
|
6864
|
+
for (const segments of paths) {
|
|
6865
|
+
let cursor = scope;
|
|
6866
|
+
const walked = [];
|
|
6867
|
+
for (const segment of segments) {
|
|
6868
|
+
const step = stepInto(cursor, segment);
|
|
6869
|
+
if (step.kind === "opaque") break;
|
|
6870
|
+
if (step.kind === "undeclared") {
|
|
6871
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
6872
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
6873
|
+
findings.push({
|
|
6874
|
+
severity: "error",
|
|
6875
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
6876
|
+
where,
|
|
6877
|
+
path,
|
|
6878
|
+
message: `predicate references \`${full}\`, which the target schema does not declare \u2014 \`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, so the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149).`,
|
|
6879
|
+
hint: `${(0, import_spec4.formatSuggestion)((0, import_spec4.findClosestMatches)(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
6880
|
+
});
|
|
6881
|
+
break;
|
|
6882
|
+
}
|
|
6883
|
+
walked.push(segment);
|
|
6884
|
+
cursor = step.next;
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
const declaredHere = keysOf(scope);
|
|
6888
|
+
if (!declaredHere) return;
|
|
6889
|
+
const values = /* @__PURE__ */ new Set();
|
|
6890
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
6891
|
+
classifyIdentifiers(ast, values, excluded);
|
|
6892
|
+
for (const id of values) {
|
|
6893
|
+
if (excluded.has(id) || !declaredHere.includes(id)) continue;
|
|
6894
|
+
findings.push({
|
|
6895
|
+
severity: "error",
|
|
6896
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
6897
|
+
where,
|
|
6898
|
+
path,
|
|
6899
|
+
message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
|
|
6900
|
+
hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
|
|
6901
|
+
});
|
|
6902
|
+
}
|
|
6903
|
+
}
|
|
6904
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
6905
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
6906
|
+
for (let i = 0; i < entries.length; i++) {
|
|
6907
|
+
const entry = entries[i];
|
|
6908
|
+
if (!isRec19(entry)) continue;
|
|
6909
|
+
const path = `${base}[${i}]`;
|
|
6910
|
+
for (const key of PREDICATE_KEYS) {
|
|
6911
|
+
const source = predicateSource2(entry[key]);
|
|
6912
|
+
if (source !== void 0 && source.trim()) {
|
|
6913
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
6914
|
+
break;
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
6918
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
6919
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
6920
|
+
}
|
|
6921
|
+
}
|
|
6922
|
+
}
|
|
6923
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
6924
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => (0, import_kernel2.getMetadataTypeSchema)(schemaId));
|
|
6925
|
+
const findings = [];
|
|
6926
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6927
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6928
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6929
|
+
const schemaId = schemaIdOf(site.view);
|
|
6930
|
+
if (!schemaId) continue;
|
|
6931
|
+
let root;
|
|
6932
|
+
try {
|
|
6933
|
+
root = resolveSchema(schemaId);
|
|
6934
|
+
} catch {
|
|
6935
|
+
continue;
|
|
6936
|
+
}
|
|
6937
|
+
if (!root) continue;
|
|
6938
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
6939
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6940
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6941
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6942
|
+
const section = sections[s];
|
|
6943
|
+
if (!isRec19(section)) continue;
|
|
6944
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
6945
|
+
for (const key of PREDICATE_KEYS) {
|
|
6946
|
+
const source = predicateSource2(section[key]);
|
|
6947
|
+
if (source !== void 0 && source.trim()) {
|
|
6948
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
6949
|
+
break;
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
6310
6953
|
}
|
|
6311
6954
|
}
|
|
6312
6955
|
}
|
|
@@ -6340,7 +6983,7 @@ var OWD_WIDTH = {
|
|
|
6340
6983
|
public_read: 1,
|
|
6341
6984
|
public_read_write: 2
|
|
6342
6985
|
};
|
|
6343
|
-
function
|
|
6986
|
+
function asArray35(v) {
|
|
6344
6987
|
if (Array.isArray(v)) return v;
|
|
6345
6988
|
if (v && typeof v === "object") {
|
|
6346
6989
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6366,7 +7009,7 @@ function refOf(def) {
|
|
|
6366
7009
|
return typeof r === "string" && r ? r : void 0;
|
|
6367
7010
|
}
|
|
6368
7011
|
function firstMasterDetailField(obj) {
|
|
6369
|
-
for (const f of
|
|
7012
|
+
for (const f of asArray35(obj.fields)) {
|
|
6370
7013
|
if (f.type === "master_detail") {
|
|
6371
7014
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
6372
7015
|
}
|
|
@@ -6379,8 +7022,8 @@ function grantsObjectAccess(p) {
|
|
|
6379
7022
|
function validateSecurityPosture(stack, opts) {
|
|
6380
7023
|
const findings = [];
|
|
6381
7024
|
if (!stack || typeof stack !== "object") return findings;
|
|
6382
|
-
const objects =
|
|
6383
|
-
const permissionSets =
|
|
7025
|
+
const objects = asArray35(stack.objects);
|
|
7026
|
+
const permissionSets = asArray35(stack.permissions);
|
|
6384
7027
|
for (let i = 0; i < objects.length; i++) {
|
|
6385
7028
|
const obj = objects[i];
|
|
6386
7029
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -6509,10 +7152,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6509
7152
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
6510
7153
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
6511
7154
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
6512
|
-
for (const f of
|
|
7155
|
+
for (const f of asArray35(obj.fields)) {
|
|
6513
7156
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
6514
7157
|
}
|
|
6515
|
-
for (const [ai, action] of
|
|
7158
|
+
for (const [ai, action] of asArray35(obj.actions).entries()) {
|
|
6516
7159
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
6517
7160
|
}
|
|
6518
7161
|
}
|
|
@@ -6521,19 +7164,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6521
7164
|
if (!ps || typeof ps !== "object") continue;
|
|
6522
7165
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
6523
7166
|
}
|
|
6524
|
-
for (const [i, pos] of
|
|
7167
|
+
for (const [i, pos] of asArray35(stack.positions).entries()) {
|
|
6525
7168
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
6526
7169
|
}
|
|
6527
|
-
for (const [i, app] of
|
|
7170
|
+
for (const [i, app] of asArray35(stack.apps).entries()) {
|
|
6528
7171
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
6529
7172
|
}
|
|
6530
|
-
for (const [i, book] of
|
|
7173
|
+
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
6531
7174
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
6532
7175
|
}
|
|
6533
7176
|
const stackSetNames = new Set(
|
|
6534
7177
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
6535
7178
|
);
|
|
6536
|
-
for (const [i, book] of
|
|
7179
|
+
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
6537
7180
|
const audience = book.audience;
|
|
6538
7181
|
if (!audience || typeof audience !== "object") continue;
|
|
6539
7182
|
const setName = audience.permissionSet;
|
|
@@ -6611,7 +7254,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6611
7254
|
}
|
|
6612
7255
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
6613
7256
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
6614
|
-
for (const [i, seed] of
|
|
7257
|
+
for (const [i, seed] of asArray35(stack.data).entries()) {
|
|
6615
7258
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
6616
7259
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
6617
7260
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -6656,7 +7299,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
|
6656
7299
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
6657
7300
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
6658
7301
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
6659
|
-
function
|
|
7302
|
+
function asArray36(v) {
|
|
6660
7303
|
if (Array.isArray(v)) return v;
|
|
6661
7304
|
if (v && typeof v === "object") {
|
|
6662
7305
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6686,9 +7329,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
6686
7329
|
function validateOrgAxisRedLines(stack) {
|
|
6687
7330
|
const findings = [];
|
|
6688
7331
|
const cfg = stack ?? {};
|
|
6689
|
-
const permissionSets =
|
|
7332
|
+
const permissionSets = asArray36(cfg.permissions);
|
|
6690
7333
|
permissionSets.forEach((ps, psIndex) => {
|
|
6691
|
-
|
|
7334
|
+
asArray36(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
6692
7335
|
for (const clause of ["using", "check"]) {
|
|
6693
7336
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
6694
7337
|
findings.push({
|
|
@@ -6702,7 +7345,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6702
7345
|
}
|
|
6703
7346
|
});
|
|
6704
7347
|
});
|
|
6705
|
-
|
|
7348
|
+
asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
6706
7349
|
const slots = [
|
|
6707
7350
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
6708
7351
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -6720,9 +7363,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6720
7363
|
}
|
|
6721
7364
|
});
|
|
6722
7365
|
const tenancyDisabledObjects = new Set(
|
|
6723
|
-
|
|
7366
|
+
asArray36(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
6724
7367
|
);
|
|
6725
|
-
|
|
7368
|
+
asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
6726
7369
|
const target = str(rule.object);
|
|
6727
7370
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
6728
7371
|
const sharedWith = rule.sharedWith;
|
|
@@ -6742,10 +7385,10 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6742
7385
|
}
|
|
6743
7386
|
|
|
6744
7387
|
// src/validate-sharing-rule-enforceability.ts
|
|
6745
|
-
var
|
|
7388
|
+
var import_formula6 = require("@objectstack/formula");
|
|
6746
7389
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
6747
7390
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
6748
|
-
function
|
|
7391
|
+
function asArray37(v) {
|
|
6749
7392
|
if (Array.isArray(v)) return v;
|
|
6750
7393
|
if (v && typeof v === "object") {
|
|
6751
7394
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6772,10 +7415,10 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
6772
7415
|
function validateSharingRuleEnforceability(stack) {
|
|
6773
7416
|
const findings = [];
|
|
6774
7417
|
const cfg = stack ?? {};
|
|
6775
|
-
|
|
7418
|
+
asArray37(cfg.sharingRules).forEach((rule, index) => {
|
|
6776
7419
|
const input = toCompilerInput(rule.condition);
|
|
6777
7420
|
if (input === null) return;
|
|
6778
|
-
const result = (0,
|
|
7421
|
+
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
6779
7422
|
if (result.ok) return;
|
|
6780
7423
|
if (result.reason === "parse-error") return;
|
|
6781
7424
|
const name = str2(rule.name) || String(index);
|
|
@@ -6808,10 +7451,11 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
6808
7451
|
}
|
|
6809
7452
|
|
|
6810
7453
|
// src/validate-rls-predicate-enforceability.ts
|
|
6811
|
-
var
|
|
7454
|
+
var import_formula7 = require("@objectstack/formula");
|
|
6812
7455
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
6813
7456
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
6814
|
-
|
|
7457
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
7458
|
+
function asArray38(v) {
|
|
6815
7459
|
if (Array.isArray(v)) return v;
|
|
6816
7460
|
if (v && typeof v === "object") {
|
|
6817
7461
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6822,6 +7466,13 @@ function str3(v) {
|
|
|
6822
7466
|
return typeof v === "string" ? v : "";
|
|
6823
7467
|
}
|
|
6824
7468
|
var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
|
|
7469
|
+
function boundsOverrunOf(bridged) {
|
|
7470
|
+
const parsed = (0, import_formula7.parseCelToAstWithReason)(bridged);
|
|
7471
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
7472
|
+
}
|
|
7473
|
+
function quote(source) {
|
|
7474
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
7475
|
+
}
|
|
6825
7476
|
function consequence(clause) {
|
|
6826
7477
|
const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
|
|
6827
7478
|
return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
|
|
@@ -6829,20 +7480,36 @@ function consequence(clause) {
|
|
|
6829
7480
|
function validateRlsPredicateEnforceability(stack) {
|
|
6830
7481
|
const findings = [];
|
|
6831
7482
|
const cfg = stack ?? {};
|
|
6832
|
-
|
|
6833
|
-
|
|
7483
|
+
asArray38(cfg.permissions).forEach((ps, psIndex) => {
|
|
7484
|
+
asArray38(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
6834
7485
|
for (const clause of ["using", "check"]) {
|
|
6835
7486
|
const source = str3(policy[clause]);
|
|
6836
7487
|
if (!source.trim()) continue;
|
|
6837
|
-
if ((0,
|
|
6838
|
-
const
|
|
7488
|
+
if ((0, import_formula7.isSupportedRlsExpression)(source)) continue;
|
|
7489
|
+
const bridged = (0, import_formula7.sqlPredicateToCel)(source);
|
|
7490
|
+
const why = (0, import_formula7.isPushdownableCel)(bridged);
|
|
6839
7491
|
const detail = why.ok ? "" : why.detail;
|
|
6840
7492
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
7493
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
6841
7494
|
const psName = str3(ps.name) || String(psIndex);
|
|
6842
7495
|
const policyName = str3(policy.name) || String(pIndex);
|
|
6843
7496
|
const object = str3(policy.object);
|
|
6844
7497
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
6845
7498
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
7499
|
+
if (overrun) {
|
|
7500
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
7501
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
7502
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
7503
|
+
findings.push({
|
|
7504
|
+
severity: "error",
|
|
7505
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
7506
|
+
where,
|
|
7507
|
+
path,
|
|
7508
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
7509
|
+
hint: `There is no syntax or dialect error to correct here \u2014 the predicate is well-formed CEL and is simply too large for ${bound}${budget}, so the fix is to make it smaller or to move the work off the predicate. (1) Collapse a long \`field == a || field == b || \u2026\` chain into a single \`field in [a, b, \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2). (2) Pre-resolve the set into a membership key the runtime exposes and test \`field in current_user.<key>\` (ADR-0105 D11) \u2014 one comparison whatever the set size. (3) Denormalise a repeated sub-expression onto this object as a formula/rollup field and test that single column. (4) Split a TOP-LEVEL \`||\` across several \`rowLevelSecurity\` policies: applicable policies are OR-ed, so that is equivalent \u2014 but never split a top-level \`&&\` this way, which would WIDEN access rather than preserve it. Logic genuinely this large is not a row filter: move it to a hook or action body (\`ScriptBody { language: 'js' }\`, the L2 sandboxed surface).`
|
|
7510
|
+
});
|
|
7511
|
+
continue;
|
|
7512
|
+
}
|
|
6846
7513
|
if (parseError) {
|
|
6847
7514
|
findings.push({
|
|
6848
7515
|
severity: "error",
|
|
@@ -6874,11 +7541,11 @@ var import_meta4 = {};
|
|
|
6874
7541
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
6875
7542
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
6876
7543
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
6877
|
-
var
|
|
6878
|
-
function
|
|
6879
|
-
if (Array.isArray(v)) return v.filter(
|
|
6880
|
-
if (
|
|
6881
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7544
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7545
|
+
function asArray39(v) {
|
|
7546
|
+
if (Array.isArray(v)) return v.filter(isRec20);
|
|
7547
|
+
if (isRec20(v)) {
|
|
7548
|
+
return Object.entries(v).filter(([, def]) => isRec20(def)).map(([name, def]) => ({ name, ...def }));
|
|
6882
7549
|
}
|
|
6883
7550
|
return [];
|
|
6884
7551
|
}
|
|
@@ -6895,7 +7562,7 @@ function loadAjv() {
|
|
|
6895
7562
|
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
|
|
6896
7563
|
);
|
|
6897
7564
|
}
|
|
6898
|
-
const ctor =
|
|
7565
|
+
const ctor = isRec20(mod) && "default" in mod ? mod.default : mod;
|
|
6899
7566
|
cachedAjv = ctor;
|
|
6900
7567
|
return ctor;
|
|
6901
7568
|
}
|
|
@@ -6910,7 +7577,7 @@ function loadAddFormats() {
|
|
|
6910
7577
|
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
|
|
6911
7578
|
);
|
|
6912
7579
|
}
|
|
6913
|
-
const plugin =
|
|
7580
|
+
const plugin = isRec20(mod) && "default" in mod ? mod.default : mod;
|
|
6914
7581
|
cachedAddFormats = plugin;
|
|
6915
7582
|
return plugin;
|
|
6916
7583
|
}
|
|
@@ -6940,17 +7607,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
6940
7607
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
6941
7608
|
for (const branch of ["then", "otherwise"]) {
|
|
6942
7609
|
const nested = rule[branch];
|
|
6943
|
-
if (
|
|
7610
|
+
if (isRec20(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
6944
7611
|
}
|
|
6945
7612
|
return out;
|
|
6946
7613
|
}
|
|
6947
7614
|
function walkObjectValidationRules(stack) {
|
|
6948
7615
|
const walked = [];
|
|
6949
|
-
if (!
|
|
6950
|
-
for (const obj of
|
|
7616
|
+
if (!isRec20(stack)) return walked;
|
|
7617
|
+
for (const obj of asArray39(stack.objects)) {
|
|
6951
7618
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6952
7619
|
const validations = obj.validations;
|
|
6953
|
-
for (const authored of
|
|
7620
|
+
for (const authored of asArray39(validations)) {
|
|
6954
7621
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
6955
7622
|
walked.push({
|
|
6956
7623
|
rule,
|
|
@@ -6981,7 +7648,7 @@ function validateRuleCompilability(stack) {
|
|
|
6981
7648
|
});
|
|
6982
7649
|
}
|
|
6983
7650
|
}
|
|
6984
|
-
if (rule.type === "json_schema" &&
|
|
7651
|
+
if (rule.type === "json_schema" && isRec20(rule.schema)) {
|
|
6985
7652
|
try {
|
|
6986
7653
|
createRuntimeAjv().compile(rule.schema);
|
|
6987
7654
|
} catch (err) {
|
|
@@ -7001,7 +7668,7 @@ function validateRuleCompilability(stack) {
|
|
|
7001
7668
|
|
|
7002
7669
|
// src/validate-rule-schema-formats.ts
|
|
7003
7670
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7004
|
-
var
|
|
7671
|
+
var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7005
7672
|
var SUBSCHEMA_KEYS = [
|
|
7006
7673
|
"additionalItems",
|
|
7007
7674
|
"additionalProperties",
|
|
@@ -7025,7 +7692,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7025
7692
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7026
7693
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7027
7694
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7028
|
-
if (!
|
|
7695
|
+
if (!isRec21(schema)) return;
|
|
7029
7696
|
if (typeof schema.format === "string") {
|
|
7030
7697
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7031
7698
|
}
|
|
@@ -7044,7 +7711,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7044
7711
|
}
|
|
7045
7712
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7046
7713
|
const value = schema[key];
|
|
7047
|
-
if (!
|
|
7714
|
+
if (!isRec21(value)) continue;
|
|
7048
7715
|
for (const [name, entry] of Object.entries(value)) {
|
|
7049
7716
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7050
7717
|
}
|
|
@@ -7052,13 +7719,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7052
7719
|
const items = schema.items;
|
|
7053
7720
|
if (Array.isArray(items)) {
|
|
7054
7721
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7055
|
-
} else if (
|
|
7722
|
+
} else if (isRec21(items)) {
|
|
7056
7723
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7057
7724
|
}
|
|
7058
7725
|
const dependencies = schema.dependencies;
|
|
7059
|
-
if (
|
|
7726
|
+
if (isRec21(dependencies)) {
|
|
7060
7727
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7061
|
-
if (!
|
|
7728
|
+
if (!isRec21(entry)) continue;
|
|
7062
7729
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7063
7730
|
}
|
|
7064
7731
|
}
|
|
@@ -7097,7 +7764,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7097
7764
|
const findings = [];
|
|
7098
7765
|
const pending = [];
|
|
7099
7766
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7100
|
-
if (rule.type !== "json_schema" || !
|
|
7767
|
+
if (rule.type !== "json_schema" || !isRec21(rule.schema)) continue;
|
|
7101
7768
|
const uses = [];
|
|
7102
7769
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7103
7770
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7123,14 +7790,14 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7123
7790
|
|
|
7124
7791
|
// src/validate-action-locations.ts
|
|
7125
7792
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
7126
|
-
function
|
|
7793
|
+
function asArray40(v) {
|
|
7127
7794
|
if (Array.isArray(v)) return v;
|
|
7128
7795
|
if (v && typeof v === "object") {
|
|
7129
7796
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7130
7797
|
}
|
|
7131
7798
|
return [];
|
|
7132
7799
|
}
|
|
7133
|
-
function
|
|
7800
|
+
function strName19(v) {
|
|
7134
7801
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7135
7802
|
}
|
|
7136
7803
|
function strList3(v) {
|
|
@@ -7144,8 +7811,8 @@ function collectNamePlacedActions(stack) {
|
|
|
7144
7811
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
7145
7812
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7146
7813
|
}
|
|
7147
|
-
for (const def of
|
|
7148
|
-
const n =
|
|
7814
|
+
for (const def of asArray40(list3.bulkActionDefs)) {
|
|
7815
|
+
const n = strName19(def?.name);
|
|
7149
7816
|
if (n) placed.add(n);
|
|
7150
7817
|
}
|
|
7151
7818
|
};
|
|
@@ -7153,12 +7820,12 @@ function collectNamePlacedActions(stack) {
|
|
|
7153
7820
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
7154
7821
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
7155
7822
|
};
|
|
7156
|
-
for (const view of
|
|
7823
|
+
for (const view of asArray40(stack.views)) {
|
|
7157
7824
|
if (!view || typeof view !== "object") continue;
|
|
7158
7825
|
harvest(view.list);
|
|
7159
7826
|
harvestListViews(view.listViews);
|
|
7160
7827
|
}
|
|
7161
|
-
for (const obj of
|
|
7828
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7162
7829
|
if (!obj || typeof obj !== "object") continue;
|
|
7163
7830
|
harvestListViews(obj.listViews);
|
|
7164
7831
|
}
|
|
@@ -7171,7 +7838,7 @@ function validateActionLocations(stack) {
|
|
|
7171
7838
|
const check = (action, path) => {
|
|
7172
7839
|
if (!action || typeof action !== "object") return;
|
|
7173
7840
|
if ("locations" in action) return;
|
|
7174
|
-
const name =
|
|
7841
|
+
const name = strName19(action.name);
|
|
7175
7842
|
if (!name) return;
|
|
7176
7843
|
if (namePlaced.has(name)) return;
|
|
7177
7844
|
findings.push({
|
|
@@ -7180,16 +7847,16 @@ function validateActionLocations(stack) {
|
|
|
7180
7847
|
where: `action "${name}"`,
|
|
7181
7848
|
path,
|
|
7182
7849
|
message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
|
|
7183
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
7850
|
+
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
|
|
7184
7851
|
});
|
|
7185
7852
|
};
|
|
7186
|
-
const actions =
|
|
7853
|
+
const actions = asArray40(stack.actions);
|
|
7187
7854
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
7188
|
-
const objects =
|
|
7855
|
+
const objects = asArray40(stack.objects);
|
|
7189
7856
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7190
7857
|
const obj = objects[oi];
|
|
7191
7858
|
if (!obj || typeof obj !== "object") continue;
|
|
7192
|
-
const own =
|
|
7859
|
+
const own = asArray40(obj.actions);
|
|
7193
7860
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
7194
7861
|
}
|
|
7195
7862
|
return findings;
|
|
@@ -7197,7 +7864,8 @@ function validateActionLocations(stack) {
|
|
|
7197
7864
|
|
|
7198
7865
|
// src/lint-flow-patterns.ts
|
|
7199
7866
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
7200
|
-
|
|
7867
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
7868
|
+
function asArray41(v) {
|
|
7201
7869
|
if (Array.isArray(v)) return v;
|
|
7202
7870
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7203
7871
|
return [];
|
|
@@ -7471,7 +8139,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7471
8139
|
function filterCarriesNoCondition(filter) {
|
|
7472
8140
|
if (filter === void 0 || filter === null) return true;
|
|
7473
8141
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7474
|
-
return
|
|
8142
|
+
return (0, import_data8.reduceFilterVerdict)(filter) === "true";
|
|
8143
|
+
}
|
|
8144
|
+
function describeUnboundedFilter(filter) {
|
|
8145
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8146
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8147
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8148
|
+
}
|
|
8149
|
+
function previewFilter(filter) {
|
|
8150
|
+
try {
|
|
8151
|
+
const json = JSON.stringify(filter);
|
|
8152
|
+
if (typeof json !== "string") return typeof filter;
|
|
8153
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8154
|
+
} catch {
|
|
8155
|
+
return typeof filter;
|
|
8156
|
+
}
|
|
7475
8157
|
}
|
|
7476
8158
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7477
8159
|
for (const node of nodes) {
|
|
@@ -7482,10 +8164,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7482
8164
|
if (cfg.multi !== true) continue;
|
|
7483
8165
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7484
8166
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7485
|
-
const filterState = cfg.filter
|
|
8167
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7486
8168
|
findings.push({
|
|
7487
8169
|
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
7488
|
-
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where
|
|
8170
|
+
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards the filter as \`where\` (an absent key becomes \`{}\`) plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` bounded by nothing \u2014 a filter that reduces to TRUE constrains no row. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
|
|
7489
8171
|
hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
|
|
7490
8172
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7491
8173
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7567,7 +8249,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
7567
8249
|
}
|
|
7568
8250
|
function lintFlowPatterns(stack) {
|
|
7569
8251
|
const findings = [];
|
|
7570
|
-
for (const flow of
|
|
8252
|
+
for (const flow of asArray41(stack.flows)) {
|
|
7571
8253
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
7572
8254
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
7573
8255
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -7665,7 +8347,7 @@ var import_node_fs = require("fs");
|
|
|
7665
8347
|
var import_meta5 = {};
|
|
7666
8348
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7667
8349
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7668
|
-
function
|
|
8350
|
+
function asArray42(v) {
|
|
7669
8351
|
if (Array.isArray(v)) return v;
|
|
7670
8352
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7671
8353
|
return [];
|
|
@@ -7787,6 +8469,13 @@ var TYPE_COLLECTIONS = [
|
|
|
7787
8469
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
7788
8470
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7789
8471
|
// silent, which is the shape this lint exists to prevent.
|
|
8472
|
+
//
|
|
8473
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
8474
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
8475
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
8476
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
8477
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
8478
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
7790
8479
|
{ type: "dashboard", key: "dashboards" }
|
|
7791
8480
|
];
|
|
7792
8481
|
function lintLivenessProperties(stack) {
|
|
@@ -7795,11 +8484,11 @@ function lintLivenessProperties(stack) {
|
|
|
7795
8484
|
const findings = [];
|
|
7796
8485
|
const objectWarn = loadWarnMap(dir, "object");
|
|
7797
8486
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
7798
|
-
for (const obj of
|
|
8487
|
+
for (const obj of asArray42(stack.objects)) {
|
|
7799
8488
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7800
8489
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
7801
8490
|
if (fieldWarn.size > 0) {
|
|
7802
|
-
for (const field of
|
|
8491
|
+
for (const field of asArray42(obj.fields)) {
|
|
7803
8492
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
7804
8493
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
7805
8494
|
}
|
|
@@ -7808,7 +8497,7 @@ function lintLivenessProperties(stack) {
|
|
|
7808
8497
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7809
8498
|
const warnMap = loadWarnMap(dir, type);
|
|
7810
8499
|
if (warnMap.size === 0) continue;
|
|
7811
|
-
for (const item of
|
|
8500
|
+
for (const item of asArray42(stack[key])) {
|
|
7812
8501
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7813
8502
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7814
8503
|
}
|
|
@@ -7817,12 +8506,12 @@ function lintLivenessProperties(stack) {
|
|
|
7817
8506
|
}
|
|
7818
8507
|
|
|
7819
8508
|
// src/lint-autonumber-formats.ts
|
|
7820
|
-
var
|
|
8509
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
7821
8510
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
7822
8511
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7823
8512
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7824
8513
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7825
|
-
function
|
|
8514
|
+
function asArray43(v) {
|
|
7826
8515
|
if (Array.isArray(v)) return v;
|
|
7827
8516
|
if (v && typeof v === "object") {
|
|
7828
8517
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7831,9 +8520,9 @@ function asArray44(v) {
|
|
|
7831
8520
|
}
|
|
7832
8521
|
function lintAutonumberFormats(stack) {
|
|
7833
8522
|
const findings = [];
|
|
7834
|
-
for (const obj of
|
|
8523
|
+
for (const obj of asArray43(stack.objects)) {
|
|
7835
8524
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7836
|
-
const fields =
|
|
8525
|
+
const fields = asArray43(obj.fields);
|
|
7837
8526
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7838
8527
|
for (const f of fields) {
|
|
7839
8528
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -7843,8 +8532,8 @@ function lintAutonumberFormats(stack) {
|
|
|
7843
8532
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
7844
8533
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
7845
8534
|
if (!fmt) continue;
|
|
7846
|
-
const tokens = (0,
|
|
7847
|
-
const refs = (0,
|
|
8535
|
+
const tokens = (0, import_data9.parseAutonumberFormat)(fmt);
|
|
8536
|
+
const refs = (0, import_data9.referencedFields)(tokens);
|
|
7848
8537
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
7849
8538
|
for (const t of tokens) {
|
|
7850
8539
|
if (t.kind !== "literal") continue;
|
|
@@ -7898,8 +8587,8 @@ function lintAutonumberFormats(stack) {
|
|
|
7898
8587
|
}
|
|
7899
8588
|
|
|
7900
8589
|
// src/lint-view-refs.ts
|
|
7901
|
-
var
|
|
7902
|
-
function
|
|
8590
|
+
var import_spec5 = require("@objectstack/spec");
|
|
8591
|
+
function asArray44(v) {
|
|
7903
8592
|
if (Array.isArray(v)) return v;
|
|
7904
8593
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7905
8594
|
return [];
|
|
@@ -7927,16 +8616,16 @@ function lintViewRefs(stack) {
|
|
|
7927
8616
|
s.add(kind);
|
|
7928
8617
|
};
|
|
7929
8618
|
const containers = [];
|
|
7930
|
-
for (const v of
|
|
8619
|
+
for (const v of asArray44(stack.views)) {
|
|
7931
8620
|
if (v.viewKind) {
|
|
7932
8621
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7933
8622
|
continue;
|
|
7934
8623
|
}
|
|
7935
|
-
if (!(0,
|
|
8624
|
+
if (!(0, import_spec5.isAggregatedViewContainer)(v)) continue;
|
|
7936
8625
|
const object = viewContainerObjectName(v);
|
|
7937
8626
|
if (object) containers.push({ object, container: v });
|
|
7938
8627
|
}
|
|
7939
|
-
for (const obj of
|
|
8628
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7940
8629
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7941
8630
|
if (!object) continue;
|
|
7942
8631
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -7944,7 +8633,7 @@ function lintViewRefs(stack) {
|
|
|
7944
8633
|
}
|
|
7945
8634
|
}
|
|
7946
8635
|
for (const { object, container } of containers) {
|
|
7947
|
-
const { items, collisions } = (0,
|
|
8636
|
+
const { items, collisions } = (0, import_spec5.expandViewContainerWithDiagnostics)(object, container);
|
|
7948
8637
|
for (const it of items) indexKind(it.name, it.viewKind);
|
|
7949
8638
|
for (const col of collisions) {
|
|
7950
8639
|
findings.push({
|
|
@@ -7990,11 +8679,11 @@ function lintViewRefs(stack) {
|
|
|
7990
8679
|
});
|
|
7991
8680
|
}
|
|
7992
8681
|
};
|
|
7993
|
-
for (const obj of
|
|
8682
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7994
8683
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7995
|
-
for (const action of
|
|
8684
|
+
for (const action of asArray44(obj.actions)) checkAction(action, object);
|
|
7996
8685
|
}
|
|
7997
|
-
for (const action of
|
|
8686
|
+
for (const action of asArray44(stack.actions)) checkAction(action);
|
|
7998
8687
|
return findings;
|
|
7999
8688
|
}
|
|
8000
8689
|
|
|
@@ -8129,6 +8818,7 @@ var CLI_ONLY = ["cli"];
|
|
|
8129
8818
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8130
8819
|
var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
|
|
8131
8820
|
var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
|
|
8821
|
+
var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
|
|
8132
8822
|
var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
|
|
8133
8823
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8134
8824
|
var AUTHORING_RULES = [
|
|
@@ -8158,8 +8848,12 @@ var AUTHORING_RULES = [
|
|
|
8158
8848
|
}))
|
|
8159
8849
|
},
|
|
8160
8850
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8161
|
-
// mode)
|
|
8162
|
-
//
|
|
8851
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
8852
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
8853
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
8854
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
8855
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
8856
|
+
// parse would have eaten".
|
|
8163
8857
|
{
|
|
8164
8858
|
name: "validateListViewMode",
|
|
8165
8859
|
tier: "gating",
|
|
@@ -8190,9 +8884,13 @@ var AUTHORING_RULES = [
|
|
|
8190
8884
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8191
8885
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8192
8886
|
},
|
|
8193
|
-
// A
|
|
8194
|
-
//
|
|
8195
|
-
//
|
|
8887
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
8888
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
8889
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
8890
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
8891
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
8892
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
8893
|
+
// declared and which survives the parse untouched.
|
|
8196
8894
|
{
|
|
8197
8895
|
name: "validateViewContainers",
|
|
8198
8896
|
tier: "gating",
|
|
@@ -8242,6 +8940,31 @@ var AUTHORING_RULES = [
|
|
|
8242
8940
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8243
8941
|
run: (stack) => validateFilterTokens(stack)
|
|
8244
8942
|
},
|
|
8943
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
8944
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
8945
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
8946
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
8947
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
8948
|
+
// same-direction precedent one shape over.
|
|
8949
|
+
{
|
|
8950
|
+
name: "validateEmptyCombinators",
|
|
8951
|
+
tier: "gating",
|
|
8952
|
+
input: "parsed",
|
|
8953
|
+
commands: ALL,
|
|
8954
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
8955
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
8956
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
8957
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
8958
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
8959
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
8960
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
8961
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
8962
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
8963
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
8964
|
+
surfaces: CLI_AND_RUNTIME,
|
|
8965
|
+
runtimeTypes: ["flow"],
|
|
8966
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
8967
|
+
},
|
|
8245
8968
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8246
8969
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8247
8970
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8290,6 +9013,16 @@ var AUTHORING_RULES = [
|
|
|
8290
9013
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8291
9014
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8292
9015
|
// separate step, once the warning-period inventory is empty.
|
|
9016
|
+
//
|
|
9017
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9018
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9019
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9020
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9021
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9022
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9023
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9024
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9025
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8293
9026
|
{
|
|
8294
9027
|
name: "validateComponentProps",
|
|
8295
9028
|
tier: "advisory",
|
|
@@ -8368,10 +9101,12 @@ var AUTHORING_RULES = [
|
|
|
8368
9101
|
//
|
|
8369
9102
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8370
9103
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8371
|
-
// dead?
|
|
9104
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8372
9105
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8373
|
-
// predicate cannot route at all,
|
|
8374
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9106
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9107
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9108
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9109
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8375
9110
|
// can be changed by installing a package, so there is no reading under which
|
|
8376
9111
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8377
9112
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8498,12 +9233,39 @@ var AUTHORING_RULES = [
|
|
|
8498
9233
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8499
9234
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8500
9235
|
},
|
|
8501
|
-
// ADR-0089 D3b —
|
|
8502
|
-
//
|
|
8503
|
-
//
|
|
9236
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
9237
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
9238
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
9239
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
9240
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
9241
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
9242
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
9243
|
+
//
|
|
9244
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
9245
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
9246
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
9247
|
+
// a change to this package's external input contract, and the maintainer's
|
|
9248
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
9249
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
9250
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
9251
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
9252
|
+
//
|
|
9253
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
9254
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
9255
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
9256
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
9257
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
9258
|
+
// "pre-parse evidence" one.
|
|
9259
|
+
//
|
|
9260
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
9261
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
9262
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
9263
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9264
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9265
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
8504
9266
|
{
|
|
8505
9267
|
name: "validateVisibilityPredicates",
|
|
8506
|
-
tier: "
|
|
9268
|
+
tier: "gating",
|
|
8507
9269
|
input: "normalized",
|
|
8508
9270
|
commands: ALL,
|
|
8509
9271
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
@@ -8511,6 +9273,30 @@ var AUTHORING_RULES = [
|
|
|
8511
9273
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8512
9274
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8513
9275
|
},
|
|
9276
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
9277
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
9278
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
9279
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
9280
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
9281
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
9282
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
9283
|
+
// #6254's 16 bare `type ==` predicates.
|
|
9284
|
+
//
|
|
9285
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
9286
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
9287
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9288
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9289
|
+
// false build errors. See the rule's module note.
|
|
9290
|
+
{
|
|
9291
|
+
name: "validatePredicatePathRefs",
|
|
9292
|
+
tier: "gating",
|
|
9293
|
+
input: "normalized",
|
|
9294
|
+
commands: ALL,
|
|
9295
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9296
|
+
surfaces: CLI_ONLY,
|
|
9297
|
+
surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
|
|
9298
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
9299
|
+
},
|
|
8514
9300
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8515
9301
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8516
9302
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|