@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.d.cts CHANGED
@@ -359,11 +359,16 @@ declare function validateFormLayout(stack: AnyRec$5): FormLayoutFinding[];
359
359
  *
360
360
  * - `visibility-alias-deprecated` — a `visibleOn` / `visibility` key in authored
361
361
  * source. Autofix intent: rename the key to `visibleWhen` (same value).
362
- * - `visibility-root-mislayered` — a runtime view/page visibility predicate
363
- * rooted at `data.` (the metadata-editing-form root, ADR-0089 §Context). Runtime
364
- * record surfaces bind `record` + `current_user` (pages also expose `page.<var>`),
365
- * so a `data.`-rooted predicate here is almost always a wrong-layer paste that
366
- * silently never matches.
362
+ * - `visibility-root-mislayered` — a visibility predicate whose binding root does
363
+ * not match its layer (ADR-0089 D3, §Context). The check is **bidirectional**:
364
+ * - **runtime** view/page surfaces (`*.view.ts` / `*.page.ts`) bind
365
+ * `record` + `current_user` (pages also expose `page.<var>`), so a `data.`-rooted
366
+ * predicate here is a wrong-layer paste that silently never matches; and
367
+ * - **metadata-editing** forms (`*.form.ts` — the row under edit) bind `data`, so
368
+ * a `record.`-rooted predicate there is the same bug in the other direction.
369
+ * The layer is supplied by the caller (`opts.layer`, default `'runtime'`): the
370
+ * app-lint path (`os validate` / `compile`) always lints runtime surfaces, while a
371
+ * file-aware caller linting a `*.form.ts` passes `layer: 'metadata'`.
367
372
  *
368
373
  * Scope: `views` (form `sections` / legacy `groups`, and their `fields`) and
369
374
  * `pages` (`regions[].components[]`). Data-field `visibleWhen` is already covered
@@ -372,6 +377,17 @@ declare function validateFormLayout(stack: AnyRec$5): FormLayoutFinding[];
372
377
  declare const VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
373
378
  declare const VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
374
379
  type VisibilitySeverity = 'error' | 'warning';
380
+ /**
381
+ * Which binding environment the linted surface belongs to (ADR-0089 §Context):
382
+ * - `runtime` — `*.view.ts` / `*.page.ts`; binds `record` + `current_user` (+ `page`).
383
+ * - `metadata` — `*.form.ts` metadata-editing forms; binds `data` (the row under edit).
384
+ */
385
+ type VisibilityLayer = 'runtime' | 'metadata';
386
+ /** Options for {@link validateVisibilityPredicates}. */
387
+ interface VisibilityOptions {
388
+ /** Binding layer of the surface being linted. Defaults to `'runtime'`. */
389
+ layer?: VisibilityLayer;
390
+ }
375
391
  interface VisibilityFinding {
376
392
  /** Always `warning` today — both rules are advisory (see module note). */
377
393
  severity: VisibilitySeverity;
@@ -394,8 +410,14 @@ type AnyRec$4 = Record<string, unknown>;
394
410
  * `visibleOn` / `visibility` aliases before the schema folds them into
395
411
  * `visibleWhen`. Returns findings (empty = clean); all advisory (`warning`) —
396
412
  * the caller must never fail the build on these alone.
413
+ *
414
+ * The binding-root check is layer-directional (ADR-0089 D3): pass
415
+ * `opts.layer = 'metadata'` when linting a `*.form.ts` metadata-editing form (so a
416
+ * `record.`-rooted predicate is flagged), or leave it at the `'runtime'` default for
417
+ * `*.view.ts` / `*.page.ts` surfaces (so a `data.`-rooted predicate is flagged). The
418
+ * alias-deprecated check is layer-agnostic.
397
419
  */
398
- declare function validateVisibilityPredicates(stack: AnyRec$4): VisibilityFinding[];
420
+ declare function validateVisibilityPredicates(stack: AnyRec$4, opts?: VisibilityOptions): VisibilityFinding[];
399
421
 
400
422
  declare const CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
401
423
  type CapabilityRefSeverity = 'error' | 'warning';
@@ -505,4 +527,4 @@ declare function buildAccessMatrix(stack: AnyRec): AccessMatrix;
505
527
  */
506
528
  declare function diffAccessMatrix(before: AccessMatrix, after: AccessMatrix): string[];
507
529
 
508
- export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type VisibilityFinding, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateVisibilityPredicates, validateWidgetBindings };
530
+ export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateVisibilityPredicates, validateWidgetBindings };
package/dist/index.d.ts CHANGED
@@ -359,11 +359,16 @@ declare function validateFormLayout(stack: AnyRec$5): FormLayoutFinding[];
359
359
  *
360
360
  * - `visibility-alias-deprecated` — a `visibleOn` / `visibility` key in authored
361
361
  * source. Autofix intent: rename the key to `visibleWhen` (same value).
362
- * - `visibility-root-mislayered` — a runtime view/page visibility predicate
363
- * rooted at `data.` (the metadata-editing-form root, ADR-0089 §Context). Runtime
364
- * record surfaces bind `record` + `current_user` (pages also expose `page.<var>`),
365
- * so a `data.`-rooted predicate here is almost always a wrong-layer paste that
366
- * silently never matches.
362
+ * - `visibility-root-mislayered` — a visibility predicate whose binding root does
363
+ * not match its layer (ADR-0089 D3, §Context). The check is **bidirectional**:
364
+ * - **runtime** view/page surfaces (`*.view.ts` / `*.page.ts`) bind
365
+ * `record` + `current_user` (pages also expose `page.<var>`), so a `data.`-rooted
366
+ * predicate here is a wrong-layer paste that silently never matches; and
367
+ * - **metadata-editing** forms (`*.form.ts` — the row under edit) bind `data`, so
368
+ * a `record.`-rooted predicate there is the same bug in the other direction.
369
+ * The layer is supplied by the caller (`opts.layer`, default `'runtime'`): the
370
+ * app-lint path (`os validate` / `compile`) always lints runtime surfaces, while a
371
+ * file-aware caller linting a `*.form.ts` passes `layer: 'metadata'`.
367
372
  *
368
373
  * Scope: `views` (form `sections` / legacy `groups`, and their `fields`) and
369
374
  * `pages` (`regions[].components[]`). Data-field `visibleWhen` is already covered
@@ -372,6 +377,17 @@ declare function validateFormLayout(stack: AnyRec$5): FormLayoutFinding[];
372
377
  declare const VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
373
378
  declare const VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
374
379
  type VisibilitySeverity = 'error' | 'warning';
380
+ /**
381
+ * Which binding environment the linted surface belongs to (ADR-0089 §Context):
382
+ * - `runtime` — `*.view.ts` / `*.page.ts`; binds `record` + `current_user` (+ `page`).
383
+ * - `metadata` — `*.form.ts` metadata-editing forms; binds `data` (the row under edit).
384
+ */
385
+ type VisibilityLayer = 'runtime' | 'metadata';
386
+ /** Options for {@link validateVisibilityPredicates}. */
387
+ interface VisibilityOptions {
388
+ /** Binding layer of the surface being linted. Defaults to `'runtime'`. */
389
+ layer?: VisibilityLayer;
390
+ }
375
391
  interface VisibilityFinding {
376
392
  /** Always `warning` today — both rules are advisory (see module note). */
377
393
  severity: VisibilitySeverity;
@@ -394,8 +410,14 @@ type AnyRec$4 = Record<string, unknown>;
394
410
  * `visibleOn` / `visibility` aliases before the schema folds them into
395
411
  * `visibleWhen`. Returns findings (empty = clean); all advisory (`warning`) —
396
412
  * the caller must never fail the build on these alone.
413
+ *
414
+ * The binding-root check is layer-directional (ADR-0089 D3): pass
415
+ * `opts.layer = 'metadata'` when linting a `*.form.ts` metadata-editing form (so a
416
+ * `record.`-rooted predicate is flagged), or leave it at the `'runtime'` default for
417
+ * `*.view.ts` / `*.page.ts` surfaces (so a `data.`-rooted predicate is flagged). The
418
+ * alias-deprecated check is layer-agnostic.
397
419
  */
398
- declare function validateVisibilityPredicates(stack: AnyRec$4): VisibilityFinding[];
420
+ declare function validateVisibilityPredicates(stack: AnyRec$4, opts?: VisibilityOptions): VisibilityFinding[];
399
421
 
400
422
  declare const CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
401
423
  type CapabilityRefSeverity = 'error' | 'warning';
@@ -505,4 +527,4 @@ declare function buildAccessMatrix(stack: AnyRec): AccessMatrix;
505
527
  */
506
528
  declare function diffAccessMatrix(before: AccessMatrix, after: AccessMatrix): string[];
507
529
 
508
- export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type VisibilityFinding, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateVisibilityPredicates, validateWidgetBindings };
530
+ export { APPROVAL_APPROVER_TYPE_UNKNOWN, APPROVAL_ESCALATION_REASSIGN_NO_TARGET, APPROVAL_ROLE_NOT_MEMBERSHIP_TIER, type ApprovalApproverFinding, type ApprovalApproverSeverity, CAPABILITY_REFERENCE_UNKNOWN, CHART_CONFIG_MISSING, CHART_FIELD_UNKNOWN, type CapabilityRefFinding, type CapabilityRefSeverity, type ExprIssue, FIELD_GROUP_EMPTY, FIELD_GROUP_UNDECLARED, FORM_COLSPAN_ABSOLUTE, FORM_FIELD_UNKNOWN, type FormLayoutFinding, type FormLayoutSeverity, type JsxPageFinding, type JsxPageSeverity, LIST_VIEW_FILTERS_IN_VIEWS_MODE, type ListViewModeFinding, type ListViewModeSeverity, MEASURE_AGGREGATE_INCOHERENT, PAGE_SOURCE_CLASSNAME, type ReactPageFinding, type ReactPageSeverity, type ReactPropFinding, type ReactPropSeverity, type RecordTitleFinding, type RecordTitleSeverity, SECURITY_ANCHOR_HIGH_PRIVILEGE, SECURITY_BOOK_AUDIENCE_UNKNOWN_SET, SECURITY_DELEGATION_MISSING_REASON, SECURITY_EXTERNAL_WIDER, SECURITY_GRANT_EXPIRED_AT_AUTHORING, SECURITY_MASTER_DETAIL_UNGRANTED, SECURITY_OWD_ALIAS, SECURITY_OWD_UNSET, SECURITY_PRIVATE_NO_READSCOPE, SECURITY_ROLE_WORD, SECURITY_WILDCARD_VAMA, SEMANTIC_ROLE_FIELD_UNKNOWN, STYLE_CLASSNAME_TAILWIND, STYLE_NODE_MISSING_ID, STYLE_RESPONSIVE_NO_BASE, STYLE_UNKNOWN_CSS_PROPERTY, STYLE_UNKNOWN_TOKEN, type SecurityFinding, type SecuritySeverity, type SemanticRoleFinding, type SemanticRoleSeverity, type SourceStyleFinding, type SourceStyleSeverity, type StyleFinding, type StyleSeverity, TABLE_COUNT_ONLY, TITLE_FORMAT_RETIRED, TITLE_UNRESOLVABLE, VISIBILITY_ALIAS_DEPRECATED, VISIBILITY_ROOT_MISLAYERED, type VisibilityFinding, type VisibilityLayer, type VisibilityOptions, type VisibilitySeverity, WIDGET_DATASET_UNKNOWN, WIDGET_DIMENSION_UNKNOWN, WIDGET_MEASURE_UNKNOWN, type WidgetBindingFinding, type WidgetBindingSeverity, buildAccessMatrix, diffAccessMatrix, validateApprovalApprovers, validateCapabilityReferences, validateFormLayout, validateJsxPages, validateListViewMode, validatePageSourceStyling, validateReactPageProps, validateReactPages, validateRecordTitle, validateResponsiveStyles, validateSecurityPosture, validateSemanticRoles, validateStackExpressions, validateVisibilityPredicates, validateWidgetBindings };
package/dist/index.js CHANGED
@@ -1037,6 +1037,7 @@ function validateRecordTitle(stack) {
1037
1037
  // src/validate-semantic-roles.ts
1038
1038
  var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1039
1039
  var FIELD_GROUP_EMPTY = "field-group-empty";
1040
+ var FIELD_GROUP_SHADOWED = "field-group-shadowed";
1040
1041
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1041
1042
  function asArray10(v) {
1042
1043
  if (Array.isArray(v)) return v;
@@ -1110,6 +1111,31 @@ function validateSemanticRoles(stack) {
1110
1111
  hint: `Fix the field name (highlightFields drives default columns, cards, previews and the detail highlight strip, in order).`
1111
1112
  });
1112
1113
  }
1114
+ const declaredStrings = highlights.filter(
1115
+ (h) => typeof h === "string" && h.length > 0
1116
+ );
1117
+ if (declaredStrings.length > 0 && declaredGroups.size > 0) {
1118
+ const declaredTitle = [obj.nameField, obj.primaryField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
1119
+ const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
1120
+ const stripSet = new Set(
1121
+ declaredStrings.filter((h) => h !== titleField).slice(0, 4)
1122
+ );
1123
+ const hiddenFromBody = new Set(stripSet);
1124
+ if (titleField) hiddenFromBody.add(titleField);
1125
+ for (const key of declaredGroups) {
1126
+ const members = Object.entries(fields).filter(([, f]) => f?.group === key && f?.hidden !== true).map(([fname]) => fname);
1127
+ if (members.length === 0) continue;
1128
+ if (!members.every((m) => hiddenFromBody.has(m))) continue;
1129
+ findings.push({
1130
+ severity: "warning",
1131
+ rule: FIELD_GROUP_SHADOWED,
1132
+ where,
1133
+ path: `${path}.fieldGroups`,
1134
+ 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`,
1135
+ 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.)`
1136
+ });
1137
+ }
1138
+ }
1113
1139
  }
1114
1140
  return findings;
1115
1141
  }
@@ -1212,10 +1238,22 @@ function predicateSource(v) {
1212
1238
  }
1213
1239
  return void 0;
1214
1240
  }
1215
- function usesDataRoot(source) {
1216
- return /(^|[^.\w$])data\.\w/.test(source);
1241
+ function usesRoot(source, root) {
1242
+ return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
1217
1243
  }
1218
- function checkElement(el, where, path, findings) {
1244
+ var MISLAYER_BY_LAYER = {
1245
+ runtime: {
1246
+ forbiddenRoot: "data",
1247
+ 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).",
1248
+ hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
1249
+ },
1250
+ metadata: {
1251
+ forbiddenRoot: "record",
1252
+ 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).",
1253
+ hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
1254
+ }
1255
+ };
1256
+ function checkElement(el, where, path, layer, findings) {
1219
1257
  for (const alias of ALIASES) {
1220
1258
  if (el[alias] !== void 0) {
1221
1259
  findings.push({
@@ -1230,21 +1268,23 @@ function checkElement(el, where, path, findings) {
1230
1268
  }
1231
1269
  const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
1232
1270
  const source = predicateSource(raw);
1233
- if (source && usesDataRoot(source)) {
1271
+ const rule = MISLAYER_BY_LAYER[layer];
1272
+ if (source && usesRoot(source, rule.forbiddenRoot)) {
1234
1273
  findings.push({
1235
1274
  severity: "warning",
1236
1275
  rule: VISIBILITY_ROOT_MISLAYERED,
1237
1276
  where,
1238
1277
  path,
1239
- 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).`,
1240
- hint: `Runtime record surfaces bind \`record\` + \`current_user\` (pages also expose \`page.<var>\`). Use e.g. \`record.status == 'open'\` instead of \`data.status == 'open'\`.`
1278
+ message: rule.message,
1279
+ hint: rule.hint
1241
1280
  });
1242
1281
  }
1243
1282
  }
1244
1283
  function isFieldObject(entry) {
1245
1284
  return !!entry && typeof entry === "object" && !Array.isArray(entry);
1246
1285
  }
1247
- function validateVisibilityPredicates(stack) {
1286
+ function validateVisibilityPredicates(stack, opts = {}) {
1287
+ const layer = opts.layer ?? "runtime";
1248
1288
  const findings = [];
1249
1289
  const views = asArray12(stack.views);
1250
1290
  for (let i = 0; i < views.length; i++) {
@@ -1258,12 +1298,12 @@ function validateVisibilityPredicates(stack) {
1258
1298
  const sec = sections[s];
1259
1299
  if (!sec || typeof sec !== "object") continue;
1260
1300
  const secPath = `views[${i}].${bucket}[${s}]`;
1261
- checkElement(sec, where, secPath, findings);
1301
+ checkElement(sec, where, secPath, layer, findings);
1262
1302
  const secFields = Array.isArray(sec.fields) ? sec.fields : [];
1263
1303
  for (let f = 0; f < secFields.length; f++) {
1264
1304
  const entry = secFields[f];
1265
1305
  if (isFieldObject(entry)) {
1266
- checkElement(entry, where, `${secPath}.fields[${f}]`, findings);
1306
+ checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
1267
1307
  }
1268
1308
  }
1269
1309
  }
@@ -1282,7 +1322,7 @@ function validateVisibilityPredicates(stack) {
1282
1322
  for (let c = 0; c < components.length; c++) {
1283
1323
  const comp = components[c];
1284
1324
  if (comp && typeof comp === "object") {
1285
- checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, findings);
1325
+ checkElement(comp, where, `pages[${i}].regions[${r}].components[${c}]`, layer, findings);
1286
1326
  }
1287
1327
  }
1288
1328
  }
@@ -1318,6 +1358,9 @@ function validateCapabilityReferences(stack) {
1318
1358
  const findings = [];
1319
1359
  if (!stack || typeof stack !== "object") return findings;
1320
1360
  const known = new Set(PLATFORM_CAPABILITY_NAMES);
1361
+ for (const cap of asArray13(stack.capabilities)) {
1362
+ if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
1363
+ }
1321
1364
  for (const ps of asArray13(stack.permissions)) {
1322
1365
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
1323
1366
  }
@@ -1328,7 +1371,7 @@ function validateCapabilityReferences(stack) {
1328
1371
  if (typeof name === "string" && name.length > 0) known.add(name);
1329
1372
  }
1330
1373
  }
1331
- 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).";
1374
+ 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).";
1332
1375
  const flag = (cap, where, path) => {
1333
1376
  if (known.has(cap)) return;
1334
1377
  findings.push({