@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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/validate-expressions.ts
|
|
2
|
-
import { validateExpression, collectCelRootIdentifiers } from "@objectstack/formula";
|
|
2
|
+
import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
|
|
3
3
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
4
4
|
|
|
5
5
|
// src/system-fields.ts
|
|
@@ -371,6 +371,38 @@ function validateStackExpressions(stack) {
|
|
|
371
371
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
372
372
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
373
373
|
};
|
|
374
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
375
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
376
|
+
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";
|
|
377
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
378
|
+
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)",
|
|
379
|
+
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",
|
|
380
|
+
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",
|
|
381
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
382
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
383
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
384
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
385
|
+
// one, not a fabricated fourth cell (#6716).
|
|
386
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
387
|
+
};
|
|
388
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
389
|
+
const source = celSourceOf(raw);
|
|
390
|
+
if (!source) return;
|
|
391
|
+
const roots = collectCelRootIdentifiers(source);
|
|
392
|
+
if (!roots.ok) return;
|
|
393
|
+
const kept = SCOPE_ROOTS.filter(
|
|
394
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
395
|
+
);
|
|
396
|
+
if (kept.length === 0) return;
|
|
397
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
398
|
+
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}\`.`;
|
|
399
|
+
issues.push({
|
|
400
|
+
where,
|
|
401
|
+
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,
|
|
402
|
+
source,
|
|
403
|
+
severity: "error"
|
|
404
|
+
});
|
|
405
|
+
};
|
|
374
406
|
const checkDeclaredPredicate = (where, raw) => {
|
|
375
407
|
if (raw == null) return;
|
|
376
408
|
const res = validateExpression("predicate", raw);
|
|
@@ -403,7 +435,14 @@ function validateStackExpressions(stack) {
|
|
|
403
435
|
if (retired.length > 0) {
|
|
404
436
|
issues.push({
|
|
405
437
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
406
|
-
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. `) +
|
|
438
|
+
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
|
|
439
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
440
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
441
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
442
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
443
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
444
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
445
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
407
446
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
408
447
|
});
|
|
409
448
|
} else if (!fn) {
|
|
@@ -437,13 +476,27 @@ function validateStackExpressions(stack) {
|
|
|
437
476
|
for (const [fname, f] of fieldList) {
|
|
438
477
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
439
478
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
479
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
440
480
|
}
|
|
441
|
-
const
|
|
442
|
-
|
|
481
|
+
for (const [oi, opt] of asArray(f.options).entries()) {
|
|
482
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
483
|
+
check(
|
|
484
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
485
|
+
opt.visibleWhen,
|
|
486
|
+
objectName,
|
|
487
|
+
"record"
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
for (const [slot, raw, consequence2] of [
|
|
491
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
492
|
+
["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`]
|
|
493
|
+
]) {
|
|
494
|
+
const source = celSourceOf(raw);
|
|
495
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
443
496
|
issues.push({
|
|
444
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
445
|
-
message:
|
|
446
|
-
source
|
|
497
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
498
|
+
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\`.`),
|
|
499
|
+
source,
|
|
447
500
|
severity: "error"
|
|
448
501
|
});
|
|
449
502
|
}
|
|
@@ -1171,7 +1224,8 @@ function validateDashboardActionRefs(stack) {
|
|
|
1171
1224
|
|
|
1172
1225
|
// src/validate-filter-tokens.ts
|
|
1173
1226
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
1174
|
-
|
|
1227
|
+
|
|
1228
|
+
// src/filter-walk.ts
|
|
1175
1229
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
1176
1230
|
function asArray5(v) {
|
|
1177
1231
|
if (Array.isArray(v)) return v;
|
|
@@ -1183,7 +1237,62 @@ function asArray5(v) {
|
|
|
1183
1237
|
function label(v, fallback) {
|
|
1184
1238
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
1185
1239
|
}
|
|
1240
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
1241
|
+
if (!node || typeof node !== "object") return;
|
|
1242
|
+
if (seen.has(node)) return;
|
|
1243
|
+
seen.add(node);
|
|
1244
|
+
if (Array.isArray(node)) {
|
|
1245
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1249
|
+
const childPath = `${path}.${k}`;
|
|
1250
|
+
if (FILTER_KEYS.has(k)) {
|
|
1251
|
+
visit({ value: v, path: childPath, where });
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
1258
|
+
if (!stack || typeof stack !== "object") return;
|
|
1259
|
+
for (const { key, kind } of surfaces) {
|
|
1260
|
+
const items = asArray5(stack[key]);
|
|
1261
|
+
items.forEach((item, i) => {
|
|
1262
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
1263
|
+
if (kind === "dashboard") {
|
|
1264
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1265
|
+
widgets.forEach((w, wi) => {
|
|
1266
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1267
|
+
scanForFilters(
|
|
1268
|
+
w,
|
|
1269
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
1270
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1271
|
+
visit,
|
|
1272
|
+
/* @__PURE__ */ new Set()
|
|
1273
|
+
);
|
|
1274
|
+
});
|
|
1275
|
+
const { widgets: _skip, ...rest } = item;
|
|
1276
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// src/validate-filter-tokens.ts
|
|
1285
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
1186
1286
|
var KNOWN_LIST = CONTEXT_TOKENS.join("}, {");
|
|
1287
|
+
var TOKEN_FILTER_SURFACES = [
|
|
1288
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1289
|
+
{ key: "objects", kind: "object" },
|
|
1290
|
+
{ key: "views", kind: "view" },
|
|
1291
|
+
{ key: "reports", kind: "report" },
|
|
1292
|
+
{ key: "datasets", kind: "dataset" },
|
|
1293
|
+
{ key: "pages", kind: "page" },
|
|
1294
|
+
{ key: "apps", kind: "app" }
|
|
1295
|
+
];
|
|
1187
1296
|
function walkFilterValues(node, path, where, out, seen) {
|
|
1188
1297
|
if (node === null || node === void 0) return;
|
|
1189
1298
|
if (typeof node === "string") {
|
|
@@ -1212,58 +1321,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
1212
1321
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
1213
1322
|
}
|
|
1214
1323
|
}
|
|
1215
|
-
function
|
|
1216
|
-
if (!
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1324
|
+
function validateFilterTokens(stack) {
|
|
1325
|
+
if (!stack || typeof stack !== "object") return [];
|
|
1326
|
+
const out = [];
|
|
1327
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
1328
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
1329
|
+
});
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/validate-empty-combinators.ts
|
|
1334
|
+
import { reduceFilterVerdict } from "@objectstack/spec/data";
|
|
1335
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1336
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1337
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
1338
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
1339
|
+
{ key: "objects", kind: "object" },
|
|
1340
|
+
{ key: "views", kind: "view" },
|
|
1341
|
+
{ key: "reports", kind: "report" },
|
|
1342
|
+
{ key: "datasets", kind: "dataset" },
|
|
1343
|
+
{ key: "pages", kind: "page" },
|
|
1344
|
+
{ key: "apps", kind: "app" },
|
|
1345
|
+
{ key: "flows", kind: "flow" }
|
|
1346
|
+
];
|
|
1347
|
+
function isFilterNode(value) {
|
|
1348
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1349
|
+
const proto = Object.getPrototypeOf(value);
|
|
1350
|
+
return proto === Object.prototype || proto === null;
|
|
1351
|
+
}
|
|
1352
|
+
var VERDICT_OF = {
|
|
1353
|
+
$and: reduceFilterVerdict({ $and: [] }),
|
|
1354
|
+
$or: reduceFilterVerdict({ $or: [] }),
|
|
1355
|
+
$not: reduceFilterVerdict({ $not: {} }),
|
|
1356
|
+
node: reduceFilterVerdict({}),
|
|
1357
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1358
|
+
orWithEmptyBranch: reduceFilterVerdict({ $or: [{ status: "open" }, {}] })
|
|
1359
|
+
};
|
|
1360
|
+
function rows(verdict) {
|
|
1361
|
+
if (verdict === "true") return "matches EVERY row";
|
|
1362
|
+
if (verdict === "false") return "matches NO row";
|
|
1363
|
+
return "carries a real predicate";
|
|
1364
|
+
}
|
|
1365
|
+
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.";
|
|
1366
|
+
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).';
|
|
1367
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
1368
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
1369
|
+
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.`;
|
|
1370
|
+
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}`;
|
|
1371
|
+
ctx.out.push({
|
|
1372
|
+
severity: "error",
|
|
1373
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
1374
|
+
where: ctx.where,
|
|
1375
|
+
path,
|
|
1376
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
1377
|
+
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.`
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
function emitEmptyNode(position, path, ctx) {
|
|
1381
|
+
if (position === "root") {
|
|
1382
|
+
ctx.out.push({
|
|
1383
|
+
severity: "error",
|
|
1384
|
+
rule: FILTER_EMPTY_NODE,
|
|
1385
|
+
where: ctx.where,
|
|
1386
|
+
path,
|
|
1387
|
+
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.`,
|
|
1388
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
1389
|
+
});
|
|
1221
1390
|
return;
|
|
1222
1391
|
}
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1392
|
+
if (position === "or-branch") {
|
|
1393
|
+
ctx.out.push({
|
|
1394
|
+
severity: "error",
|
|
1395
|
+
rule: FILTER_EMPTY_NODE,
|
|
1396
|
+
where: ctx.where,
|
|
1397
|
+
path,
|
|
1398
|
+
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.`,
|
|
1399
|
+
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.)"
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
ctx.out.push({
|
|
1404
|
+
severity: "error",
|
|
1405
|
+
rule: FILTER_EMPTY_NODE,
|
|
1406
|
+
where: ctx.where,
|
|
1407
|
+
path,
|
|
1408
|
+
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.",
|
|
1409
|
+
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."
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
function scanNodeKeys(node, path, ctx) {
|
|
1413
|
+
for (const [key, value] of Object.entries(node)) {
|
|
1414
|
+
if (key === "$and" || key === "$or") {
|
|
1415
|
+
if (!Array.isArray(value)) continue;
|
|
1416
|
+
if (value.length === 0) {
|
|
1417
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
value.forEach((element, index) => {
|
|
1421
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
1422
|
+
});
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
if (key === "$not") {
|
|
1426
|
+
if (!isFilterNode(value)) continue;
|
|
1427
|
+
if (Object.keys(value).length === 0) {
|
|
1428
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
1429
|
+
continue;
|
|
1430
|
+
}
|
|
1431
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
1227
1432
|
continue;
|
|
1228
1433
|
}
|
|
1229
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
1230
1434
|
}
|
|
1231
1435
|
}
|
|
1232
|
-
function
|
|
1436
|
+
function scanBranch(value, path, position, ctx) {
|
|
1437
|
+
if (!isFilterNode(value)) return;
|
|
1438
|
+
if (Object.keys(value).length === 0) {
|
|
1439
|
+
emitEmptyNode(position, path, ctx);
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
scanNodeKeys(value, path, ctx);
|
|
1443
|
+
}
|
|
1444
|
+
function validateEmptyCombinators(stack) {
|
|
1233
1445
|
if (!stack || typeof stack !== "object") return [];
|
|
1234
1446
|
const out = [];
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
["views", "view"],
|
|
1239
|
-
["reports", "report"],
|
|
1240
|
-
["datasets", "dataset"],
|
|
1241
|
-
["pages", "page"],
|
|
1242
|
-
["apps", "app"]
|
|
1243
|
-
];
|
|
1244
|
-
for (const [key, kind] of surfaces) {
|
|
1245
|
-
const items = asArray5(stack[key]);
|
|
1246
|
-
items.forEach((item, i) => {
|
|
1247
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
1248
|
-
if (kind === "dashboard") {
|
|
1249
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
1250
|
-
widgets.forEach((w, wi) => {
|
|
1251
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
1252
|
-
scanForFilters(
|
|
1253
|
-
w,
|
|
1254
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
1255
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
1256
|
-
out,
|
|
1257
|
-
/* @__PURE__ */ new Set()
|
|
1258
|
-
);
|
|
1259
|
-
});
|
|
1260
|
-
const { widgets: _skip, ...rest } = item;
|
|
1261
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1262
|
-
return;
|
|
1263
|
-
}
|
|
1264
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1447
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
1448
|
+
scanBranch(value, path, "root", { where, out });
|
|
1449
|
+
});
|
|
1267
1450
|
return out;
|
|
1268
1451
|
}
|
|
1269
1452
|
|
|
@@ -1463,6 +1646,7 @@ function validateObjectReferences(stack) {
|
|
|
1463
1646
|
// src/validate-searchable-fields.ts
|
|
1464
1647
|
import {
|
|
1465
1648
|
resolveSearchFieldResolution,
|
|
1649
|
+
isVirtualSearchField,
|
|
1466
1650
|
SEARCHABLE_TEXTUAL_TYPES,
|
|
1467
1651
|
SEARCHABLE_ENUM_TYPES,
|
|
1468
1652
|
SEARCH_AUTO_EXCLUDED_FIELDS
|
|
@@ -1580,7 +1764,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1580
1764
|
where,
|
|
1581
1765
|
path: `${path}[${i}]`,
|
|
1582
1766
|
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)),
|
|
1583
|
-
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
|
|
1767
|
+
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(", ")}.` : "")
|
|
1768
|
+
});
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
if (isVirtualSearchField(target.fields[name])) {
|
|
1772
|
+
const vtype = target.fields[name]?.type;
|
|
1773
|
+
findings.push({
|
|
1774
|
+
severity: "error",
|
|
1775
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1776
|
+
where,
|
|
1777
|
+
path: `${path}[${i}]`,
|
|
1778
|
+
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).`,
|
|
1779
|
+
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).`
|
|
1584
1780
|
});
|
|
1585
1781
|
continue;
|
|
1586
1782
|
}
|
|
@@ -1615,7 +1811,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1615
1811
|
where,
|
|
1616
1812
|
path: `${path}[${i}]`,
|
|
1617
1813
|
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...SEARCHABLE_TEXTUAL_TYPES, ...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).`,
|
|
1618
|
-
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
|
|
1814
|
+
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.`
|
|
1619
1815
|
});
|
|
1620
1816
|
}
|
|
1621
1817
|
return findings;
|
|
@@ -1934,6 +2130,15 @@ function validateActionNameRefs(stack) {
|
|
|
1934
2130
|
"Navigation action item"
|
|
1935
2131
|
);
|
|
1936
2132
|
}
|
|
2133
|
+
const runAction = strName5(nav.runAction);
|
|
2134
|
+
if (nav.type === "object" && runAction) {
|
|
2135
|
+
check(
|
|
2136
|
+
runAction,
|
|
2137
|
+
`app "${appName}" \xB7 nav "${strName5(nav.id) ?? `#${ni}`}"`,
|
|
2138
|
+
`${navPath}.runAction`,
|
|
2139
|
+
"Navigation deep-link auto-run"
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
1937
2142
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
1938
2143
|
}
|
|
1939
2144
|
};
|
|
@@ -1999,8 +2204,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
1999
2204
|
"element:number": { props: ["field"] },
|
|
2000
2205
|
"element:filter": { props: ["fields"] },
|
|
2001
2206
|
"element:form": { props: ["fields"] },
|
|
2002
|
-
//
|
|
2003
|
-
|
|
2207
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
2208
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
2209
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
2210
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
2211
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
2212
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
2213
|
+
//
|
|
2214
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
2215
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
2216
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
2217
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
2218
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
2219
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
2220
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
2221
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2004
2222
|
};
|
|
2005
2223
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2006
2224
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2569,18 +2787,57 @@ function validateNavTargetRefs(stack) {
|
|
|
2569
2787
|
}
|
|
2570
2788
|
|
|
2571
2789
|
// src/validate-translation-references.ts
|
|
2790
|
+
import { expandViewContainer } from "@objectstack/spec";
|
|
2572
2791
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
2792
|
+
|
|
2793
|
+
// src/view-walk.ts
|
|
2794
|
+
function isRec7(v) {
|
|
2795
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2796
|
+
}
|
|
2797
|
+
function strName10(v) {
|
|
2798
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2799
|
+
}
|
|
2800
|
+
function viewObjectName(view) {
|
|
2801
|
+
return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
|
|
2802
|
+
}
|
|
2803
|
+
function viewContainerSites(view, basePath) {
|
|
2804
|
+
if (!isRec7(view)) return [];
|
|
2805
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
2806
|
+
if (isRec7(view.form)) {
|
|
2807
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
2808
|
+
}
|
|
2809
|
+
for (const key of ["listViews", "formViews"]) {
|
|
2810
|
+
const container = view[key];
|
|
2811
|
+
if (!isRec7(container)) continue;
|
|
2812
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
2813
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
2814
|
+
if (!isRec7(sub)) continue;
|
|
2815
|
+
sites.push({
|
|
2816
|
+
view: sub,
|
|
2817
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
2818
|
+
surface: `${key}.${subKey}`,
|
|
2819
|
+
kind
|
|
2820
|
+
});
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
return sites;
|
|
2824
|
+
}
|
|
2825
|
+
function formViewSites(view, basePath) {
|
|
2826
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/validate-translation-references.ts
|
|
2573
2830
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
2574
2831
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
2575
|
-
function
|
|
2832
|
+
function isRec8(v) {
|
|
2576
2833
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2577
2834
|
}
|
|
2578
2835
|
function asArray14(v) {
|
|
2579
2836
|
if (Array.isArray(v)) return v;
|
|
2580
|
-
if (
|
|
2837
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec8(def) ? def : {} }));
|
|
2581
2838
|
return [];
|
|
2582
2839
|
}
|
|
2583
|
-
function
|
|
2840
|
+
function strName11(v) {
|
|
2584
2841
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2585
2842
|
}
|
|
2586
2843
|
function distance5(a, b) {
|
|
@@ -2641,29 +2898,51 @@ function collectViewRecord(view, factsFor) {
|
|
|
2641
2898
|
const addSections = (container, binding) => {
|
|
2642
2899
|
if (!binding) return;
|
|
2643
2900
|
for (const section of asArray14(container.sections)) {
|
|
2644
|
-
const sectionName =
|
|
2901
|
+
const sectionName = strName11(section.name);
|
|
2645
2902
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2646
2903
|
}
|
|
2647
2904
|
};
|
|
2648
|
-
const listBinding =
|
|
2649
|
-
if (
|
|
2650
|
-
addView(recordObject ?? listBinding,
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2905
|
+
const listBinding = isRec8(view.list) ? bindingOf(view.list) : void 0;
|
|
2906
|
+
if (isRec8(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
2907
|
+
addView(recordObject ?? listBinding, strName11(view.name));
|
|
2908
|
+
const named = namedViewKeys(view);
|
|
2909
|
+
for (const family of ["listViews", "formViews"]) {
|
|
2910
|
+
const container = view[family];
|
|
2911
|
+
if (!isRec8(container)) continue;
|
|
2912
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
2913
|
+
let at = 0;
|
|
2914
|
+
for (const sub of Object.values(container)) {
|
|
2915
|
+
if (!sub || typeof sub !== "object") continue;
|
|
2916
|
+
const registryKey = registryKeys[at++];
|
|
2917
|
+
if (!isRec8(sub)) continue;
|
|
2656
2918
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2657
|
-
addView(binding,
|
|
2658
|
-
addView(binding, strName10(sub.name));
|
|
2919
|
+
addView(binding, registryKey);
|
|
2659
2920
|
addSections(sub, binding);
|
|
2660
2921
|
}
|
|
2661
2922
|
}
|
|
2662
|
-
if (
|
|
2923
|
+
if (isRec8(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2663
2924
|
addSections(view, recordObject ?? listBinding);
|
|
2664
2925
|
}
|
|
2665
|
-
function
|
|
2666
|
-
|
|
2926
|
+
function defaultListViewKey(object, container) {
|
|
2927
|
+
if (!object || !isRec8(container.list)) return void 0;
|
|
2928
|
+
const item = expandViewContainer(object, container).find(
|
|
2929
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
2930
|
+
);
|
|
2931
|
+
if (!item) return void 0;
|
|
2932
|
+
const prefix = `${object}.`;
|
|
2933
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
2934
|
+
}
|
|
2935
|
+
function namedViewKeys(container) {
|
|
2936
|
+
const object = "probe";
|
|
2937
|
+
const prefix = `${object}.`;
|
|
2938
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
2939
|
+
const countEntries = (v) => isRec8(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
2940
|
+
const listCount = countEntries(container.listViews);
|
|
2941
|
+
const formCount = countEntries(container.formViews);
|
|
2942
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
2943
|
+
const items = expandViewContainer(object, container);
|
|
2944
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
2945
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
2667
2946
|
}
|
|
2668
2947
|
function readOptions(field) {
|
|
2669
2948
|
const raw = field.options;
|
|
@@ -2675,14 +2954,14 @@ function readOptions(field) {
|
|
|
2675
2954
|
values.add(opt);
|
|
2676
2955
|
continue;
|
|
2677
2956
|
}
|
|
2678
|
-
if (!
|
|
2679
|
-
const value =
|
|
2957
|
+
if (!isRec8(opt)) continue;
|
|
2958
|
+
const value = strName11(opt.value);
|
|
2680
2959
|
if (!value) continue;
|
|
2681
2960
|
values.add(value);
|
|
2682
|
-
const label2 =
|
|
2961
|
+
const label2 = strName11(opt.label);
|
|
2683
2962
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2684
2963
|
}
|
|
2685
|
-
} else if (
|
|
2964
|
+
} else if (isRec8(raw)) {
|
|
2686
2965
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2687
2966
|
values.add(value);
|
|
2688
2967
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2703,23 +2982,23 @@ function buildUniverse(stack) {
|
|
|
2703
2982
|
return facts;
|
|
2704
2983
|
};
|
|
2705
2984
|
for (const obj of asArray14(stack.objects)) {
|
|
2706
|
-
const objectName =
|
|
2985
|
+
const objectName = strName11(obj.name);
|
|
2707
2986
|
if (!objectName) continue;
|
|
2708
2987
|
const facts = factsFor(objectName);
|
|
2709
2988
|
for (const field of asArray14(obj.fields)) {
|
|
2710
|
-
const fieldName =
|
|
2989
|
+
const fieldName = strName11(field.name);
|
|
2711
2990
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
2712
2991
|
}
|
|
2713
2992
|
for (const action of asArray14(obj.actions)) {
|
|
2714
|
-
const actionName =
|
|
2993
|
+
const actionName = strName11(action.name);
|
|
2715
2994
|
if (actionName) facts.actions.set(actionName, action);
|
|
2716
2995
|
}
|
|
2717
2996
|
for (const view of asArray14(obj.views)) {
|
|
2718
|
-
collectViewRecord({ ...view, object:
|
|
2997
|
+
collectViewRecord({ ...view, object: strName11(view.object) ?? objectName }, factsFor);
|
|
2719
2998
|
}
|
|
2720
2999
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
2721
3000
|
for (const group of asArray14(obj.fieldGroups)) {
|
|
2722
|
-
const key =
|
|
3001
|
+
const key = strName11(group.key) ?? strName11(group.name);
|
|
2723
3002
|
if (key) facts.sections.add(key);
|
|
2724
3003
|
}
|
|
2725
3004
|
}
|
|
@@ -2730,10 +3009,10 @@ function buildUniverse(stack) {
|
|
|
2730
3009
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2731
3010
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
2732
3011
|
if (!walked.objectName) continue;
|
|
2733
|
-
const props =
|
|
3012
|
+
const props = isRec8(walked.component.properties) ? walked.component.properties : void 0;
|
|
2734
3013
|
if (!props) continue;
|
|
2735
3014
|
for (const section of asArray14(props.sections)) {
|
|
2736
|
-
const sectionName =
|
|
3015
|
+
const sectionName = strName11(section.name);
|
|
2737
3016
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
2738
3017
|
}
|
|
2739
3018
|
}
|
|
@@ -2741,9 +3020,9 @@ function buildUniverse(stack) {
|
|
|
2741
3020
|
const globalActions = /* @__PURE__ */ new Map();
|
|
2742
3021
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
2743
3022
|
for (const action of asArray14(stack.actions)) {
|
|
2744
|
-
const actionName =
|
|
3023
|
+
const actionName = strName11(action.name);
|
|
2745
3024
|
if (!actionName) continue;
|
|
2746
|
-
const owner =
|
|
3025
|
+
const owner = strName11(action.objectName) ?? strName11(action.object);
|
|
2747
3026
|
if (owner) {
|
|
2748
3027
|
factsFor(owner).actions.set(actionName, action);
|
|
2749
3028
|
actionOwners.set(actionName, owner);
|
|
@@ -2758,19 +3037,19 @@ function buildUniverse(stack) {
|
|
|
2758
3037
|
}
|
|
2759
3038
|
const apps = /* @__PURE__ */ new Map();
|
|
2760
3039
|
for (const app of asArray14(stack.apps)) {
|
|
2761
|
-
const appName =
|
|
3040
|
+
const appName = strName11(app.name);
|
|
2762
3041
|
if (!appName) continue;
|
|
2763
3042
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
2764
3043
|
const walkNav = (items) => {
|
|
2765
3044
|
for (const item of asArray14(items)) {
|
|
2766
|
-
const id =
|
|
3045
|
+
const id = strName11(item.id);
|
|
2767
3046
|
if (id) navIds.add(id);
|
|
2768
3047
|
if (item.children) walkNav(item.children);
|
|
2769
3048
|
}
|
|
2770
3049
|
};
|
|
2771
3050
|
walkNav(app.navigation);
|
|
2772
3051
|
for (const area of asArray14(app.areas)) {
|
|
2773
|
-
const areaId =
|
|
3052
|
+
const areaId = strName11(area.id);
|
|
2774
3053
|
if (areaId) navIds.add(areaId);
|
|
2775
3054
|
walkNav(area.navigation);
|
|
2776
3055
|
}
|
|
@@ -2778,20 +3057,20 @@ function buildUniverse(stack) {
|
|
|
2778
3057
|
}
|
|
2779
3058
|
const dashboards = /* @__PURE__ */ new Map();
|
|
2780
3059
|
for (const dash of asArray14(stack.dashboards)) {
|
|
2781
|
-
const dashName =
|
|
3060
|
+
const dashName = strName11(dash.name);
|
|
2782
3061
|
if (!dashName) continue;
|
|
2783
3062
|
const widgets = /* @__PURE__ */ new Set();
|
|
2784
3063
|
for (const widget of asArray14(dash.widgets)) {
|
|
2785
|
-
const id =
|
|
3064
|
+
const id = strName11(widget.id) ?? strName11(widget.name);
|
|
2786
3065
|
if (id) widgets.add(id);
|
|
2787
3066
|
}
|
|
2788
3067
|
const actions = /* @__PURE__ */ new Set();
|
|
2789
3068
|
const headerActions = [
|
|
2790
|
-
...asArray14(
|
|
3069
|
+
...asArray14(isRec8(dash.header) ? dash.header.actions : void 0),
|
|
2791
3070
|
...asArray14(dash.actions)
|
|
2792
3071
|
];
|
|
2793
3072
|
for (const action of headerActions) {
|
|
2794
|
-
const key =
|
|
3073
|
+
const key = strName11(action.actionUrl) ?? strName11(action.url) ?? strName11(action.name);
|
|
2795
3074
|
if (key) actions.add(key);
|
|
2796
3075
|
}
|
|
2797
3076
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -2803,7 +3082,7 @@ function localePath(bundleIndex, locale) {
|
|
|
2803
3082
|
}
|
|
2804
3083
|
function validateTranslationReferences(stack) {
|
|
2805
3084
|
const findings = [];
|
|
2806
|
-
if (!
|
|
3085
|
+
if (!isRec8(stack)) return findings;
|
|
2807
3086
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
2808
3087
|
if (bundles.length === 0) return findings;
|
|
2809
3088
|
const universe = buildUniverse(stack);
|
|
@@ -2812,13 +3091,13 @@ function validateTranslationReferences(stack) {
|
|
|
2812
3091
|
};
|
|
2813
3092
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
2814
3093
|
const bundle = bundles[bi];
|
|
2815
|
-
if (!
|
|
3094
|
+
if (!isRec8(bundle)) continue;
|
|
2816
3095
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
2817
|
-
if (!
|
|
3096
|
+
if (!isRec8(rawData)) continue;
|
|
2818
3097
|
const base = localePath(bi, locale);
|
|
2819
3098
|
const inLocale = `locale "${locale}"`;
|
|
2820
3099
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
2821
|
-
if (!
|
|
3100
|
+
if (!isRec8(rawNode)) continue;
|
|
2822
3101
|
const objPath = `${base}.objects.${objectName}`;
|
|
2823
3102
|
const facts = universe.objects.get(objectName);
|
|
2824
3103
|
if (!facts) {
|
|
@@ -2844,7 +3123,7 @@ function validateTranslationReferences(stack) {
|
|
|
2844
3123
|
);
|
|
2845
3124
|
continue;
|
|
2846
3125
|
}
|
|
2847
|
-
if (!
|
|
3126
|
+
if (!isRec8(rawField)) continue;
|
|
2848
3127
|
checkOptionKeys(findings, {
|
|
2849
3128
|
optionMap: rawField.options,
|
|
2850
3129
|
field,
|
|
@@ -2926,7 +3205,7 @@ function validateTranslationReferences(stack) {
|
|
|
2926
3205
|
);
|
|
2927
3206
|
continue;
|
|
2928
3207
|
}
|
|
2929
|
-
if (!
|
|
3208
|
+
if (!isRec8(rawApp)) continue;
|
|
2930
3209
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
2931
3210
|
if (navIds.has(navId)) continue;
|
|
2932
3211
|
orphan(
|
|
@@ -2949,7 +3228,7 @@ function validateTranslationReferences(stack) {
|
|
|
2949
3228
|
);
|
|
2950
3229
|
continue;
|
|
2951
3230
|
}
|
|
2952
|
-
if (!
|
|
3231
|
+
if (!isRec8(rawDash)) continue;
|
|
2953
3232
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
2954
3233
|
if (dash.widgets.has(widgetId)) continue;
|
|
2955
3234
|
orphan(
|
|
@@ -2974,7 +3253,7 @@ function validateTranslationReferences(stack) {
|
|
|
2974
3253
|
return findings;
|
|
2975
3254
|
}
|
|
2976
3255
|
function asRecord(v) {
|
|
2977
|
-
return
|
|
3256
|
+
return isRec8(v) ? v : {};
|
|
2978
3257
|
}
|
|
2979
3258
|
function checkOptionKeys(findings, ctx) {
|
|
2980
3259
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -2986,7 +3265,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
2986
3265
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
2987
3266
|
where: ctx.where,
|
|
2988
3267
|
path: ctx.path,
|
|
2989
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3268
|
+
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.`,
|
|
2990
3269
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
2991
3270
|
});
|
|
2992
3271
|
return;
|
|
@@ -3005,11 +3284,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3005
3284
|
}
|
|
3006
3285
|
}
|
|
3007
3286
|
function checkActionParams(findings, ctx) {
|
|
3008
|
-
const rawParams = Object.keys(asRecord(
|
|
3287
|
+
const rawParams = Object.keys(asRecord(isRec8(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3009
3288
|
if (rawParams.length === 0) return;
|
|
3010
3289
|
const declared = /* @__PURE__ */ new Set();
|
|
3011
3290
|
for (const param of asArray14(ctx.action.params)) {
|
|
3012
|
-
const name =
|
|
3291
|
+
const name = strName11(param.name) ?? strName11(param.field);
|
|
3013
3292
|
if (name) declared.add(name);
|
|
3014
3293
|
}
|
|
3015
3294
|
for (const paramName of rawParams) {
|
|
@@ -3025,78 +3304,60 @@ function checkActionParams(findings, ctx) {
|
|
|
3025
3304
|
}
|
|
3026
3305
|
}
|
|
3027
3306
|
|
|
3028
|
-
// src/
|
|
3029
|
-
|
|
3030
|
-
function isRec8(v) {
|
|
3307
|
+
// src/collection-entries.ts
|
|
3308
|
+
function isRec9(v) {
|
|
3031
3309
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3032
3310
|
}
|
|
3033
|
-
function strName11(v) {
|
|
3034
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3035
|
-
}
|
|
3036
|
-
function viewObjectName2(view) {
|
|
3037
|
-
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec8(view.data) ? strName11(view.data.object) : void 0);
|
|
3038
|
-
}
|
|
3039
3311
|
function collectionEntries(v, base) {
|
|
3040
3312
|
if (Array.isArray(v)) {
|
|
3041
3313
|
const out = [];
|
|
3042
3314
|
for (let i = 0; i < v.length; i++) {
|
|
3043
|
-
if (
|
|
3315
|
+
if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3044
3316
|
}
|
|
3045
3317
|
return out;
|
|
3046
3318
|
}
|
|
3047
|
-
if (
|
|
3048
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3319
|
+
if (isRec9(v)) {
|
|
3320
|
+
return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3049
3321
|
}
|
|
3050
3322
|
return [];
|
|
3051
3323
|
}
|
|
3324
|
+
|
|
3325
|
+
// src/validate-translatable-sections.ts
|
|
3326
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3327
|
+
function isRec10(v) {
|
|
3328
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3329
|
+
}
|
|
3330
|
+
function strName12(v) {
|
|
3331
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3332
|
+
}
|
|
3052
3333
|
function viewLabel(view) {
|
|
3053
|
-
const name =
|
|
3334
|
+
const name = strName12(view.name);
|
|
3054
3335
|
return name ? `view "${name}"` : "";
|
|
3055
3336
|
}
|
|
3056
3337
|
function joinWhere(...parts) {
|
|
3057
3338
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
3058
3339
|
}
|
|
3059
3340
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3060
|
-
const recordObject =
|
|
3061
|
-
const listBinding =
|
|
3062
|
-
const
|
|
3063
|
-
sites.push({
|
|
3064
|
-
path: `${basePath}.sections`,
|
|
3065
|
-
surface: label2,
|
|
3066
|
-
objectName: recordObject ?? listBinding,
|
|
3067
|
-
sections: view.sections
|
|
3068
|
-
});
|
|
3069
|
-
if (isRec8(view.form)) {
|
|
3341
|
+
const recordObject = viewObjectName(view);
|
|
3342
|
+
const listBinding = isRec10(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3343
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
3070
3344
|
sites.push({
|
|
3071
|
-
path: `${
|
|
3072
|
-
surface: joinWhere(label2,
|
|
3073
|
-
objectName:
|
|
3074
|
-
sections: view.
|
|
3345
|
+
path: `${site.path}.sections`,
|
|
3346
|
+
surface: joinWhere(label2, site.surface),
|
|
3347
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
3348
|
+
sections: site.view.sections
|
|
3075
3349
|
});
|
|
3076
3350
|
}
|
|
3077
|
-
for (const key of ["listViews", "formViews"]) {
|
|
3078
|
-
const container = view[key];
|
|
3079
|
-
if (!isRec8(container)) continue;
|
|
3080
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
3081
|
-
if (!isRec8(sub)) continue;
|
|
3082
|
-
sites.push({
|
|
3083
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
3084
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
3085
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
3086
|
-
sections: sub.sections
|
|
3087
|
-
});
|
|
3088
|
-
}
|
|
3089
|
-
}
|
|
3090
3351
|
}
|
|
3091
3352
|
function translatedObjectNames(stack) {
|
|
3092
3353
|
const out = /* @__PURE__ */ new Set();
|
|
3093
3354
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3094
3355
|
for (const bundle of bundles) {
|
|
3095
|
-
if (!
|
|
3356
|
+
if (!isRec10(bundle)) continue;
|
|
3096
3357
|
for (const data of Object.values(bundle)) {
|
|
3097
|
-
if (!
|
|
3358
|
+
if (!isRec10(data) || !isRec10(data.objects)) continue;
|
|
3098
3359
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3099
|
-
if (
|
|
3360
|
+
if (isRec10(node)) out.add(objectName);
|
|
3100
3361
|
}
|
|
3101
3362
|
}
|
|
3102
3363
|
}
|
|
@@ -3108,22 +3369,22 @@ function suggestedName(label2) {
|
|
|
3108
3369
|
}
|
|
3109
3370
|
function validateTranslatableSections(stack) {
|
|
3110
3371
|
const findings = [];
|
|
3111
|
-
if (!
|
|
3372
|
+
if (!isRec10(stack)) return findings;
|
|
3112
3373
|
const translated = translatedObjectNames(stack);
|
|
3113
3374
|
if (translated.size === 0) return findings;
|
|
3114
3375
|
const sites = [];
|
|
3115
3376
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3116
|
-
const objectName =
|
|
3377
|
+
const objectName = strName12(obj.name);
|
|
3117
3378
|
if (!objectName) continue;
|
|
3118
3379
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3119
3380
|
collectViewSites(
|
|
3120
|
-
{ ...view, object:
|
|
3381
|
+
{ ...view, object: strName12(view.object) ?? objectName },
|
|
3121
3382
|
path,
|
|
3122
3383
|
viewLabel(view),
|
|
3123
3384
|
sites
|
|
3124
3385
|
);
|
|
3125
3386
|
}
|
|
3126
|
-
if (
|
|
3387
|
+
if (isRec10(obj.listViews)) {
|
|
3127
3388
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3128
3389
|
}
|
|
3129
3390
|
}
|
|
@@ -3131,13 +3392,13 @@ function validateTranslatableSections(stack) {
|
|
|
3131
3392
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3132
3393
|
}
|
|
3133
3394
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3134
|
-
const pageName =
|
|
3395
|
+
const pageName = strName12(page.name);
|
|
3135
3396
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3136
3397
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3137
3398
|
if (!walked.objectName) continue;
|
|
3138
|
-
const props =
|
|
3399
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
3139
3400
|
if (!props) continue;
|
|
3140
|
-
const type =
|
|
3401
|
+
const type = strName12(walked.component.type) ?? "component";
|
|
3141
3402
|
sites.push({
|
|
3142
3403
|
path: `${walked.path}.properties.sections`,
|
|
3143
3404
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3152,9 +3413,9 @@ function validateTranslatableSections(stack) {
|
|
|
3152
3413
|
if (!Array.isArray(site.sections)) continue;
|
|
3153
3414
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3154
3415
|
const section = site.sections[i];
|
|
3155
|
-
if (!
|
|
3156
|
-
if (
|
|
3157
|
-
const heading =
|
|
3416
|
+
if (!isRec10(section)) continue;
|
|
3417
|
+
if (strName12(section.name)) continue;
|
|
3418
|
+
const heading = strName12(section.label);
|
|
3158
3419
|
if (!heading) continue;
|
|
3159
3420
|
const slug = suggestedName(heading);
|
|
3160
3421
|
findings.push({
|
|
@@ -3172,10 +3433,10 @@ function validateTranslatableSections(stack) {
|
|
|
3172
3433
|
|
|
3173
3434
|
// src/flow-walk.ts
|
|
3174
3435
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
3175
|
-
function
|
|
3436
|
+
function isRec11(v) {
|
|
3176
3437
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3177
3438
|
}
|
|
3178
|
-
function
|
|
3439
|
+
function strName13(v) {
|
|
3179
3440
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3180
3441
|
}
|
|
3181
3442
|
var REGION_SLOTS = new Map(
|
|
@@ -3184,10 +3445,10 @@ var REGION_SLOTS = new Map(
|
|
|
3184
3445
|
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
3185
3446
|
var MAX_REGION_DEPTH = 16;
|
|
3186
3447
|
function flowNodeLabel(node, index) {
|
|
3187
|
-
return
|
|
3448
|
+
return strName13(node.label) ?? strName13(node.id) ?? `#${index}`;
|
|
3188
3449
|
}
|
|
3189
3450
|
function stripRegions(config) {
|
|
3190
|
-
if (!
|
|
3451
|
+
if (!isRec11(config)) return void 0;
|
|
3191
3452
|
let out;
|
|
3192
3453
|
for (const key of Object.keys(config)) {
|
|
3193
3454
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3198,11 +3459,11 @@ function stripRegions(config) {
|
|
|
3198
3459
|
}
|
|
3199
3460
|
function walkFlowNodes(flow, flowPath) {
|
|
3200
3461
|
const out = [];
|
|
3201
|
-
if (!
|
|
3462
|
+
if (!isRec11(flow)) return out;
|
|
3202
3463
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3203
3464
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3204
3465
|
nodes.forEach((raw, index) => {
|
|
3205
|
-
if (!
|
|
3466
|
+
if (!isRec11(raw)) return;
|
|
3206
3467
|
const path = `${basePath}[${index}]`;
|
|
3207
3468
|
out.push({
|
|
3208
3469
|
node: raw,
|
|
@@ -3211,9 +3472,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3211
3472
|
regionTrail: trail,
|
|
3212
3473
|
depth
|
|
3213
3474
|
});
|
|
3214
|
-
const type =
|
|
3475
|
+
const type = strName13(raw.type);
|
|
3215
3476
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3216
|
-
if (!slots || !
|
|
3477
|
+
if (!slots || !isRec11(raw.config)) return;
|
|
3217
3478
|
const config = raw.config;
|
|
3218
3479
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3219
3480
|
for (const slot of slots) {
|
|
@@ -3221,8 +3482,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3221
3482
|
if (slot === "branches") {
|
|
3222
3483
|
if (!Array.isArray(value)) continue;
|
|
3223
3484
|
value.forEach((branch, b) => {
|
|
3224
|
-
if (!
|
|
3225
|
-
const branchName =
|
|
3485
|
+
if (!isRec11(branch)) return;
|
|
3486
|
+
const branchName = strName13(branch.name) ?? `#${b}`;
|
|
3226
3487
|
visitList(
|
|
3227
3488
|
branch.nodes,
|
|
3228
3489
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3232,7 +3493,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3232
3493
|
});
|
|
3233
3494
|
continue;
|
|
3234
3495
|
}
|
|
3235
|
-
if (!
|
|
3496
|
+
if (!isRec11(value)) continue;
|
|
3236
3497
|
visitList(
|
|
3237
3498
|
value.nodes,
|
|
3238
3499
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3457,7 +3718,7 @@ function asArray16(v) {
|
|
|
3457
3718
|
}
|
|
3458
3719
|
return [];
|
|
3459
3720
|
}
|
|
3460
|
-
function
|
|
3721
|
+
function strName14(v) {
|
|
3461
3722
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3462
3723
|
}
|
|
3463
3724
|
function surfaceOf(v) {
|
|
@@ -3468,17 +3729,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3468
3729
|
if (!stack || typeof stack !== "object") return findings;
|
|
3469
3730
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3470
3731
|
for (const skill of asArray16(stack.skills)) {
|
|
3471
|
-
const n =
|
|
3732
|
+
const n = strName14(skill.name);
|
|
3472
3733
|
if (n) skillsByName.set(n, skill);
|
|
3473
3734
|
}
|
|
3474
3735
|
const agents = asArray16(stack.agents);
|
|
3475
3736
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3476
3737
|
const agent = agents[ai];
|
|
3477
|
-
const agentName =
|
|
3738
|
+
const agentName = strName14(agent.name) ?? `#${ai}`;
|
|
3478
3739
|
const agentSurface = surfaceOf(agent.surface);
|
|
3479
3740
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3480
3741
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3481
|
-
const ref =
|
|
3742
|
+
const ref = strName14(skillRefs[si]);
|
|
3482
3743
|
if (!ref) continue;
|
|
3483
3744
|
const skill = skillsByName.get(ref);
|
|
3484
3745
|
if (!skill) continue;
|
|
@@ -3507,7 +3768,7 @@ function asArray17(v) {
|
|
|
3507
3768
|
}
|
|
3508
3769
|
return [];
|
|
3509
3770
|
}
|
|
3510
|
-
function
|
|
3771
|
+
function strName15(v) {
|
|
3511
3772
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3512
3773
|
}
|
|
3513
3774
|
function distance6(a, b) {
|
|
@@ -3548,8 +3809,8 @@ function materialisesAsTool(action) {
|
|
|
3548
3809
|
if (!ai || typeof ai !== "object") return false;
|
|
3549
3810
|
const aiRec = ai;
|
|
3550
3811
|
if (aiRec.exposed !== true) return false;
|
|
3551
|
-
if (!
|
|
3552
|
-
const type =
|
|
3812
|
+
if (!strName15(aiRec.description)) return false;
|
|
3813
|
+
const type = strName15(action.type);
|
|
3553
3814
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3554
3815
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3555
3816
|
return Boolean(action.target);
|
|
@@ -3557,12 +3818,12 @@ function materialisesAsTool(action) {
|
|
|
3557
3818
|
function collectToolUniverse(stack) {
|
|
3558
3819
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3559
3820
|
for (const tool of asArray17(stack.tools)) {
|
|
3560
|
-
const n =
|
|
3821
|
+
const n = strName15(tool.name);
|
|
3561
3822
|
if (n) universe.add(n);
|
|
3562
3823
|
}
|
|
3563
3824
|
const addActionFamily = (actions) => {
|
|
3564
3825
|
for (const action of asArray17(actions)) {
|
|
3565
|
-
const n =
|
|
3826
|
+
const n = strName15(action.name);
|
|
3566
3827
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3567
3828
|
}
|
|
3568
3829
|
};
|
|
@@ -3576,7 +3837,7 @@ function collectUnexposedActionNames(stack) {
|
|
|
3576
3837
|
const names = /* @__PURE__ */ new Set();
|
|
3577
3838
|
const scan = (actions) => {
|
|
3578
3839
|
for (const action of asArray17(actions)) {
|
|
3579
|
-
const n =
|
|
3840
|
+
const n = strName15(action.name);
|
|
3580
3841
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3581
3842
|
}
|
|
3582
3843
|
};
|
|
@@ -3602,10 +3863,10 @@ function validateAiToolReferences(stack) {
|
|
|
3602
3863
|
const skills = asArray17(stack.skills);
|
|
3603
3864
|
for (let si = 0; si < skills.length; si++) {
|
|
3604
3865
|
const skill = skills[si];
|
|
3605
|
-
const skillName =
|
|
3866
|
+
const skillName = strName15(skill.name) ?? `#${si}`;
|
|
3606
3867
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3607
3868
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3608
|
-
const ref =
|
|
3869
|
+
const ref = strName15(refs[ti]);
|
|
3609
3870
|
if (!ref || resolves(ref)) continue;
|
|
3610
3871
|
const isPattern = ref.endsWith("*");
|
|
3611
3872
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3624,6 +3885,7 @@ function validateAiToolReferences(stack) {
|
|
|
3624
3885
|
|
|
3625
3886
|
// src/validate-ai-agent-authoring.ts
|
|
3626
3887
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3888
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
3627
3889
|
function asArray18(v) {
|
|
3628
3890
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3629
3891
|
if (v && typeof v === "object") {
|
|
@@ -3631,7 +3893,7 @@ function asArray18(v) {
|
|
|
3631
3893
|
}
|
|
3632
3894
|
return [];
|
|
3633
3895
|
}
|
|
3634
|
-
function
|
|
3896
|
+
function strName16(v) {
|
|
3635
3897
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3636
3898
|
}
|
|
3637
3899
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
@@ -3641,7 +3903,7 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3641
3903
|
const agents = asArray18(stack.agents);
|
|
3642
3904
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3643
3905
|
const agent = agents[ai];
|
|
3644
|
-
const name =
|
|
3906
|
+
const name = strName16(agent.name) ?? `#${ai}`;
|
|
3645
3907
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3646
3908
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3647
3909
|
findings.push({
|
|
@@ -3653,6 +3915,22 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3653
3915
|
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.` : ``)
|
|
3654
3916
|
});
|
|
3655
3917
|
}
|
|
3918
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
3919
|
+
const apps = asArray18(stack.apps);
|
|
3920
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
3921
|
+
const app = apps[appIdx];
|
|
3922
|
+
const defaultAgent = strName16(app.defaultAgent);
|
|
3923
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
3924
|
+
const appName = strName16(app.name) ?? `#${appIdx}`;
|
|
3925
|
+
findings.push({
|
|
3926
|
+
severity: "warning",
|
|
3927
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
3928
|
+
where: `app "${appName}".defaultAgent`,
|
|
3929
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
3930
|
+
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.`,
|
|
3931
|
+
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.`
|
|
3932
|
+
});
|
|
3933
|
+
}
|
|
3656
3934
|
return findings;
|
|
3657
3935
|
}
|
|
3658
3936
|
|
|
@@ -3739,13 +4017,13 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
3739
4017
|
"owner",
|
|
3740
4018
|
"record_type"
|
|
3741
4019
|
]);
|
|
3742
|
-
var
|
|
4020
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3743
4021
|
function asArray19(v) {
|
|
3744
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3745
|
-
if (
|
|
4022
|
+
if (Array.isArray(v)) return v.filter((x) => isRec12(x));
|
|
4023
|
+
if (isRec12(v)) {
|
|
3746
4024
|
return Object.entries(v).map(([name, def]) => ({
|
|
3747
4025
|
name,
|
|
3748
|
-
...
|
|
4026
|
+
...isRec12(def) ? def : {}
|
|
3749
4027
|
}));
|
|
3750
4028
|
}
|
|
3751
4029
|
return [];
|
|
@@ -3891,7 +4169,7 @@ function validateHookBodyWrites(stack) {
|
|
|
3891
4169
|
let objectFields = null;
|
|
3892
4170
|
hooks.forEach((hook, hookIndex) => {
|
|
3893
4171
|
const body = hook.body;
|
|
3894
|
-
if (!
|
|
4172
|
+
if (!isRec12(body) || body.language !== "js") return;
|
|
3895
4173
|
const source = body.source;
|
|
3896
4174
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3897
4175
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -3962,13 +4240,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
3962
4240
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
3963
4241
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
3964
4242
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
3965
|
-
var
|
|
4243
|
+
var isRec13 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3966
4244
|
function asArray20(v) {
|
|
3967
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
3968
|
-
if (
|
|
4245
|
+
if (Array.isArray(v)) return v.filter((x) => isRec13(x));
|
|
4246
|
+
if (isRec13(v)) {
|
|
3969
4247
|
return Object.entries(v).map(([name, def]) => ({
|
|
3970
4248
|
name,
|
|
3971
|
-
...
|
|
4249
|
+
...isRec13(def) ? def : {}
|
|
3972
4250
|
}));
|
|
3973
4251
|
}
|
|
3974
4252
|
return [];
|
|
@@ -3986,7 +4264,7 @@ function collectActionBodies(stack) {
|
|
|
3986
4264
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
3987
4265
|
if (type !== "script") return;
|
|
3988
4266
|
const body = action.body;
|
|
3989
|
-
if (!
|
|
4267
|
+
if (!isRec13(body) || body.language !== "js") return;
|
|
3990
4268
|
const source = body.source;
|
|
3991
4269
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
3992
4270
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4005,7 +4283,7 @@ function collectActionBodies(stack) {
|
|
|
4005
4283
|
}
|
|
4006
4284
|
function validateActionBodyWrites(stack) {
|
|
4007
4285
|
const findings = [];
|
|
4008
|
-
if (!
|
|
4286
|
+
if (!isRec13(stack)) return findings;
|
|
4009
4287
|
const sites = collectActionBodies(stack);
|
|
4010
4288
|
if (sites.length === 0) return findings;
|
|
4011
4289
|
let objectFields = null;
|
|
@@ -4063,13 +4341,13 @@ function fixHint2(field, declared) {
|
|
|
4063
4341
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
4064
4342
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4065
4343
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4066
|
-
var
|
|
4344
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4067
4345
|
function asArray21(v) {
|
|
4068
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4069
|
-
if (
|
|
4346
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4347
|
+
if (isRec14(v)) {
|
|
4070
4348
|
return Object.entries(v).map(([name, def]) => ({
|
|
4071
4349
|
name,
|
|
4072
|
-
...
|
|
4350
|
+
...isRec14(def) ? def : {}
|
|
4073
4351
|
}));
|
|
4074
4352
|
}
|
|
4075
4353
|
return [];
|
|
@@ -4082,7 +4360,7 @@ function readLiteralObjectName(config) {
|
|
|
4082
4360
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4083
4361
|
function validateFlowNodeWrites(stack) {
|
|
4084
4362
|
const findings = [];
|
|
4085
|
-
if (!
|
|
4363
|
+
if (!isRec14(stack)) return findings;
|
|
4086
4364
|
const flows = asArray21(stack.flows);
|
|
4087
4365
|
if (flows.length === 0) return findings;
|
|
4088
4366
|
let objectFields = null;
|
|
@@ -4091,10 +4369,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4091
4369
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4092
4370
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4093
4371
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4094
|
-
const config =
|
|
4372
|
+
const config = isRec14(node.config) ? node.config : void 0;
|
|
4095
4373
|
if (!config) return;
|
|
4096
4374
|
const fields = config.fields;
|
|
4097
|
-
if (!
|
|
4375
|
+
if (!isRec14(fields)) return;
|
|
4098
4376
|
const written = Object.keys(fields);
|
|
4099
4377
|
if (written.length === 0) return;
|
|
4100
4378
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4230,11 +4508,11 @@ import {
|
|
|
4230
4508
|
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
4231
4509
|
|
|
4232
4510
|
// src/zod-issue-format.ts
|
|
4233
|
-
var
|
|
4511
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4234
4512
|
var valueAtPath = (root, path) => {
|
|
4235
4513
|
let cur = root;
|
|
4236
4514
|
for (const key of path) {
|
|
4237
|
-
if (!
|
|
4515
|
+
if (!isRec15(cur) && !Array.isArray(cur)) return void 0;
|
|
4238
4516
|
cur = cur[key];
|
|
4239
4517
|
}
|
|
4240
4518
|
return cur;
|
|
@@ -4372,7 +4650,7 @@ var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
|
4372
4650
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4373
4651
|
function checkChartDrillDown(raw, push2) {
|
|
4374
4652
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4375
|
-
if (!
|
|
4653
|
+
if (!isRec16(raw)) {
|
|
4376
4654
|
push2(
|
|
4377
4655
|
"error",
|
|
4378
4656
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4395,7 +4673,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4395
4673
|
}
|
|
4396
4674
|
function checkChartAggregate(raw, push2) {
|
|
4397
4675
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4398
|
-
if (!
|
|
4676
|
+
if (!isRec16(raw)) {
|
|
4399
4677
|
push2(
|
|
4400
4678
|
"error",
|
|
4401
4679
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4410,7 +4688,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4410
4688
|
"warning",
|
|
4411
4689
|
REACT_CHART_AGGREGATE_INVALID,
|
|
4412
4690
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
4413
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
4691
|
+
"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."
|
|
4414
4692
|
);
|
|
4415
4693
|
}
|
|
4416
4694
|
const parsed = ChartAggregateSchema.safeParse(raw);
|
|
@@ -4426,7 +4704,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
4426
4704
|
);
|
|
4427
4705
|
}
|
|
4428
4706
|
}
|
|
4429
|
-
var
|
|
4707
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4430
4708
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4431
4709
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
4432
4710
|
const { values, where, path } = attrs;
|
|
@@ -4436,11 +4714,11 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4436
4714
|
const aggregate = values.get("aggregate");
|
|
4437
4715
|
checkChartAggregate(aggregate, push2);
|
|
4438
4716
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4439
|
-
if (!
|
|
4717
|
+
if (!isRec16(aggregate)) return;
|
|
4440
4718
|
const fn = strOf(aggregate.function);
|
|
4441
4719
|
const field = strOf(aggregate.field);
|
|
4442
4720
|
const groupBy = aggregate.groupBy;
|
|
4443
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4721
|
+
const groupByField = strOf(groupBy) ?? (isRec16(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4444
4722
|
const objectName = strOf(values.get("objectName"));
|
|
4445
4723
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4446
4724
|
if (objectName && known) {
|
|
@@ -4473,18 +4751,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4473
4751
|
);
|
|
4474
4752
|
};
|
|
4475
4753
|
const xAxisRaw = values.get("xAxis");
|
|
4476
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
4754
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec16(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4477
4755
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4478
4756
|
axisRef(categoryAxis, categoryProp);
|
|
4479
4757
|
const yAxisRaw = values.get("yAxis");
|
|
4480
4758
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4481
4759
|
for (const a of yAxisList) {
|
|
4482
|
-
axisRef(strOf(a) ?? (
|
|
4760
|
+
axisRef(strOf(a) ?? (isRec16(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4483
4761
|
}
|
|
4484
4762
|
const series = values.get("series");
|
|
4485
4763
|
if (Array.isArray(series)) {
|
|
4486
4764
|
for (const s of series) {
|
|
4487
|
-
if (!
|
|
4765
|
+
if (!isRec16(s)) continue;
|
|
4488
4766
|
const dataKey = strOf(s.dataKey);
|
|
4489
4767
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4490
4768
|
}
|
|
@@ -4540,7 +4818,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4540
4818
|
if (!Array.isArray(value)) return { child, parent };
|
|
4541
4819
|
for (let i = 0; i < value.length; i++) {
|
|
4542
4820
|
const sub = value[i];
|
|
4543
|
-
if (!
|
|
4821
|
+
if (!isRec16(sub)) continue;
|
|
4544
4822
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4545
4823
|
child.push({
|
|
4546
4824
|
objectName: strOf(sub.childObject),
|
|
@@ -4585,20 +4863,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4585
4863
|
}
|
|
4586
4864
|
for (const key of spec.nestedFields ?? []) {
|
|
4587
4865
|
const v = readable(key);
|
|
4588
|
-
if (
|
|
4866
|
+
if (isRec16(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4589
4867
|
}
|
|
4590
4868
|
for (const key of spec.sections ?? []) {
|
|
4591
4869
|
const v = readable(key);
|
|
4592
4870
|
if (!Array.isArray(v)) continue;
|
|
4593
4871
|
for (let i = 0; i < v.length; i++) {
|
|
4594
4872
|
const section = v[i];
|
|
4595
|
-
if (!
|
|
4873
|
+
if (!isRec16(section)) continue;
|
|
4596
4874
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4597
4875
|
}
|
|
4598
4876
|
}
|
|
4599
4877
|
for (const key of spec.keyedByField ?? []) {
|
|
4600
4878
|
const v = readable(key);
|
|
4601
|
-
if (!
|
|
4879
|
+
if (!isRec16(v)) continue;
|
|
4602
4880
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4603
4881
|
}
|
|
4604
4882
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4885,10 +5163,10 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
4885
5163
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
4886
5164
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
4887
5165
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
4888
|
-
function
|
|
5166
|
+
function isRec17(v) {
|
|
4889
5167
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4890
5168
|
}
|
|
4891
|
-
function
|
|
5169
|
+
function strName17(v) {
|
|
4892
5170
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4893
5171
|
}
|
|
4894
5172
|
function asArray24(v) {
|
|
@@ -4902,23 +5180,23 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
4902
5180
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
4903
5181
|
function suppliedByDataSource(issue, component) {
|
|
4904
5182
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
4905
|
-
const dataSource =
|
|
4906
|
-
return
|
|
5183
|
+
const dataSource = isRec17(component.dataSource) ? component.dataSource : void 0;
|
|
5184
|
+
return strName17(dataSource?.object) !== void 0;
|
|
4907
5185
|
}
|
|
4908
5186
|
function validateComponentProps(stack) {
|
|
4909
5187
|
const findings = [];
|
|
4910
|
-
if (!
|
|
5188
|
+
if (!isRec17(stack)) return findings;
|
|
4911
5189
|
const pages = asArray24(stack.pages);
|
|
4912
5190
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4913
5191
|
const page = pages[pi];
|
|
4914
|
-
if (!
|
|
4915
|
-
const pageName =
|
|
5192
|
+
if (!isRec17(page)) continue;
|
|
5193
|
+
const pageName = strName17(page.name) ?? `#${pi}`;
|
|
4916
5194
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4917
|
-
const type =
|
|
5195
|
+
const type = strName17(component.type);
|
|
4918
5196
|
if (!type) continue;
|
|
4919
5197
|
const schema = PROPS_SCHEMAS[type];
|
|
4920
5198
|
if (!schema) continue;
|
|
4921
|
-
const props =
|
|
5199
|
+
const props = isRec17(component.properties) ? component.properties : void 0;
|
|
4922
5200
|
if (!props) continue;
|
|
4923
5201
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
4924
5202
|
const base = `${path}.properties`;
|
|
@@ -5511,6 +5789,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
5511
5789
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
5512
5790
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
5513
5791
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
5792
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5514
5793
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5515
5794
|
function asArray30(v) {
|
|
5516
5795
|
if (Array.isArray(v)) return v;
|
|
@@ -5528,6 +5807,11 @@ function renderNonObject(v) {
|
|
|
5528
5807
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
5529
5808
|
return `a ${t}`;
|
|
5530
5809
|
}
|
|
5810
|
+
function renderTriggerToken(v) {
|
|
5811
|
+
if (typeof v === "string") return `'${v}'`;
|
|
5812
|
+
const json = JSON.stringify(v);
|
|
5813
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
5814
|
+
}
|
|
5531
5815
|
function startNodeOf(flow) {
|
|
5532
5816
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
5533
5817
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -5647,6 +5931,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
5647
5931
|
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.`
|
|
5648
5932
|
});
|
|
5649
5933
|
}
|
|
5934
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
5935
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
5936
|
+
const hasTriggerType = config.triggerType != null;
|
|
5937
|
+
findings.push({
|
|
5938
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
5939
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
5940
|
+
// with no registry lookup in them — so no installed package can make
|
|
5941
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
5942
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
5943
|
+
// record-change trigger itself, and it still would not help, because
|
|
5944
|
+
// the flow never reaches the point of asking for one.
|
|
5945
|
+
severity: "error",
|
|
5946
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
5947
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
5948
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
5949
|
+
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.`,
|
|
5950
|
+
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.`
|
|
5951
|
+
});
|
|
5952
|
+
}
|
|
5650
5953
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
5651
5954
|
findings.push({
|
|
5652
5955
|
severity: "warning",
|
|
@@ -6006,7 +6309,7 @@ function validateSemanticRoles(stack) {
|
|
|
6006
6309
|
(h) => typeof h === "string" && h.length > 0
|
|
6007
6310
|
);
|
|
6008
6311
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
6009
|
-
const declaredTitle = [obj.nameField, obj.
|
|
6312
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
6010
6313
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
6011
6314
|
const stripSet = new Set(
|
|
6012
6315
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -6041,6 +6344,12 @@ function asArray34(v) {
|
|
|
6041
6344
|
}
|
|
6042
6345
|
return [];
|
|
6043
6346
|
}
|
|
6347
|
+
function isRec18(v) {
|
|
6348
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6349
|
+
}
|
|
6350
|
+
function strName18(v) {
|
|
6351
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6352
|
+
}
|
|
6044
6353
|
function fieldNameOf(entry) {
|
|
6045
6354
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
6046
6355
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -6049,13 +6358,6 @@ function fieldNameOf(entry) {
|
|
|
6049
6358
|
}
|
|
6050
6359
|
return null;
|
|
6051
6360
|
}
|
|
6052
|
-
function boundObject(view) {
|
|
6053
|
-
const data = view.data;
|
|
6054
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
6055
|
-
return data.object;
|
|
6056
|
-
}
|
|
6057
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
6058
|
-
}
|
|
6059
6361
|
function validateFormLayout(stack) {
|
|
6060
6362
|
const findings = [];
|
|
6061
6363
|
const objectFields = /* @__PURE__ */ new Map();
|
|
@@ -6065,44 +6367,44 @@ function validateFormLayout(stack) {
|
|
|
6065
6367
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6066
6368
|
objectFields.set(name, new Set(fields));
|
|
6067
6369
|
}
|
|
6068
|
-
const
|
|
6069
|
-
|
|
6070
|
-
const
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
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
|
-
}
|
|
6370
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6371
|
+
const viewName = strName18(view.name) ?? strName18(view.object) ?? viewPath;
|
|
6372
|
+
const containerObject = viewObjectName(view);
|
|
6373
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6374
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
6375
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
6376
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6377
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6378
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6379
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6380
|
+
const sec = sections[s];
|
|
6381
|
+
const secFields = isRec18(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6382
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6383
|
+
const entry = secFields[f];
|
|
6384
|
+
const fname = fieldNameOf(entry);
|
|
6385
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
6386
|
+
if (fname && known && !known.has(fname)) {
|
|
6387
|
+
findings.push({
|
|
6388
|
+
severity: "warning",
|
|
6389
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
6390
|
+
where,
|
|
6391
|
+
path: fpath,
|
|
6392
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
6393
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6394
|
+
});
|
|
6395
|
+
}
|
|
6396
|
+
const colSpan = isRec18(entry) ? entry.colSpan : void 0;
|
|
6397
|
+
if (colSpan != null) {
|
|
6398
|
+
findings.push({
|
|
6399
|
+
severity: "warning",
|
|
6400
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
6401
|
+
where,
|
|
6402
|
+
path: `${fpath}.colSpan`,
|
|
6403
|
+
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`,
|
|
6404
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
6405
|
+
});
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6106
6408
|
}
|
|
6107
6409
|
}
|
|
6108
6410
|
}
|
|
@@ -6201,17 +6503,17 @@ function validateSeedStateMachine(stack) {
|
|
|
6201
6503
|
}
|
|
6202
6504
|
|
|
6203
6505
|
// src/validate-visibility-predicates.ts
|
|
6204
|
-
|
|
6506
|
+
import {
|
|
6507
|
+
collectCelRootIdentifiers as collectCelRootIdentifiers3,
|
|
6508
|
+
firstUndeclaredReference,
|
|
6509
|
+
parseCelToAst as parseCelToAst2,
|
|
6510
|
+
parseCelToAstWithReason
|
|
6511
|
+
} from "@objectstack/formula";
|
|
6205
6512
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6513
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6514
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
6515
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
6206
6516
|
var CANONICAL = "visibleWhen";
|
|
6207
|
-
var ALIASES = ["visibleOn", "visibility"];
|
|
6208
|
-
function asArray35(v) {
|
|
6209
|
-
if (Array.isArray(v)) return v;
|
|
6210
|
-
if (v && typeof v === "object") {
|
|
6211
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6212
|
-
}
|
|
6213
|
-
return [];
|
|
6214
|
-
}
|
|
6215
6517
|
function predicateSource(v) {
|
|
6216
6518
|
if (typeof v === "string") return v;
|
|
6217
6519
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
@@ -6222,6 +6524,67 @@ function predicateSource(v) {
|
|
|
6222
6524
|
function usesRoot(source, root) {
|
|
6223
6525
|
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
6224
6526
|
}
|
|
6527
|
+
function withoutStringLiterals(source) {
|
|
6528
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
6529
|
+
}
|
|
6530
|
+
var NON_CEL_SPELLINGS = [
|
|
6531
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
6532
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
6533
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
6534
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
6535
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
6536
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
6537
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
6538
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
6539
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
6540
|
+
];
|
|
6541
|
+
function quoteSource(source) {
|
|
6542
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
6543
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
6544
|
+
}
|
|
6545
|
+
function celRefusal(source) {
|
|
6546
|
+
if (!source.trim()) return null;
|
|
6547
|
+
const parsed = parseCelToAstWithReason(source);
|
|
6548
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
6549
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
6550
|
+
const identifiers = collectCelRootIdentifiers3(source);
|
|
6551
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
6552
|
+
const scannable = withoutStringLiterals(source);
|
|
6553
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
6554
|
+
}
|
|
6555
|
+
function boundName(overrun) {
|
|
6556
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6557
|
+
}
|
|
6558
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6559
|
+
function isNode2(v) {
|
|
6560
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6561
|
+
}
|
|
6562
|
+
function namespaceRoots(node, out) {
|
|
6563
|
+
if (Array.isArray(node)) {
|
|
6564
|
+
for (const child of node) namespaceRoots(child, out);
|
|
6565
|
+
return;
|
|
6566
|
+
}
|
|
6567
|
+
if (!isNode2(node)) return;
|
|
6568
|
+
const args = node.args;
|
|
6569
|
+
if (Array.isArray(args)) {
|
|
6570
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6571
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6572
|
+
out.add(receiver.args);
|
|
6573
|
+
}
|
|
6574
|
+
}
|
|
6575
|
+
namespaceRoots(args, out);
|
|
6576
|
+
}
|
|
6577
|
+
function firstBareIdentifier(source) {
|
|
6578
|
+
const ast = parseCelToAst2(source);
|
|
6579
|
+
if (!ast) return null;
|
|
6580
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
6581
|
+
namespaceRoots(ast, rooted);
|
|
6582
|
+
return firstUndeclaredReference(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
|
|
6583
|
+
}
|
|
6584
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
6585
|
+
runtime: "record",
|
|
6586
|
+
metadata: "data"
|
|
6587
|
+
};
|
|
6225
6588
|
var MISLAYER_BY_LAYER = {
|
|
6226
6589
|
runtime: {
|
|
6227
6590
|
forbiddenRoot: "data",
|
|
@@ -6235,18 +6598,6 @@ var MISLAYER_BY_LAYER = {
|
|
|
6235
6598
|
}
|
|
6236
6599
|
};
|
|
6237
6600
|
function checkElement(el, where, path, layer, findings) {
|
|
6238
|
-
for (const alias of ALIASES) {
|
|
6239
|
-
if (el[alias] !== void 0) {
|
|
6240
|
-
findings.push({
|
|
6241
|
-
severity: "warning",
|
|
6242
|
-
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
6243
|
-
where,
|
|
6244
|
-
path: `${path}.${alias}`,
|
|
6245
|
-
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\`.`,
|
|
6246
|
-
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
6247
|
-
});
|
|
6248
|
-
}
|
|
6249
|
-
}
|
|
6250
6601
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6251
6602
|
const source = predicateSource(raw);
|
|
6252
6603
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -6260,6 +6611,43 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6260
6611
|
hint: rule.hint
|
|
6261
6612
|
});
|
|
6262
6613
|
}
|
|
6614
|
+
const refusal = source ? celRefusal(source) : null;
|
|
6615
|
+
if (source && refusal?.kind === "bounds") {
|
|
6616
|
+
const bound = boundName(refusal.overrun);
|
|
6617
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6618
|
+
findings.push({
|
|
6619
|
+
severity: "error",
|
|
6620
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
6621
|
+
where,
|
|
6622
|
+
path,
|
|
6623
|
+
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).`,
|
|
6624
|
+
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.`
|
|
6625
|
+
});
|
|
6626
|
+
}
|
|
6627
|
+
if (source && refusal?.kind === "syntax") {
|
|
6628
|
+
findings.push({
|
|
6629
|
+
severity: "error",
|
|
6630
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
6631
|
+
where,
|
|
6632
|
+
path,
|
|
6633
|
+
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).`,
|
|
6634
|
+
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\`).`
|
|
6635
|
+
});
|
|
6636
|
+
}
|
|
6637
|
+
if (source && !refusal) {
|
|
6638
|
+
const bare = firstBareIdentifier(source);
|
|
6639
|
+
if (bare) {
|
|
6640
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6641
|
+
findings.push({
|
|
6642
|
+
severity: "error",
|
|
6643
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
6644
|
+
where,
|
|
6645
|
+
path,
|
|
6646
|
+
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).`,
|
|
6647
|
+
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`).")
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6650
|
+
}
|
|
6263
6651
|
}
|
|
6264
6652
|
function isFieldObject(entry) {
|
|
6265
6653
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
@@ -6267,43 +6655,304 @@ function isFieldObject(entry) {
|
|
|
6267
6655
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
6268
6656
|
const layer = opts.layer ?? "runtime";
|
|
6269
6657
|
const findings = [];
|
|
6270
|
-
const
|
|
6271
|
-
|
|
6272
|
-
const view
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
6658
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6659
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6660
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6661
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
6662
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6663
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6664
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6665
|
+
const sec = sections[s];
|
|
6666
|
+
if (!sec || typeof sec !== "object") continue;
|
|
6667
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
6668
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
6669
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
6670
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
6671
|
+
const entry = secFields[f];
|
|
6672
|
+
if (isFieldObject(entry)) {
|
|
6673
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
6674
|
+
}
|
|
6288
6675
|
}
|
|
6289
6676
|
}
|
|
6290
6677
|
}
|
|
6291
6678
|
}
|
|
6292
6679
|
}
|
|
6293
|
-
const
|
|
6294
|
-
|
|
6295
|
-
const
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6680
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6681
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
6682
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
6683
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6684
|
+
checkElement(walked.component, where, walked.path, layer, findings);
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
return findings;
|
|
6688
|
+
}
|
|
6689
|
+
|
|
6690
|
+
// src/validate-predicate-path-refs.ts
|
|
6691
|
+
import { parseCelToAst as parseCelToAst3 } from "@objectstack/formula";
|
|
6692
|
+
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
6693
|
+
import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec";
|
|
6694
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
6695
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
6696
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
6697
|
+
var ROOT = "data";
|
|
6698
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6699
|
+
function defOf(schema) {
|
|
6700
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
6701
|
+
const s = schema;
|
|
6702
|
+
return s.def ?? s._def;
|
|
6703
|
+
}
|
|
6704
|
+
function peel(schema, depth = 0) {
|
|
6705
|
+
if (!schema || depth > 25) return schema;
|
|
6706
|
+
const d = defOf(schema);
|
|
6707
|
+
if (!d) return schema;
|
|
6708
|
+
switch (d.type) {
|
|
6709
|
+
case "optional":
|
|
6710
|
+
case "nullable":
|
|
6711
|
+
case "default":
|
|
6712
|
+
case "prefault":
|
|
6713
|
+
case "readonly":
|
|
6714
|
+
case "catch":
|
|
6715
|
+
case "nonoptional":
|
|
6716
|
+
return peel(d.innerType, depth + 1);
|
|
6717
|
+
case "lazy":
|
|
6718
|
+
return peel(d.getter(), depth + 1);
|
|
6719
|
+
case "pipe": {
|
|
6720
|
+
const inner = peel(d.in, depth + 1);
|
|
6721
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
6722
|
+
}
|
|
6723
|
+
default:
|
|
6724
|
+
return schema;
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
function optionsOf(d) {
|
|
6728
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
6729
|
+
}
|
|
6730
|
+
function keysOf(schema, depth = 0) {
|
|
6731
|
+
if (depth > 25) return null;
|
|
6732
|
+
const u = peel(schema);
|
|
6733
|
+
const d = defOf(u);
|
|
6734
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
6735
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
6736
|
+
const all = /* @__PURE__ */ new Set();
|
|
6737
|
+
let keyBearing = false;
|
|
6738
|
+
for (const option of optionsOf(d)) {
|
|
6739
|
+
const k = keysOf(option, depth + 1);
|
|
6740
|
+
if (!k) continue;
|
|
6741
|
+
keyBearing = true;
|
|
6742
|
+
for (const key of k) all.add(key);
|
|
6743
|
+
}
|
|
6744
|
+
return keyBearing ? [...all] : null;
|
|
6745
|
+
}
|
|
6746
|
+
if (d?.type === "intersection") {
|
|
6747
|
+
const left = keysOf(d.left, depth + 1);
|
|
6748
|
+
const right = keysOf(d.right, depth + 1);
|
|
6749
|
+
if (!left && !right) return null;
|
|
6750
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
6751
|
+
}
|
|
6752
|
+
return null;
|
|
6753
|
+
}
|
|
6754
|
+
function propertyOf(schema, key, depth = 0) {
|
|
6755
|
+
if (depth > 25) return void 0;
|
|
6756
|
+
const u = peel(schema);
|
|
6757
|
+
const d = defOf(u);
|
|
6758
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
6759
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
6760
|
+
for (const option of optionsOf(d)) {
|
|
6761
|
+
const found = propertyOf(option, key, depth + 1);
|
|
6762
|
+
if (found !== void 0) return found;
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
if (d?.type === "intersection") {
|
|
6766
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
6767
|
+
}
|
|
6768
|
+
return void 0;
|
|
6769
|
+
}
|
|
6770
|
+
function rowScopeOf(scope, key) {
|
|
6771
|
+
const prop = propertyOf(scope, key);
|
|
6772
|
+
if (prop === void 0) return void 0;
|
|
6773
|
+
let node = peel(prop);
|
|
6774
|
+
for (let i = 0; i < 25; i++) {
|
|
6775
|
+
const d = defOf(node);
|
|
6776
|
+
if (d?.type === "array") node = peel(d.element);
|
|
6777
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
6778
|
+
else return node;
|
|
6779
|
+
}
|
|
6780
|
+
return node;
|
|
6781
|
+
}
|
|
6782
|
+
function stepInto(scope, segment) {
|
|
6783
|
+
const u = peel(scope);
|
|
6784
|
+
const d = defOf(u);
|
|
6785
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
6786
|
+
const declared = keysOf(u);
|
|
6787
|
+
if (declared === null) return { kind: "opaque" };
|
|
6788
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
6789
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
6790
|
+
}
|
|
6791
|
+
function isNode3(v) {
|
|
6792
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6793
|
+
}
|
|
6794
|
+
function memberChain(node) {
|
|
6795
|
+
if (!isNode3(node)) return null;
|
|
6796
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
6797
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
6798
|
+
const head = memberChain(node.args[0]);
|
|
6799
|
+
return head ? [...head, node.args[1]] : null;
|
|
6800
|
+
}
|
|
6801
|
+
return null;
|
|
6802
|
+
}
|
|
6803
|
+
function rootedPaths(node, out) {
|
|
6804
|
+
if (Array.isArray(node)) {
|
|
6805
|
+
for (const child of node) rootedPaths(child, out);
|
|
6806
|
+
return;
|
|
6807
|
+
}
|
|
6808
|
+
if (!isNode3(node)) return;
|
|
6809
|
+
if (node.op === ".") {
|
|
6810
|
+
const chain = memberChain(node);
|
|
6811
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
6812
|
+
out.push(chain.slice(1));
|
|
6813
|
+
return;
|
|
6814
|
+
}
|
|
6815
|
+
}
|
|
6816
|
+
rootedPaths(node.args, out);
|
|
6817
|
+
}
|
|
6818
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
6819
|
+
if (Array.isArray(node)) {
|
|
6820
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
6821
|
+
return;
|
|
6822
|
+
}
|
|
6823
|
+
if (!isNode3(node)) return;
|
|
6824
|
+
const args = node.args;
|
|
6825
|
+
if (Array.isArray(args)) {
|
|
6826
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6827
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6828
|
+
excluded.add(receiver.args);
|
|
6829
|
+
}
|
|
6830
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6831
|
+
const macroArgs = args[2];
|
|
6832
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
6833
|
+
const bound = macroArgs[0];
|
|
6834
|
+
if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
6835
|
+
excluded.add(bound.args);
|
|
6836
|
+
}
|
|
6837
|
+
}
|
|
6838
|
+
}
|
|
6839
|
+
}
|
|
6840
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
6841
|
+
values.add(node.args);
|
|
6842
|
+
return;
|
|
6843
|
+
}
|
|
6844
|
+
classifyIdentifiers(args, values, excluded);
|
|
6845
|
+
}
|
|
6846
|
+
function predicateSource2(v) {
|
|
6847
|
+
if (typeof v === "string") return v;
|
|
6848
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
6849
|
+
return v.source;
|
|
6850
|
+
}
|
|
6851
|
+
return void 0;
|
|
6852
|
+
}
|
|
6853
|
+
function isRec19(v) {
|
|
6854
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6855
|
+
}
|
|
6856
|
+
function schemaIdOf(view) {
|
|
6857
|
+
const data = view.data;
|
|
6858
|
+
if (!isRec19(data)) return void 0;
|
|
6859
|
+
if (data.provider !== "schema") return void 0;
|
|
6860
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6861
|
+
}
|
|
6862
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
6863
|
+
const ast = parseCelToAst3(source);
|
|
6864
|
+
if (!ast) return;
|
|
6865
|
+
const paths = [];
|
|
6866
|
+
rootedPaths(ast, paths);
|
|
6867
|
+
for (const segments of paths) {
|
|
6868
|
+
let cursor = scope;
|
|
6869
|
+
const walked = [];
|
|
6870
|
+
for (const segment of segments) {
|
|
6871
|
+
const step = stepInto(cursor, segment);
|
|
6872
|
+
if (step.kind === "opaque") break;
|
|
6873
|
+
if (step.kind === "undeclared") {
|
|
6874
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
6875
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
6876
|
+
findings.push({
|
|
6877
|
+
severity: "error",
|
|
6878
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
6879
|
+
where,
|
|
6880
|
+
path,
|
|
6881
|
+
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).`,
|
|
6882
|
+
hint: `${formatSuggestion4(findClosestMatches4(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
6883
|
+
});
|
|
6884
|
+
break;
|
|
6885
|
+
}
|
|
6886
|
+
walked.push(segment);
|
|
6887
|
+
cursor = step.next;
|
|
6888
|
+
}
|
|
6889
|
+
}
|
|
6890
|
+
const declaredHere = keysOf(scope);
|
|
6891
|
+
if (!declaredHere) return;
|
|
6892
|
+
const values = /* @__PURE__ */ new Set();
|
|
6893
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
6894
|
+
classifyIdentifiers(ast, values, excluded);
|
|
6895
|
+
for (const id of values) {
|
|
6896
|
+
if (excluded.has(id) || !declaredHere.includes(id)) continue;
|
|
6897
|
+
findings.push({
|
|
6898
|
+
severity: "error",
|
|
6899
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
6900
|
+
where,
|
|
6901
|
+
path,
|
|
6902
|
+
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).`,
|
|
6903
|
+
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).`
|
|
6904
|
+
});
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
6908
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
6909
|
+
for (let i = 0; i < entries.length; i++) {
|
|
6910
|
+
const entry = entries[i];
|
|
6911
|
+
if (!isRec19(entry)) continue;
|
|
6912
|
+
const path = `${base}[${i}]`;
|
|
6913
|
+
for (const key of PREDICATE_KEYS) {
|
|
6914
|
+
const source = predicateSource2(entry[key]);
|
|
6915
|
+
if (source !== void 0 && source.trim()) {
|
|
6916
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
6917
|
+
break;
|
|
6918
|
+
}
|
|
6919
|
+
}
|
|
6920
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
6921
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
6922
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
6923
|
+
}
|
|
6924
|
+
}
|
|
6925
|
+
}
|
|
6926
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
6927
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => getMetadataTypeSchema(schemaId));
|
|
6928
|
+
const findings = [];
|
|
6929
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6930
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6931
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
6932
|
+
const schemaId = schemaIdOf(site.view);
|
|
6933
|
+
if (!schemaId) continue;
|
|
6934
|
+
let root;
|
|
6935
|
+
try {
|
|
6936
|
+
root = resolveSchema(schemaId);
|
|
6937
|
+
} catch {
|
|
6938
|
+
continue;
|
|
6939
|
+
}
|
|
6940
|
+
if (!root) continue;
|
|
6941
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
6942
|
+
for (const bucket of ["sections", "groups"]) {
|
|
6943
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6944
|
+
for (let s = 0; s < sections.length; s++) {
|
|
6945
|
+
const section = sections[s];
|
|
6946
|
+
if (!isRec19(section)) continue;
|
|
6947
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
6948
|
+
for (const key of PREDICATE_KEYS) {
|
|
6949
|
+
const source = predicateSource2(section[key]);
|
|
6950
|
+
if (source !== void 0 && source.trim()) {
|
|
6951
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
6952
|
+
break;
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
6307
6956
|
}
|
|
6308
6957
|
}
|
|
6309
6958
|
}
|
|
@@ -6337,7 +6986,7 @@ var OWD_WIDTH = {
|
|
|
6337
6986
|
public_read: 1,
|
|
6338
6987
|
public_read_write: 2
|
|
6339
6988
|
};
|
|
6340
|
-
function
|
|
6989
|
+
function asArray35(v) {
|
|
6341
6990
|
if (Array.isArray(v)) return v;
|
|
6342
6991
|
if (v && typeof v === "object") {
|
|
6343
6992
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6363,7 +7012,7 @@ function refOf(def) {
|
|
|
6363
7012
|
return typeof r === "string" && r ? r : void 0;
|
|
6364
7013
|
}
|
|
6365
7014
|
function firstMasterDetailField(obj) {
|
|
6366
|
-
for (const f of
|
|
7015
|
+
for (const f of asArray35(obj.fields)) {
|
|
6367
7016
|
if (f.type === "master_detail") {
|
|
6368
7017
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
6369
7018
|
}
|
|
@@ -6376,8 +7025,8 @@ function grantsObjectAccess(p) {
|
|
|
6376
7025
|
function validateSecurityPosture(stack, opts) {
|
|
6377
7026
|
const findings = [];
|
|
6378
7027
|
if (!stack || typeof stack !== "object") return findings;
|
|
6379
|
-
const objects =
|
|
6380
|
-
const permissionSets =
|
|
7028
|
+
const objects = asArray35(stack.objects);
|
|
7029
|
+
const permissionSets = asArray35(stack.permissions);
|
|
6381
7030
|
for (let i = 0; i < objects.length; i++) {
|
|
6382
7031
|
const obj = objects[i];
|
|
6383
7032
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -6506,10 +7155,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6506
7155
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
6507
7156
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
6508
7157
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
6509
|
-
for (const f of
|
|
7158
|
+
for (const f of asArray35(obj.fields)) {
|
|
6510
7159
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
6511
7160
|
}
|
|
6512
|
-
for (const [ai, action] of
|
|
7161
|
+
for (const [ai, action] of asArray35(obj.actions).entries()) {
|
|
6513
7162
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
6514
7163
|
}
|
|
6515
7164
|
}
|
|
@@ -6518,19 +7167,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6518
7167
|
if (!ps || typeof ps !== "object") continue;
|
|
6519
7168
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
6520
7169
|
}
|
|
6521
|
-
for (const [i, pos] of
|
|
7170
|
+
for (const [i, pos] of asArray35(stack.positions).entries()) {
|
|
6522
7171
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
6523
7172
|
}
|
|
6524
|
-
for (const [i, app] of
|
|
7173
|
+
for (const [i, app] of asArray35(stack.apps).entries()) {
|
|
6525
7174
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
6526
7175
|
}
|
|
6527
|
-
for (const [i, book] of
|
|
7176
|
+
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
6528
7177
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
6529
7178
|
}
|
|
6530
7179
|
const stackSetNames = new Set(
|
|
6531
7180
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
6532
7181
|
);
|
|
6533
|
-
for (const [i, book] of
|
|
7182
|
+
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
6534
7183
|
const audience = book.audience;
|
|
6535
7184
|
if (!audience || typeof audience !== "object") continue;
|
|
6536
7185
|
const setName = audience.permissionSet;
|
|
@@ -6608,7 +7257,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
6608
7257
|
}
|
|
6609
7258
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
6610
7259
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
6611
|
-
for (const [i, seed] of
|
|
7260
|
+
for (const [i, seed] of asArray35(stack.data).entries()) {
|
|
6612
7261
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
6613
7262
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
6614
7263
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -6653,7 +7302,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
|
6653
7302
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
6654
7303
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
6655
7304
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
6656
|
-
function
|
|
7305
|
+
function asArray36(v) {
|
|
6657
7306
|
if (Array.isArray(v)) return v;
|
|
6658
7307
|
if (v && typeof v === "object") {
|
|
6659
7308
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6683,9 +7332,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
6683
7332
|
function validateOrgAxisRedLines(stack) {
|
|
6684
7333
|
const findings = [];
|
|
6685
7334
|
const cfg = stack ?? {};
|
|
6686
|
-
const permissionSets =
|
|
7335
|
+
const permissionSets = asArray36(cfg.permissions);
|
|
6687
7336
|
permissionSets.forEach((ps, psIndex) => {
|
|
6688
|
-
|
|
7337
|
+
asArray36(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
6689
7338
|
for (const clause of ["using", "check"]) {
|
|
6690
7339
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
6691
7340
|
findings.push({
|
|
@@ -6699,7 +7348,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6699
7348
|
}
|
|
6700
7349
|
});
|
|
6701
7350
|
});
|
|
6702
|
-
|
|
7351
|
+
asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
6703
7352
|
const slots = [
|
|
6704
7353
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
6705
7354
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -6717,9 +7366,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6717
7366
|
}
|
|
6718
7367
|
});
|
|
6719
7368
|
const tenancyDisabledObjects = new Set(
|
|
6720
|
-
|
|
7369
|
+
asArray36(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
6721
7370
|
);
|
|
6722
|
-
|
|
7371
|
+
asArray36(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
6723
7372
|
const target = str(rule.object);
|
|
6724
7373
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
6725
7374
|
const sharedWith = rule.sharedWith;
|
|
@@ -6742,7 +7391,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
6742
7391
|
import { compileCelToFilter } from "@objectstack/formula";
|
|
6743
7392
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
6744
7393
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
6745
|
-
function
|
|
7394
|
+
function asArray37(v) {
|
|
6746
7395
|
if (Array.isArray(v)) return v;
|
|
6747
7396
|
if (v && typeof v === "object") {
|
|
6748
7397
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6769,7 +7418,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
6769
7418
|
function validateSharingRuleEnforceability(stack) {
|
|
6770
7419
|
const findings = [];
|
|
6771
7420
|
const cfg = stack ?? {};
|
|
6772
|
-
|
|
7421
|
+
asArray37(cfg.sharingRules).forEach((rule, index) => {
|
|
6773
7422
|
const input = toCompilerInput(rule.condition);
|
|
6774
7423
|
if (input === null) return;
|
|
6775
7424
|
const result = compileCelToFilter(input, { variables: {} });
|
|
@@ -6805,10 +7454,16 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
6805
7454
|
}
|
|
6806
7455
|
|
|
6807
7456
|
// src/validate-rls-predicate-enforceability.ts
|
|
6808
|
-
import {
|
|
7457
|
+
import {
|
|
7458
|
+
isPushdownableCel,
|
|
7459
|
+
isSupportedRlsExpression,
|
|
7460
|
+
parseCelToAstWithReason as parseCelToAstWithReason2,
|
|
7461
|
+
sqlPredicateToCel
|
|
7462
|
+
} from "@objectstack/formula";
|
|
6809
7463
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
6810
7464
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
6811
|
-
|
|
7465
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
7466
|
+
function asArray38(v) {
|
|
6812
7467
|
if (Array.isArray(v)) return v;
|
|
6813
7468
|
if (v && typeof v === "object") {
|
|
6814
7469
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6819,6 +7474,13 @@ function str3(v) {
|
|
|
6819
7474
|
return typeof v === "string" ? v : "";
|
|
6820
7475
|
}
|
|
6821
7476
|
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.";
|
|
7477
|
+
function boundsOverrunOf(bridged) {
|
|
7478
|
+
const parsed = parseCelToAstWithReason2(bridged);
|
|
7479
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
7480
|
+
}
|
|
7481
|
+
function quote(source) {
|
|
7482
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
7483
|
+
}
|
|
6822
7484
|
function consequence(clause) {
|
|
6823
7485
|
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). ';
|
|
6824
7486
|
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.";
|
|
@@ -6826,20 +7488,36 @@ function consequence(clause) {
|
|
|
6826
7488
|
function validateRlsPredicateEnforceability(stack) {
|
|
6827
7489
|
const findings = [];
|
|
6828
7490
|
const cfg = stack ?? {};
|
|
6829
|
-
|
|
6830
|
-
|
|
7491
|
+
asArray38(cfg.permissions).forEach((ps, psIndex) => {
|
|
7492
|
+
asArray38(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
6831
7493
|
for (const clause of ["using", "check"]) {
|
|
6832
7494
|
const source = str3(policy[clause]);
|
|
6833
7495
|
if (!source.trim()) continue;
|
|
6834
7496
|
if (isSupportedRlsExpression(source)) continue;
|
|
6835
|
-
const
|
|
7497
|
+
const bridged = sqlPredicateToCel(source);
|
|
7498
|
+
const why = isPushdownableCel(bridged);
|
|
6836
7499
|
const detail = why.ok ? "" : why.detail;
|
|
6837
7500
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
7501
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
6838
7502
|
const psName = str3(ps.name) || String(psIndex);
|
|
6839
7503
|
const policyName = str3(policy.name) || String(pIndex);
|
|
6840
7504
|
const object = str3(policy.object);
|
|
6841
7505
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
6842
7506
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
7507
|
+
if (overrun) {
|
|
7508
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
7509
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
7510
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
7511
|
+
findings.push({
|
|
7512
|
+
severity: "error",
|
|
7513
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
7514
|
+
where,
|
|
7515
|
+
path,
|
|
7516
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
7517
|
+
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).`
|
|
7518
|
+
});
|
|
7519
|
+
continue;
|
|
7520
|
+
}
|
|
6843
7521
|
if (parseError) {
|
|
6844
7522
|
findings.push({
|
|
6845
7523
|
severity: "error",
|
|
@@ -6870,11 +7548,11 @@ import { createRequire as createRequire4 } from "module";
|
|
|
6870
7548
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
6871
7549
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
6872
7550
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
6873
|
-
var
|
|
6874
|
-
function
|
|
6875
|
-
if (Array.isArray(v)) return v.filter(
|
|
6876
|
-
if (
|
|
6877
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7551
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7552
|
+
function asArray39(v) {
|
|
7553
|
+
if (Array.isArray(v)) return v.filter(isRec20);
|
|
7554
|
+
if (isRec20(v)) {
|
|
7555
|
+
return Object.entries(v).filter(([, def]) => isRec20(def)).map(([name, def]) => ({ name, ...def }));
|
|
6878
7556
|
}
|
|
6879
7557
|
return [];
|
|
6880
7558
|
}
|
|
@@ -6891,7 +7569,7 @@ function loadAjv() {
|
|
|
6891
7569
|
`@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.`
|
|
6892
7570
|
);
|
|
6893
7571
|
}
|
|
6894
|
-
const ctor =
|
|
7572
|
+
const ctor = isRec20(mod) && "default" in mod ? mod.default : mod;
|
|
6895
7573
|
cachedAjv = ctor;
|
|
6896
7574
|
return ctor;
|
|
6897
7575
|
}
|
|
@@ -6906,7 +7584,7 @@ function loadAddFormats() {
|
|
|
6906
7584
|
`@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.`
|
|
6907
7585
|
);
|
|
6908
7586
|
}
|
|
6909
|
-
const plugin =
|
|
7587
|
+
const plugin = isRec20(mod) && "default" in mod ? mod.default : mod;
|
|
6910
7588
|
cachedAddFormats = plugin;
|
|
6911
7589
|
return plugin;
|
|
6912
7590
|
}
|
|
@@ -6936,17 +7614,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
6936
7614
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
6937
7615
|
for (const branch of ["then", "otherwise"]) {
|
|
6938
7616
|
const nested = rule[branch];
|
|
6939
|
-
if (
|
|
7617
|
+
if (isRec20(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
6940
7618
|
}
|
|
6941
7619
|
return out;
|
|
6942
7620
|
}
|
|
6943
7621
|
function walkObjectValidationRules(stack) {
|
|
6944
7622
|
const walked = [];
|
|
6945
|
-
if (!
|
|
6946
|
-
for (const obj of
|
|
7623
|
+
if (!isRec20(stack)) return walked;
|
|
7624
|
+
for (const obj of asArray39(stack.objects)) {
|
|
6947
7625
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6948
7626
|
const validations = obj.validations;
|
|
6949
|
-
for (const authored of
|
|
7627
|
+
for (const authored of asArray39(validations)) {
|
|
6950
7628
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
6951
7629
|
walked.push({
|
|
6952
7630
|
rule,
|
|
@@ -6977,7 +7655,7 @@ function validateRuleCompilability(stack) {
|
|
|
6977
7655
|
});
|
|
6978
7656
|
}
|
|
6979
7657
|
}
|
|
6980
|
-
if (rule.type === "json_schema" &&
|
|
7658
|
+
if (rule.type === "json_schema" && isRec20(rule.schema)) {
|
|
6981
7659
|
try {
|
|
6982
7660
|
createRuntimeAjv().compile(rule.schema);
|
|
6983
7661
|
} catch (err) {
|
|
@@ -6997,7 +7675,7 @@ function validateRuleCompilability(stack) {
|
|
|
6997
7675
|
|
|
6998
7676
|
// src/validate-rule-schema-formats.ts
|
|
6999
7677
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7000
|
-
var
|
|
7678
|
+
var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7001
7679
|
var SUBSCHEMA_KEYS = [
|
|
7002
7680
|
"additionalItems",
|
|
7003
7681
|
"additionalProperties",
|
|
@@ -7021,7 +7699,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7021
7699
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7022
7700
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7023
7701
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7024
|
-
if (!
|
|
7702
|
+
if (!isRec21(schema)) return;
|
|
7025
7703
|
if (typeof schema.format === "string") {
|
|
7026
7704
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7027
7705
|
}
|
|
@@ -7040,7 +7718,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7040
7718
|
}
|
|
7041
7719
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7042
7720
|
const value = schema[key];
|
|
7043
|
-
if (!
|
|
7721
|
+
if (!isRec21(value)) continue;
|
|
7044
7722
|
for (const [name, entry] of Object.entries(value)) {
|
|
7045
7723
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7046
7724
|
}
|
|
@@ -7048,13 +7726,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7048
7726
|
const items = schema.items;
|
|
7049
7727
|
if (Array.isArray(items)) {
|
|
7050
7728
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7051
|
-
} else if (
|
|
7729
|
+
} else if (isRec21(items)) {
|
|
7052
7730
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7053
7731
|
}
|
|
7054
7732
|
const dependencies = schema.dependencies;
|
|
7055
|
-
if (
|
|
7733
|
+
if (isRec21(dependencies)) {
|
|
7056
7734
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7057
|
-
if (!
|
|
7735
|
+
if (!isRec21(entry)) continue;
|
|
7058
7736
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7059
7737
|
}
|
|
7060
7738
|
}
|
|
@@ -7093,7 +7771,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7093
7771
|
const findings = [];
|
|
7094
7772
|
const pending = [];
|
|
7095
7773
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7096
|
-
if (rule.type !== "json_schema" || !
|
|
7774
|
+
if (rule.type !== "json_schema" || !isRec21(rule.schema)) continue;
|
|
7097
7775
|
const uses = [];
|
|
7098
7776
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7099
7777
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7119,14 +7797,14 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7119
7797
|
|
|
7120
7798
|
// src/validate-action-locations.ts
|
|
7121
7799
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
7122
|
-
function
|
|
7800
|
+
function asArray40(v) {
|
|
7123
7801
|
if (Array.isArray(v)) return v;
|
|
7124
7802
|
if (v && typeof v === "object") {
|
|
7125
7803
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7126
7804
|
}
|
|
7127
7805
|
return [];
|
|
7128
7806
|
}
|
|
7129
|
-
function
|
|
7807
|
+
function strName19(v) {
|
|
7130
7808
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7131
7809
|
}
|
|
7132
7810
|
function strList3(v) {
|
|
@@ -7140,8 +7818,8 @@ function collectNamePlacedActions(stack) {
|
|
|
7140
7818
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
7141
7819
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7142
7820
|
}
|
|
7143
|
-
for (const def of
|
|
7144
|
-
const n =
|
|
7821
|
+
for (const def of asArray40(list3.bulkActionDefs)) {
|
|
7822
|
+
const n = strName19(def?.name);
|
|
7145
7823
|
if (n) placed.add(n);
|
|
7146
7824
|
}
|
|
7147
7825
|
};
|
|
@@ -7149,12 +7827,12 @@ function collectNamePlacedActions(stack) {
|
|
|
7149
7827
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
7150
7828
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
7151
7829
|
};
|
|
7152
|
-
for (const view of
|
|
7830
|
+
for (const view of asArray40(stack.views)) {
|
|
7153
7831
|
if (!view || typeof view !== "object") continue;
|
|
7154
7832
|
harvest(view.list);
|
|
7155
7833
|
harvestListViews(view.listViews);
|
|
7156
7834
|
}
|
|
7157
|
-
for (const obj of
|
|
7835
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7158
7836
|
if (!obj || typeof obj !== "object") continue;
|
|
7159
7837
|
harvestListViews(obj.listViews);
|
|
7160
7838
|
}
|
|
@@ -7167,7 +7845,7 @@ function validateActionLocations(stack) {
|
|
|
7167
7845
|
const check = (action, path) => {
|
|
7168
7846
|
if (!action || typeof action !== "object") return;
|
|
7169
7847
|
if ("locations" in action) return;
|
|
7170
|
-
const name =
|
|
7848
|
+
const name = strName19(action.name);
|
|
7171
7849
|
if (!name) return;
|
|
7172
7850
|
if (namePlaced.has(name)) return;
|
|
7173
7851
|
findings.push({
|
|
@@ -7176,16 +7854,16 @@ function validateActionLocations(stack) {
|
|
|
7176
7854
|
where: `action "${name}"`,
|
|
7177
7855
|
path,
|
|
7178
7856
|
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.`,
|
|
7179
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
7857
|
+
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."
|
|
7180
7858
|
});
|
|
7181
7859
|
};
|
|
7182
|
-
const actions =
|
|
7860
|
+
const actions = asArray40(stack.actions);
|
|
7183
7861
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
7184
|
-
const objects =
|
|
7862
|
+
const objects = asArray40(stack.objects);
|
|
7185
7863
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7186
7864
|
const obj = objects[oi];
|
|
7187
7865
|
if (!obj || typeof obj !== "object") continue;
|
|
7188
|
-
const own =
|
|
7866
|
+
const own = asArray40(obj.actions);
|
|
7189
7867
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
7190
7868
|
}
|
|
7191
7869
|
return findings;
|
|
@@ -7197,7 +7875,8 @@ import {
|
|
|
7197
7875
|
APPROVAL_REVISE_NODE_TYPE,
|
|
7198
7876
|
collectFlowGraphs as collectFlowGraphs2
|
|
7199
7877
|
} from "@objectstack/spec/automation";
|
|
7200
|
-
|
|
7878
|
+
import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
|
|
7879
|
+
function asArray41(v) {
|
|
7201
7880
|
if (Array.isArray(v)) return v;
|
|
7202
7881
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7203
7882
|
return [];
|
|
@@ -7471,7 +8150,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7471
8150
|
function filterCarriesNoCondition(filter) {
|
|
7472
8151
|
if (filter === void 0 || filter === null) return true;
|
|
7473
8152
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7474
|
-
return
|
|
8153
|
+
return reduceFilterVerdict2(filter) === "true";
|
|
8154
|
+
}
|
|
8155
|
+
function describeUnboundedFilter(filter) {
|
|
8156
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8157
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8158
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8159
|
+
}
|
|
8160
|
+
function previewFilter(filter) {
|
|
8161
|
+
try {
|
|
8162
|
+
const json = JSON.stringify(filter);
|
|
8163
|
+
if (typeof json !== "string") return typeof filter;
|
|
8164
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8165
|
+
} catch {
|
|
8166
|
+
return typeof filter;
|
|
8167
|
+
}
|
|
7475
8168
|
}
|
|
7476
8169
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7477
8170
|
for (const node of nodes) {
|
|
@@ -7482,10 +8175,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7482
8175
|
if (cfg.multi !== true) continue;
|
|
7483
8176
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7484
8177
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7485
|
-
const filterState = cfg.filter
|
|
8178
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7486
8179
|
findings.push({
|
|
7487
8180
|
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
|
|
8181
|
+
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
8182
|
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
8183
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7491
8184
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7567,7 +8260,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
7567
8260
|
}
|
|
7568
8261
|
function lintFlowPatterns(stack) {
|
|
7569
8262
|
const findings = [];
|
|
7570
|
-
for (const flow of
|
|
8263
|
+
for (const flow of asArray41(stack.flows)) {
|
|
7571
8264
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
7572
8265
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
7573
8266
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -7664,7 +8357,7 @@ import { dirname, join } from "path";
|
|
|
7664
8357
|
import { existsSync, readFileSync } from "fs";
|
|
7665
8358
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7666
8359
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7667
|
-
function
|
|
8360
|
+
function asArray42(v) {
|
|
7668
8361
|
if (Array.isArray(v)) return v;
|
|
7669
8362
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7670
8363
|
return [];
|
|
@@ -7786,6 +8479,13 @@ var TYPE_COLLECTIONS = [
|
|
|
7786
8479
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
7787
8480
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
7788
8481
|
// silent, which is the shape this lint exists to prevent.
|
|
8482
|
+
//
|
|
8483
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
8484
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
8485
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
8486
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
8487
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
8488
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
7789
8489
|
{ type: "dashboard", key: "dashboards" }
|
|
7790
8490
|
];
|
|
7791
8491
|
function lintLivenessProperties(stack) {
|
|
@@ -7794,11 +8494,11 @@ function lintLivenessProperties(stack) {
|
|
|
7794
8494
|
const findings = [];
|
|
7795
8495
|
const objectWarn = loadWarnMap(dir, "object");
|
|
7796
8496
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
7797
|
-
for (const obj of
|
|
8497
|
+
for (const obj of asArray42(stack.objects)) {
|
|
7798
8498
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7799
8499
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
7800
8500
|
if (fieldWarn.size > 0) {
|
|
7801
|
-
for (const field of
|
|
8501
|
+
for (const field of asArray42(obj.fields)) {
|
|
7802
8502
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
7803
8503
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
7804
8504
|
}
|
|
@@ -7807,7 +8507,7 @@ function lintLivenessProperties(stack) {
|
|
|
7807
8507
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7808
8508
|
const warnMap = loadWarnMap(dir, type);
|
|
7809
8509
|
if (warnMap.size === 0) continue;
|
|
7810
|
-
for (const item of
|
|
8510
|
+
for (const item of asArray42(stack[key])) {
|
|
7811
8511
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7812
8512
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7813
8513
|
}
|
|
@@ -7821,7 +8521,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
|
7821
8521
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7822
8522
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7823
8523
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7824
|
-
function
|
|
8524
|
+
function asArray43(v) {
|
|
7825
8525
|
if (Array.isArray(v)) return v;
|
|
7826
8526
|
if (v && typeof v === "object") {
|
|
7827
8527
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7830,9 +8530,9 @@ function asArray44(v) {
|
|
|
7830
8530
|
}
|
|
7831
8531
|
function lintAutonumberFormats(stack) {
|
|
7832
8532
|
const findings = [];
|
|
7833
|
-
for (const obj of
|
|
8533
|
+
for (const obj of asArray43(stack.objects)) {
|
|
7834
8534
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7835
|
-
const fields =
|
|
8535
|
+
const fields = asArray43(obj.fields);
|
|
7836
8536
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7837
8537
|
for (const f of fields) {
|
|
7838
8538
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -7898,7 +8598,7 @@ function lintAutonumberFormats(stack) {
|
|
|
7898
8598
|
|
|
7899
8599
|
// src/lint-view-refs.ts
|
|
7900
8600
|
import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
|
|
7901
|
-
function
|
|
8601
|
+
function asArray44(v) {
|
|
7902
8602
|
if (Array.isArray(v)) return v;
|
|
7903
8603
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7904
8604
|
return [];
|
|
@@ -7926,7 +8626,7 @@ function lintViewRefs(stack) {
|
|
|
7926
8626
|
s.add(kind);
|
|
7927
8627
|
};
|
|
7928
8628
|
const containers = [];
|
|
7929
|
-
for (const v of
|
|
8629
|
+
for (const v of asArray44(stack.views)) {
|
|
7930
8630
|
if (v.viewKind) {
|
|
7931
8631
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7932
8632
|
continue;
|
|
@@ -7935,7 +8635,7 @@ function lintViewRefs(stack) {
|
|
|
7935
8635
|
const object = viewContainerObjectName(v);
|
|
7936
8636
|
if (object) containers.push({ object, container: v });
|
|
7937
8637
|
}
|
|
7938
|
-
for (const obj of
|
|
8638
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7939
8639
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7940
8640
|
if (!object) continue;
|
|
7941
8641
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -7989,11 +8689,11 @@ function lintViewRefs(stack) {
|
|
|
7989
8689
|
});
|
|
7990
8690
|
}
|
|
7991
8691
|
};
|
|
7992
|
-
for (const obj of
|
|
8692
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7993
8693
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7994
|
-
for (const action of
|
|
8694
|
+
for (const action of asArray44(obj.actions)) checkAction(action, object);
|
|
7995
8695
|
}
|
|
7996
|
-
for (const action of
|
|
8696
|
+
for (const action of asArray44(stack.actions)) checkAction(action);
|
|
7997
8697
|
return findings;
|
|
7998
8698
|
}
|
|
7999
8699
|
|
|
@@ -8128,6 +8828,7 @@ var CLI_ONLY = ["cli"];
|
|
|
8128
8828
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8129
8829
|
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.";
|
|
8130
8830
|
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.";
|
|
8831
|
+
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.";
|
|
8131
8832
|
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.";
|
|
8132
8833
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8133
8834
|
var AUTHORING_RULES = [
|
|
@@ -8157,8 +8858,12 @@ var AUTHORING_RULES = [
|
|
|
8157
8858
|
}))
|
|
8158
8859
|
},
|
|
8159
8860
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8160
|
-
// mode)
|
|
8161
|
-
//
|
|
8861
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
8862
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
8863
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
8864
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
8865
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
8866
|
+
// parse would have eaten".
|
|
8162
8867
|
{
|
|
8163
8868
|
name: "validateListViewMode",
|
|
8164
8869
|
tier: "gating",
|
|
@@ -8189,9 +8894,13 @@ var AUTHORING_RULES = [
|
|
|
8189
8894
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8190
8895
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8191
8896
|
},
|
|
8192
|
-
// A
|
|
8193
|
-
//
|
|
8194
|
-
//
|
|
8897
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
8898
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
8899
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
8900
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
8901
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
8902
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
8903
|
+
// declared and which survives the parse untouched.
|
|
8195
8904
|
{
|
|
8196
8905
|
name: "validateViewContainers",
|
|
8197
8906
|
tier: "gating",
|
|
@@ -8241,6 +8950,31 @@ var AUTHORING_RULES = [
|
|
|
8241
8950
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8242
8951
|
run: (stack) => validateFilterTokens(stack)
|
|
8243
8952
|
},
|
|
8953
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
8954
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
8955
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
8956
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
8957
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
8958
|
+
// same-direction precedent one shape over.
|
|
8959
|
+
{
|
|
8960
|
+
name: "validateEmptyCombinators",
|
|
8961
|
+
tier: "gating",
|
|
8962
|
+
input: "parsed",
|
|
8963
|
+
commands: ALL,
|
|
8964
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
8965
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
8966
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
8967
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
8968
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
8969
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
8970
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
8971
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
8972
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
8973
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
8974
|
+
surfaces: CLI_AND_RUNTIME,
|
|
8975
|
+
runtimeTypes: ["flow"],
|
|
8976
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
8977
|
+
},
|
|
8244
8978
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8245
8979
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8246
8980
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8289,6 +9023,16 @@ var AUTHORING_RULES = [
|
|
|
8289
9023
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8290
9024
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8291
9025
|
// separate step, once the warning-period inventory is empty.
|
|
9026
|
+
//
|
|
9027
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9028
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9029
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9030
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9031
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9032
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9033
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9034
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9035
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8292
9036
|
{
|
|
8293
9037
|
name: "validateComponentProps",
|
|
8294
9038
|
tier: "advisory",
|
|
@@ -8367,10 +9111,12 @@ var AUTHORING_RULES = [
|
|
|
8367
9111
|
//
|
|
8368
9112
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8369
9113
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8370
|
-
// dead?
|
|
9114
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8371
9115
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8372
|
-
// predicate cannot route at all,
|
|
8373
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9116
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9117
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9118
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9119
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8374
9120
|
// can be changed by installing a package, so there is no reading under which
|
|
8375
9121
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8376
9122
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8497,12 +9243,39 @@ var AUTHORING_RULES = [
|
|
|
8497
9243
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8498
9244
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8499
9245
|
},
|
|
8500
|
-
// ADR-0089 D3b —
|
|
8501
|
-
//
|
|
8502
|
-
//
|
|
9246
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
9247
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
9248
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
9249
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
9250
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
9251
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
9252
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
9253
|
+
//
|
|
9254
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
9255
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
9256
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
9257
|
+
// a change to this package's external input contract, and the maintainer's
|
|
9258
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
9259
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
9260
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
9261
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
9262
|
+
//
|
|
9263
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
9264
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
9265
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
9266
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
9267
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
9268
|
+
// "pre-parse evidence" one.
|
|
9269
|
+
//
|
|
9270
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
9271
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
9272
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
9273
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9274
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9275
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
8503
9276
|
{
|
|
8504
9277
|
name: "validateVisibilityPredicates",
|
|
8505
|
-
tier: "
|
|
9278
|
+
tier: "gating",
|
|
8506
9279
|
input: "normalized",
|
|
8507
9280
|
commands: ALL,
|
|
8508
9281
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
@@ -8510,6 +9283,30 @@ var AUTHORING_RULES = [
|
|
|
8510
9283
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8511
9284
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8512
9285
|
},
|
|
9286
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
9287
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
9288
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
9289
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
9290
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
9291
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
9292
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
9293
|
+
// #6254's 16 bare `type ==` predicates.
|
|
9294
|
+
//
|
|
9295
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
9296
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
9297
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9298
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9299
|
+
// false build errors. See the rule's module note.
|
|
9300
|
+
{
|
|
9301
|
+
name: "validatePredicatePathRefs",
|
|
9302
|
+
tier: "gating",
|
|
9303
|
+
input: "normalized",
|
|
9304
|
+
commands: ALL,
|
|
9305
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9306
|
+
surfaces: CLI_ONLY,
|
|
9307
|
+
surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
|
|
9308
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
9309
|
+
},
|
|
8513
9310
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8514
9311
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8515
9312
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|