@objectstack/lint 14.8.0 → 15.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +54 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +54 -11
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -1119,6 +1119,7 @@ function validateRecordTitle(stack) {
|
|
|
1119
1119
|
// src/validate-semantic-roles.ts
|
|
1120
1120
|
var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
1121
1121
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
1122
|
+
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
1122
1123
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
1123
1124
|
function asArray10(v) {
|
|
1124
1125
|
if (Array.isArray(v)) return v;
|
|
@@ -1192,6 +1193,31 @@ function validateSemanticRoles(stack) {
|
|
|
1192
1193
|
hint: `Fix the field name (highlightFields drives default columns, cards, previews and the detail highlight strip, in order).`
|
|
1193
1194
|
});
|
|
1194
1195
|
}
|
|
1196
|
+
const declaredStrings = highlights.filter(
|
|
1197
|
+
(h) => typeof h === "string" && h.length > 0
|
|
1198
|
+
);
|
|
1199
|
+
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
1200
|
+
const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
1201
|
+
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
1202
|
+
const stripSet = new Set(
|
|
1203
|
+
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
1204
|
+
);
|
|
1205
|
+
const hiddenFromBody = new Set(stripSet);
|
|
1206
|
+
if (titleField) hiddenFromBody.add(titleField);
|
|
1207
|
+
for (const key of declaredGroups) {
|
|
1208
|
+
const members = Object.entries(fields).filter(([, f]) => f?.group === key && f?.hidden !== true).map(([fname]) => fname);
|
|
1209
|
+
if (members.length === 0) continue;
|
|
1210
|
+
if (!members.every((m) => hiddenFromBody.has(m))) continue;
|
|
1211
|
+
findings.push({
|
|
1212
|
+
severity: "warning",
|
|
1213
|
+
rule: FIELD_GROUP_SHADOWED,
|
|
1214
|
+
where,
|
|
1215
|
+
path: `${path}.fieldGroups`,
|
|
1216
|
+
message: `${objName}: every field in group "${key}" (${members.join(", ")}) is hoisted into the detail highlight strip (or is the record title) \u2014 the group renders on forms but never on detail pages`,
|
|
1217
|
+
hint: `Keep at least one non-highlighted field in "${key}", or remove the group if the strip already covers it. (Detail pages show the first 4 highlightFields as the top strip and hide them from the body.)`
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1195
1221
|
}
|
|
1196
1222
|
return findings;
|
|
1197
1223
|
}
|
|
@@ -1294,10 +1320,22 @@ function predicateSource(v) {
|
|
|
1294
1320
|
}
|
|
1295
1321
|
return void 0;
|
|
1296
1322
|
}
|
|
1297
|
-
function
|
|
1298
|
-
return
|
|
1323
|
+
function usesRoot(source, root) {
|
|
1324
|
+
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
1299
1325
|
}
|
|
1300
|
-
|
|
1326
|
+
var MISLAYER_BY_LAYER = {
|
|
1327
|
+
runtime: {
|
|
1328
|
+
forbiddenRoot: "data",
|
|
1329
|
+
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing-form root (a `*.form.ts` row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
|
|
1330
|
+
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
1331
|
+
},
|
|
1332
|
+
metadata: {
|
|
1333
|
+
forbiddenRoot: "record",
|
|
1334
|
+
message: "visibility predicate is rooted at `record.` \u2014 that is the runtime record-surface root (a `*.view.ts` / `*.page.ts` live record), not a metadata-editing form. A `*.form.ts` predicate that binds `record.` never matches and the element renders unconditionally (ADR-0089).",
|
|
1335
|
+
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
function checkElement(el, where, path, layer, findings) {
|
|
1301
1339
|
for (const alias of ALIASES) {
|
|
1302
1340
|
if (el[alias] !== void 0) {
|
|
1303
1341
|
findings.push({
|
|
@@ -1312,21 +1350,23 @@ function checkElement(el, where, path, findings) {
|
|
|
1312
1350
|
}
|
|
1313
1351
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
1314
1352
|
const source = predicateSource(raw);
|
|
1315
|
-
|
|
1353
|
+
const rule = MISLAYER_BY_LAYER[layer];
|
|
1354
|
+
if (source && usesRoot(source, rule.forbiddenRoot)) {
|
|
1316
1355
|
findings.push({
|
|
1317
1356
|
severity: "warning",
|
|
1318
1357
|
rule: VISIBILITY_ROOT_MISLAYERED,
|
|
1319
1358
|
where,
|
|
1320
1359
|
path,
|
|
1321
|
-
message:
|
|
1322
|
-
hint:
|
|
1360
|
+
message: rule.message,
|
|
1361
|
+
hint: rule.hint
|
|
1323
1362
|
});
|
|
1324
1363
|
}
|
|
1325
1364
|
}
|
|
1326
1365
|
function isFieldObject(entry) {
|
|
1327
1366
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
1328
1367
|
}
|
|
1329
|
-
function validateVisibilityPredicates(stack) {
|
|
1368
|
+
function validateVisibilityPredicates(stack, opts = {}) {
|
|
1369
|
+
const layer = opts.layer ?? "runtime";
|
|
1330
1370
|
const findings = [];
|
|
1331
1371
|
const views = asArray12(stack.views);
|
|
1332
1372
|
for (let i = 0; i < views.length; i++) {
|
|
@@ -1340,12 +1380,12 @@ function validateVisibilityPredicates(stack) {
|
|
|
1340
1380
|
const sec = sections[s];
|
|
1341
1381
|
if (!sec || typeof sec !== "object") continue;
|
|
1342
1382
|
const secPath = `views[${i}].${bucket}[${s}]`;
|
|
1343
|
-
checkElement(sec, where, secPath, findings);
|
|
1383
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
1344
1384
|
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
1345
1385
|
for (let f = 0; f < secFields.length; f++) {
|
|
1346
1386
|
const entry = secFields[f];
|
|
1347
1387
|
if (isFieldObject(entry)) {
|
|
1348
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, findings);
|
|
1388
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
1349
1389
|
}
|
|
1350
1390
|
}
|
|
1351
1391
|
}
|
|
@@ -1364,7 +1404,7 @@ function validateVisibilityPredicates(stack) {
|
|
|
1364
1404
|
for (let c = 0; c < components.length; c++) {
|
|
1365
1405
|
const comp = components[c];
|
|
1366
1406
|
if (comp && typeof comp === "object") {
|
|
1367
|
-
checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, findings);
|
|
1407
|
+
checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
|
|
1368
1408
|
}
|
|
1369
1409
|
}
|
|
1370
1410
|
}
|
|
@@ -1400,6 +1440,9 @@ function validateCapabilityReferences(stack) {
|
|
|
1400
1440
|
const findings = [];
|
|
1401
1441
|
if (!stack || typeof stack !== "object") return findings;
|
|
1402
1442
|
const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
|
|
1443
|
+
for (const cap of asArray13(stack.capabilities)) {
|
|
1444
|
+
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
1445
|
+
}
|
|
1403
1446
|
for (const ps of asArray13(stack.permissions)) {
|
|
1404
1447
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
1405
1448
|
}
|
|
@@ -1410,7 +1453,7 @@ function validateCapabilityReferences(stack) {
|
|
|
1410
1453
|
if (typeof name === "string" && name.length > 0) known.add(name);
|
|
1411
1454
|
}
|
|
1412
1455
|
}
|
|
1413
|
-
const hint = "Fix the capability name, declare it on a permission set\u2019s systemPermissions, ship a sys_capability seed row, or ignore this if the capability is provided by another installed package (references fail closed at runtime).";
|
|
1456
|
+
const hint = "Fix the capability name, define it with defineCapability (stack.capabilities), declare it on a permission set\u2019s systemPermissions, ship a sys_capability seed row, or ignore this if the capability is provided by another installed package (references fail closed at runtime).";
|
|
1414
1457
|
const flag = (cap, where, path) => {
|
|
1415
1458
|
if (known.has(cap)) return;
|
|
1416
1459
|
findings.push({
|