@objectstack/lint 15.1.0 → 16.0.0-rc.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.js CHANGED
@@ -7,6 +7,18 @@ var CHART_FIELD_UNKNOWN = "chart-field-unknown";
7
7
  var CHART_CONFIG_MISSING = "chart-config-missing";
8
8
  var TABLE_COUNT_ONLY = "table-count-only";
9
9
  var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
10
+ var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
11
+ var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
12
+ var LEGACY_ANALYTICS_KEYS = [
13
+ "categoryField",
14
+ "valueField",
15
+ "xAxisField",
16
+ "yAxisFields",
17
+ "aggregate",
18
+ "aggregation",
19
+ "rowField",
20
+ "columnField"
21
+ ];
10
22
  function asArray(v) {
11
23
  if (Array.isArray(v)) return v;
12
24
  if (v && typeof v === "object") {
@@ -128,6 +140,29 @@ function validateWidgetBindings(stack) {
128
140
  if (f.severity === "warning" && suppressed(f.rule)) return;
129
141
  findings.push({ ...f, where, path });
130
142
  };
143
+ const legacyUsed = LEGACY_ANALYTICS_KEYS.filter((k) => w[k] !== void 0);
144
+ if (legacyUsed.length > 0) {
145
+ const optionsData = typeof w.options === "object" && w.options !== null && w.options.data !== void 0;
146
+ const hasDataSource = w.dataset !== void 0 || w.object !== void 0 || w.data !== void 0 || optionsData;
147
+ const keyList = legacyUsed.map((k) => `\`${k}\``).join(", ");
148
+ const plural = legacyUsed.length > 1;
149
+ const datasetHint = `Bind a semantic dataset and select fields BY NAME: \`dataset: '<name>', dimensions: [...], values: [...]\`. Dataset-bound widgets render through DatasetWidget (pivot rows/cols come from \`dimensions\`, cell values from \`values\`).`;
150
+ if (!hasDataSource) {
151
+ push({
152
+ severity: "error",
153
+ rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
154
+ message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} (removed by the ADR-0021 single-form cutover) and binds no data source (no \`dataset\`, \`object\`, or inline \`data\`) \u2014 it renders nothing.`,
155
+ hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
156
+ });
157
+ } else {
158
+ push({
159
+ severity: "warning",
160
+ rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
161
+ message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
162
+ hint: `${datasetHint} These inline keys are a no-op. Suppress with suppressWarnings: ['${WIDGET_LEGACY_ANALYTICS_SHAPE}'] if intentional.`
163
+ });
164
+ }
165
+ }
131
166
  const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
132
167
  const dataset = dsName ? datasets.get(dsName) : void 0;
133
168
  if (dsName && !dataset) {
@@ -248,17 +283,42 @@ function buildFieldIndex(objects) {
248
283
  }
249
284
  return idx;
250
285
  }
286
+ function buildFieldTypeIndex(objects) {
287
+ const idx = /* @__PURE__ */ new Map();
288
+ for (const obj of objects) {
289
+ const name = typeof obj.name === "string" ? obj.name : void 0;
290
+ if (!name) continue;
291
+ const fields = obj.fields;
292
+ const types = {};
293
+ if (Array.isArray(fields)) {
294
+ for (const f of fields) {
295
+ const fn = f?.name;
296
+ const ft = f?.type;
297
+ if (typeof fn === "string" && typeof ft === "string") types[fn] = ft;
298
+ }
299
+ } else if (fields && typeof fields === "object") {
300
+ for (const [fn, def] of Object.entries(fields)) {
301
+ const ft = def?.type;
302
+ if (typeof ft === "string") types[fn] = ft;
303
+ }
304
+ }
305
+ idx.set(name, types);
306
+ }
307
+ return idx;
308
+ }
251
309
  function validateStackExpressions(stack) {
252
310
  const issues = [];
253
311
  const objects = asArray2(stack.objects);
254
312
  const fieldIndex = buildFieldIndex(objects);
313
+ const fieldTypeIndex = buildFieldTypeIndex(objects);
255
314
  const check = (where, raw, objectName, scope = "flattened") => {
256
315
  if (raw == null) return;
257
316
  const fields = objectName ? fieldIndex.get(objectName) : void 0;
317
+ const fieldTypes = objectName ? fieldTypeIndex.get(objectName) : void 0;
258
318
  const res = validateExpression(
259
319
  "predicate",
260
320
  raw,
261
- objectName ? { objectName, fields, scope } : { scope }
321
+ objectName ? { objectName, fields, fieldTypes, scope } : { scope }
262
322
  );
263
323
  for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
264
324
  for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
@@ -306,19 +366,6 @@ function validateStackExpressions(stack) {
306
366
  }
307
367
  const fields = obj.fields;
308
368
  const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
309
- if (obj.external != null) {
310
- const fieldEntries = Array.isArray(fields) ? fields.map((f) => [f?.name ?? "?", f]) : fields && typeof fields === "object" ? Object.entries(fields) : [];
311
- for (const [fname, fdef] of fieldEntries) {
312
- if (fdef && typeof fdef === "object" && fdef.columnName != null) {
313
- issues.push({
314
- where: `object '${objectName}' \xB7 field '${fname}'`,
315
- message: `external object '${objectName}': field '${fname}' sets columnName='${String(fdef.columnName)}', which is not supported on federated objects (ADR-0062 D7). The driver's query pipeline ignores field.columnName for external objects; map remote columns via the datasource's external.columnMap instead.`,
316
- source: `columnName='${String(fdef.columnName)}'`,
317
- severity: "error"
318
- });
319
- }
320
- }
321
- }
322
369
  for (const f of fieldList) {
323
370
  if (f && typeof f === "object") {
324
371
  const fname = f.name ?? "?";
@@ -330,7 +377,7 @@ function validateStackExpressions(stack) {
330
377
  const res = validateExpression(
331
378
  "value",
332
379
  f.formula,
333
- objectName ? { objectName, fields: fieldIndex.get(objectName), scope: "record" } : { scope: "record" }
380
+ objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
334
381
  );
335
382
  const fieldWhere = `object '${objectName}' \xB7 field '${f.name ?? "?"}' formula`;
336
383
  for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
@@ -437,6 +484,117 @@ function validateListViewMode(stack) {
437
484
  return out;
438
485
  }
439
486
 
487
+ // src/validate-flow-trigger-readiness.ts
488
+ var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
489
+ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
490
+ function asArray4(v) {
491
+ if (Array.isArray(v)) return v;
492
+ if (v && typeof v === "object") {
493
+ return Object.entries(v).map(([name, def]) => ({
494
+ name,
495
+ ...def
496
+ }));
497
+ }
498
+ return [];
499
+ }
500
+ function startNodeOf(flow) {
501
+ const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
502
+ const index = nodes.findIndex((n) => n?.type === "start");
503
+ return index >= 0 ? { node: nodes[index], index } : void 0;
504
+ }
505
+ function validateFlowTriggerReadiness(stack) {
506
+ const findings = [];
507
+ const flows = asArray4(stack.flows);
508
+ if (flows.length === 0) return findings;
509
+ const objectNames = new Set(
510
+ asArray4(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
511
+ );
512
+ flows.forEach((flow, flowIndex) => {
513
+ const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
514
+ const start = startNodeOf(flow);
515
+ const config = start?.node.config ?? {};
516
+ const triggerType = typeof config.triggerType === "string" ? config.triggerType : void 0;
517
+ const isRecordTriggered = !!triggerType && triggerType.startsWith("record-");
518
+ const isTimeRelative = config.timeRelative != null && typeof config.timeRelative === "object";
519
+ const isAutoTriggered = isRecordTriggered || triggerType === "api" || config.schedule != null || isTimeRelative || flow.type === "schedule" || flow.type === "api";
520
+ if (isRecordTriggered && start) {
521
+ const objectName = typeof config.objectName === "string" ? config.objectName : void 0;
522
+ if (objectName && !objectNames.has(objectName) && !objectName.startsWith("sys_")) {
523
+ findings.push({
524
+ severity: "warning",
525
+ rule: FLOW_TRIGGER_UNKNOWN_OBJECT,
526
+ where: `flow "${flowName}" \u203A start node`,
527
+ path: `flows[${flowIndex}].nodes[${start.index}].config.objectName`,
528
+ message: `targets object '${objectName}', which this stack does not define \u2014 if the name is wrong, the flow will never fire (and the runtime stays silent about it).`,
529
+ hint: `Object names match exactly. Check config.objectName against the object's registered name (e.g. 'app_candidate', not 'candidate'). If the object comes from another installed package, this warning can be ignored.`
530
+ });
531
+ }
532
+ }
533
+ if (isTimeRelative && start) {
534
+ const tr = config.timeRelative;
535
+ const objectName = typeof tr.object === "string" ? tr.object : void 0;
536
+ if (objectName && !objectNames.has(objectName) && !objectName.startsWith("sys_")) {
537
+ findings.push({
538
+ severity: "warning",
539
+ rule: FLOW_TRIGGER_UNKNOWN_OBJECT,
540
+ where: `flow "${flowName}" \u203A start node`,
541
+ path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative.object`,
542
+ message: `sweeps object '${objectName}', which this stack does not define \u2014 if the name is wrong, the sweep will match nothing (and the runtime stays quiet about it).`,
543
+ hint: `Object names match exactly. Check config.timeRelative.object against the object's registered name. If the object comes from another installed package, this warning can be ignored.`
544
+ });
545
+ }
546
+ }
547
+ if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
548
+ findings.push({
549
+ severity: "warning",
550
+ rule: FLOW_DRAFT_STATUS_AMBIGUOUS,
551
+ where: `flow "${flowName}"`,
552
+ path: `flows[${flowIndex}].status`,
553
+ message: `has status 'draft' (the default when none is authored). Draft flows DO still fire their triggers (only 'obsolete'/'invalid' disable), so the intent is ambiguous.`,
554
+ hint: `Declare status: 'active' to arm it deliberately, or status: 'obsolete' to disable it.`
555
+ });
556
+ }
557
+ });
558
+ return findings;
559
+ }
560
+
561
+ // src/validate-view-containers.ts
562
+ var VIEW_CONTAINER_SHAPE = "view-container-shape";
563
+ var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
564
+ function asEntries(v) {
565
+ if (Array.isArray(v)) return v.map((value, i) => ({ key: `[${i}]`, value }));
566
+ if (v && typeof v === "object") {
567
+ return Object.entries(v).map(([name, value]) => ({ key: `.${name}`, value }));
568
+ }
569
+ return [];
570
+ }
571
+ function containerViewCount(rec) {
572
+ const named = (slot) => slot && typeof slot === "object" && !Array.isArray(slot) ? Object.keys(slot).length : 0;
573
+ return (rec.list ? 1 : 0) + (rec.form ? 1 : 0) + named(rec.listViews) + named(rec.formViews);
574
+ }
575
+ function validateViewContainers(stack) {
576
+ const out = [];
577
+ if (!stack || typeof stack !== "object") return out;
578
+ for (const { key, value } of asEntries(stack.views)) {
579
+ if (!value || typeof value !== "object" || Array.isArray(value)) continue;
580
+ const rec = value;
581
+ if (rec.viewKind != null) continue;
582
+ if (containerViewCount(rec) > 0) continue;
583
+ const label = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
584
+ const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
585
+ const looksFlat = !hasContainerSlot && ["type", "columns", "data", "filter", "sort"].some((k) => k in rec);
586
+ out.push({
587
+ severity: "error",
588
+ rule: VIEW_CONTAINER_SHAPE,
589
+ where: `views${key}${label}`,
590
+ path: `views${key}`,
591
+ message: looksFlat ? "Flat list-view object is not a view container: `ViewSchema` strips its keys, so it parses to an EMPTY container \u2014 zero views register and the Console renders no view for it." : "View container defines no views \u2014 all of `list` / `form` / `listViews` / `formViews` are absent or empty, so nothing registers.",
592
+ hint: "Wrap every view in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... }, formViews: { ... } }). See examples/app-showcase/src/ui/views/task.view.ts."
593
+ });
594
+ }
595
+ return out;
596
+ }
597
+
440
598
  // src/validate-responsive-styles.ts
441
599
  var STYLE_NODE_MISSING_ID = "style-node-missing-id";
442
600
  var STYLE_CLASSNAME_TAILWIND = "style-classname-tailwind";
@@ -640,7 +798,7 @@ function looksLikeTailwind(className) {
640
798
  return false;
641
799
  });
642
800
  }
643
- function asArray4(v) {
801
+ function asArray5(v) {
644
802
  if (Array.isArray(v)) return v;
645
803
  if (v && typeof v === "object") {
646
804
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -733,13 +891,13 @@ function checkNode(node, pageName, path, findings) {
733
891
  }
734
892
  function validateResponsiveStyles(stack) {
735
893
  const findings = [];
736
- const pages = asArray4(stack.pages);
894
+ const pages = asArray5(stack.pages);
737
895
  for (let p = 0; p < pages.length; p++) {
738
896
  const page = pages[p];
739
897
  const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
740
- const regions = asArray4(page.regions);
898
+ const regions = asArray5(page.regions);
741
899
  for (let r = 0; r < regions.length; r++) {
742
- const components = asArray4(regions[r].components);
900
+ const components = asArray5(regions[r].components);
743
901
  for (let c = 0; c < components.length; c++) {
744
902
  checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
745
903
  }
@@ -750,10 +908,10 @@ function validateResponsiveStyles(stack) {
750
908
 
751
909
  // src/validate-jsx-pages.ts
752
910
  import { parseJsx, compile } from "@objectstack/sdui-parser";
753
- var asArray5 = (v) => Array.isArray(v) ? v : [];
911
+ var asArray6 = (v) => Array.isArray(v) ? v : [];
754
912
  function validateJsxPages(stack, opts = {}) {
755
913
  const findings = [];
756
- const pages = asArray5(stack.pages);
914
+ const pages = asArray6(stack.pages);
757
915
  for (let p = 0; p < pages.length; p++) {
758
916
  const page = pages[p];
759
917
  if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
@@ -800,10 +958,10 @@ function loadSucraseTransform() {
800
958
  }
801
959
  return cachedTransform;
802
960
  }
803
- var asArray6 = (v) => Array.isArray(v) ? v : [];
961
+ var asArray7 = (v) => Array.isArray(v) ? v : [];
804
962
  function validateReactPages(stack) {
805
963
  const findings = [];
806
- const pages = asArray6(stack.pages);
964
+ const pages = asArray7(stack.pages);
807
965
  for (let p = 0; p < pages.length; p++) {
808
966
  const page = pages[p];
809
967
  if (!page || page.kind !== "react") continue;
@@ -854,7 +1012,7 @@ function loadTypeScript() {
854
1012
  }
855
1013
  return cachedTs;
856
1014
  }
857
- var asArray7 = (v) => Array.isArray(v) ? v : [];
1015
+ var asArray8 = (v) => Array.isArray(v) ? v : [];
858
1016
  var BLOCKS = new Map(
859
1017
  REACT_BLOCKS.map((b) => [
860
1018
  b.tag,
@@ -893,7 +1051,7 @@ function nearestKnown(prop, known) {
893
1051
  }
894
1052
  function validateReactPageProps(stack) {
895
1053
  const findings = [];
896
- const pages = asArray7(stack.pages);
1054
+ const pages = asArray8(stack.pages);
897
1055
  for (let p = 0; p < pages.length; p++) {
898
1056
  const page = pages[p];
899
1057
  if (!page || page.kind !== "react") continue;
@@ -961,11 +1119,11 @@ function validateReactPageProps(stack) {
961
1119
 
962
1120
  // src/validate-page-source-styling.ts
963
1121
  var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
964
- var asArray8 = (v) => Array.isArray(v) ? v : [];
1122
+ var asArray9 = (v) => Array.isArray(v) ? v : [];
965
1123
  var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
966
1124
  function validatePageSourceStyling(stack) {
967
1125
  const findings = [];
968
- const pages = asArray8(stack.pages);
1126
+ const pages = asArray9(stack.pages);
969
1127
  for (let p = 0; p < pages.length; p++) {
970
1128
  const page = pages[p];
971
1129
  if (!page) continue;
@@ -994,7 +1152,7 @@ function validatePageSourceStyling(stack) {
994
1152
  import { objectTitleCompleteness } from "@objectstack/spec/data";
995
1153
  var TITLE_FORMAT_RETIRED = "title-format-retired";
996
1154
  var TITLE_UNRESOLVABLE = "title-unresolvable";
997
- function asArray9(v) {
1155
+ function asArray10(v) {
998
1156
  if (Array.isArray(v)) return v;
999
1157
  if (v && typeof v === "object") {
1000
1158
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1003,7 +1161,7 @@ function asArray9(v) {
1003
1161
  }
1004
1162
  function validateRecordTitle(stack) {
1005
1163
  const findings = [];
1006
- const objects = asArray9(stack.objects);
1164
+ const objects = asArray10(stack.objects);
1007
1165
  for (let i = 0; i < objects.length; i++) {
1008
1166
  const obj = objects[i];
1009
1167
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
@@ -1039,7 +1197,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
1039
1197
  var FIELD_GROUP_EMPTY = "field-group-empty";
1040
1198
  var FIELD_GROUP_SHADOWED = "field-group-shadowed";
1041
1199
  var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
1042
- function asArray10(v) {
1200
+ function asArray11(v) {
1043
1201
  if (Array.isArray(v)) return v;
1044
1202
  if (v && typeof v === "object") {
1045
1203
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1048,7 +1206,7 @@ function asArray10(v) {
1048
1206
  }
1049
1207
  function validateSemanticRoles(stack) {
1050
1208
  const findings = [];
1051
- const objects = asArray10(stack.objects);
1209
+ const objects = asArray11(stack.objects);
1052
1210
  for (let i = 0; i < objects.length; i++) {
1053
1211
  const obj = objects[i];
1054
1212
  if (!obj || typeof obj !== "object") continue;
@@ -1143,7 +1301,7 @@ function validateSemanticRoles(stack) {
1143
1301
  // src/validate-form-layout.ts
1144
1302
  var FORM_FIELD_UNKNOWN = "form-field-unknown";
1145
1303
  var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
1146
- function asArray11(v) {
1304
+ function asArray12(v) {
1147
1305
  if (Array.isArray(v)) return v;
1148
1306
  if (v && typeof v === "object") {
1149
1307
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1168,13 +1326,13 @@ function boundObject(view) {
1168
1326
  function validateFormLayout(stack) {
1169
1327
  const findings = [];
1170
1328
  const objectFields = /* @__PURE__ */ new Map();
1171
- for (const obj of asArray11(stack.objects)) {
1329
+ for (const obj of asArray12(stack.objects)) {
1172
1330
  const name = typeof obj.name === "string" ? obj.name : void 0;
1173
1331
  if (!name) continue;
1174
1332
  const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
1175
1333
  objectFields.set(name, new Set(fields));
1176
1334
  }
1177
- const views = asArray11(stack.views);
1335
+ const views = asArray12(stack.views);
1178
1336
  for (let i = 0; i < views.length; i++) {
1179
1337
  const view = views[i];
1180
1338
  if (!view || typeof view !== "object") continue;
@@ -1224,7 +1382,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
1224
1382
  var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
1225
1383
  var CANONICAL = "visibleWhen";
1226
1384
  var ALIASES = ["visibleOn", "visibility"];
1227
- function asArray12(v) {
1385
+ function asArray13(v) {
1228
1386
  if (Array.isArray(v)) return v;
1229
1387
  if (v && typeof v === "object") {
1230
1388
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1286,7 +1444,7 @@ function isFieldObject(entry) {
1286
1444
  function validateVisibilityPredicates(stack, opts = {}) {
1287
1445
  const layer = opts.layer ?? "runtime";
1288
1446
  const findings = [];
1289
- const views = asArray12(stack.views);
1447
+ const views = asArray13(stack.views);
1290
1448
  for (let i = 0; i < views.length; i++) {
1291
1449
  const view = views[i];
1292
1450
  if (!view || typeof view !== "object") continue;
@@ -1309,7 +1467,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
1309
1467
  }
1310
1468
  }
1311
1469
  }
1312
- const pages = asArray12(stack.pages);
1470
+ const pages = asArray13(stack.pages);
1313
1471
  for (let i = 0; i < pages.length; i++) {
1314
1472
  const page = pages[i];
1315
1473
  if (!page || typeof page !== "object") continue;
@@ -1333,7 +1491,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
1333
1491
  // src/validate-capability-references.ts
1334
1492
  import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
1335
1493
  var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
1336
- function asArray13(v) {
1494
+ function asArray14(v) {
1337
1495
  if (Array.isArray(v)) return v;
1338
1496
  if (v && typeof v === "object") {
1339
1497
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1358,13 +1516,13 @@ function validateCapabilityReferences(stack) {
1358
1516
  const findings = [];
1359
1517
  if (!stack || typeof stack !== "object") return findings;
1360
1518
  const known = new Set(PLATFORM_CAPABILITY_NAMES);
1361
- for (const cap of asArray13(stack.capabilities)) {
1519
+ for (const cap of asArray14(stack.capabilities)) {
1362
1520
  if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
1363
1521
  }
1364
- for (const ps of asArray13(stack.permissions)) {
1522
+ for (const ps of asArray14(stack.permissions)) {
1365
1523
  for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
1366
1524
  }
1367
- for (const seed of asArray13(stack.data)) {
1525
+ for (const seed of asArray14(stack.data)) {
1368
1526
  if (seed.object !== "sys_capability") continue;
1369
1527
  for (const rec of Array.isArray(seed.records) ? seed.records : []) {
1370
1528
  const name = rec?.name;
@@ -1383,7 +1541,7 @@ function validateCapabilityReferences(stack) {
1383
1541
  hint
1384
1542
  });
1385
1543
  };
1386
- const objects = asArray13(stack.objects);
1544
+ const objects = asArray14(stack.objects);
1387
1545
  for (let i = 0; i < objects.length; i++) {
1388
1546
  const obj = objects[i];
1389
1547
  if (!obj || typeof obj !== "object") continue;
@@ -1392,27 +1550,27 @@ function validateCapabilityReferences(stack) {
1392
1550
  for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
1393
1551
  flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
1394
1552
  }
1395
- const fields = asArray13(obj.fields);
1553
+ const fields = asArray14(obj.fields);
1396
1554
  for (const f of fields) {
1397
1555
  const fname = typeof f.name === "string" ? f.name : "(field)";
1398
1556
  for (const cap of asCapArray(f.requiredPermissions)) {
1399
1557
  flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
1400
1558
  }
1401
1559
  }
1402
- for (const [ai, action] of asArray13(obj.actions).entries()) {
1560
+ for (const [ai, action] of asArray14(obj.actions).entries()) {
1403
1561
  const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
1404
1562
  for (const cap of asCapArray(action.requiredPermissions)) {
1405
1563
  flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
1406
1564
  }
1407
1565
  }
1408
1566
  }
1409
- for (const [i, action] of asArray13(stack.actions).entries()) {
1567
+ for (const [i, action] of asArray14(stack.actions).entries()) {
1410
1568
  const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
1411
1569
  for (const cap of asCapArray(action.requiredPermissions)) {
1412
1570
  flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
1413
1571
  }
1414
1572
  }
1415
- const apps = asArray13(stack.apps);
1573
+ const apps = asArray14(stack.apps);
1416
1574
  for (let i = 0; i < apps.length; i++) {
1417
1575
  const app = apps[i];
1418
1576
  if (!app || typeof app !== "object") continue;
@@ -1439,8 +1597,14 @@ function validateCapabilityReferences(stack) {
1439
1597
  }
1440
1598
 
1441
1599
  // src/validate-approval-approvers.ts
1442
- import { ApproverType, APPROVAL_NODE_TYPE } from "@objectstack/spec/automation";
1443
- var APPROVAL_ROLE_NOT_MEMBERSHIP_TIER = "approval-role-not-membership-tier";
1600
+ import {
1601
+ ApproverType,
1602
+ APPROVAL_NODE_TYPE,
1603
+ DEPRECATED_APPROVER_TYPES,
1604
+ canonicalApproverType
1605
+ } from "@objectstack/spec/automation";
1606
+ var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
1607
+ var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
1444
1608
  var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
1445
1609
  var APPROVAL_ESCALATION_REASSIGN_NO_TARGET = "approval-escalation-reassign-no-target";
1446
1610
  var MEMBERSHIP_TIERS = /* @__PURE__ */ new Set(["owner", "admin", "member", "guest"]);
@@ -1448,7 +1612,7 @@ var TYPE_FIX = {
1448
1612
  business_unit: "department",
1449
1613
  bu: "department"
1450
1614
  };
1451
- function asArray14(v) {
1615
+ function asArray15(v) {
1452
1616
  if (Array.isArray(v)) return v;
1453
1617
  if (v && typeof v === "object") {
1454
1618
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1458,7 +1622,7 @@ function asArray14(v) {
1458
1622
  function validateApprovalApprovers(stack) {
1459
1623
  const findings = [];
1460
1624
  if (!stack || typeof stack !== "object") return findings;
1461
- const flows = asArray14(stack.flows);
1625
+ const flows = asArray15(stack.flows);
1462
1626
  const validTypes = new Set(ApproverType.options);
1463
1627
  for (let fi = 0; fi < flows.length; fi++) {
1464
1628
  const flow = flows[fi];
@@ -1490,14 +1654,25 @@ function validateApprovalApprovers(stack) {
1490
1654
  });
1491
1655
  continue;
1492
1656
  }
1493
- if (type === "role" && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
1657
+ const canonical = canonicalApproverType(type);
1658
+ if (canonical === "org_membership_level" && value && !MEMBERSHIP_TIERS.has(value.toLowerCase())) {
1494
1659
  findings.push({
1495
1660
  severity: "warning",
1496
- rule: APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
1661
+ rule: APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
1497
1662
  where,
1498
1663
  path: `${path}.value`,
1499
- message: `approver { type: 'role', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: owner/admin/member) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
1500
- hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'role' only for membership tiers (owner/admin/member).`
1664
+ message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: owner/admin/member) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
1665
+ hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' only for membership tiers (owner/admin/member).`
1666
+ });
1667
+ } else if (type in DEPRECATED_APPROVER_TYPES) {
1668
+ const fix = canonicalApproverType(type);
1669
+ findings.push({
1670
+ severity: "warning",
1671
+ rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
1672
+ where,
1673
+ path: `${path}.type`,
1674
+ message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
1675
+ hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
1501
1676
  });
1502
1677
  }
1503
1678
  }
@@ -1546,7 +1721,7 @@ var OWD_WIDTH = {
1546
1721
  public_read: 1,
1547
1722
  public_read_write: 2
1548
1723
  };
1549
- function asArray15(v) {
1724
+ function asArray16(v) {
1550
1725
  if (Array.isArray(v)) return v;
1551
1726
  if (v && typeof v === "object") {
1552
1727
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1572,7 +1747,7 @@ function refOf(def) {
1572
1747
  return typeof r === "string" && r ? r : void 0;
1573
1748
  }
1574
1749
  function firstMasterDetailField(obj) {
1575
- for (const f of asArray15(obj.fields)) {
1750
+ for (const f of asArray16(obj.fields)) {
1576
1751
  if (f.type === "master_detail") {
1577
1752
  return { name: String(f.name ?? "?"), parent: refOf(f) };
1578
1753
  }
@@ -1585,8 +1760,8 @@ function grantsObjectAccess(p) {
1585
1760
  function validateSecurityPosture(stack, opts) {
1586
1761
  const findings = [];
1587
1762
  if (!stack || typeof stack !== "object") return findings;
1588
- const objects = asArray15(stack.objects);
1589
- const permissionSets = asArray15(stack.permissions);
1763
+ const objects = asArray16(stack.objects);
1764
+ const permissionSets = asArray16(stack.permissions);
1590
1765
  for (let i = 0; i < objects.length; i++) {
1591
1766
  const obj = objects[i];
1592
1767
  if (!obj || typeof obj !== "object") continue;
@@ -1715,10 +1890,10 @@ function validateSecurityPosture(stack, opts) {
1715
1890
  if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
1716
1891
  const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
1717
1892
  flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
1718
- for (const f of asArray15(obj.fields)) {
1893
+ for (const f of asArray16(obj.fields)) {
1719
1894
  flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
1720
1895
  }
1721
- for (const [ai, action] of asArray15(obj.actions).entries()) {
1896
+ for (const [ai, action] of asArray16(obj.actions).entries()) {
1722
1897
  flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
1723
1898
  }
1724
1899
  }
@@ -1727,19 +1902,19 @@ function validateSecurityPosture(stack, opts) {
1727
1902
  if (!ps || typeof ps !== "object") continue;
1728
1903
  flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
1729
1904
  }
1730
- for (const [i, pos] of asArray15(stack.positions).entries()) {
1905
+ for (const [i, pos] of asArray16(stack.positions).entries()) {
1731
1906
  flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
1732
1907
  }
1733
- for (const [i, app] of asArray15(stack.apps).entries()) {
1908
+ for (const [i, app] of asArray16(stack.apps).entries()) {
1734
1909
  flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
1735
1910
  }
1736
- for (const [i, book] of asArray15(stack.books).entries()) {
1911
+ for (const [i, book] of asArray16(stack.books).entries()) {
1737
1912
  flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
1738
1913
  }
1739
1914
  const stackSetNames = new Set(
1740
1915
  permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
1741
1916
  );
1742
- for (const [i, book] of asArray15(stack.books).entries()) {
1917
+ for (const [i, book] of asArray16(stack.books).entries()) {
1743
1918
  const audience = book.audience;
1744
1919
  if (!audience || typeof audience !== "object") continue;
1745
1920
  const setName = audience.permissionSet;
@@ -1817,7 +1992,7 @@ function validateSecurityPosture(stack, opts) {
1817
1992
  }
1818
1993
  const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
1819
1994
  const nowMs = opts?.nowMs ?? Date.now();
1820
- for (const [i, seed] of asArray15(stack.data).entries()) {
1995
+ for (const [i, seed] of asArray16(stack.data).entries()) {
1821
1996
  const seedObject = typeof seed.object === "string" ? seed.object : "";
1822
1997
  if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
1823
1998
  const records = Array.isArray(seed.records) ? seed.records : [];
@@ -1858,7 +2033,7 @@ function validateSecurityPosture(stack, opts) {
1858
2033
  }
1859
2034
 
1860
2035
  // src/build-access-matrix.ts
1861
- function asArray16(v) {
2036
+ function asArray17(v) {
1862
2037
  if (Array.isArray(v)) return v;
1863
2038
  if (v && typeof v === "object") {
1864
2039
  return Object.entries(v).map(([name, def]) => ({ name, ...def }));
@@ -1869,13 +2044,13 @@ function buildAccessMatrix(stack) {
1869
2044
  const entries = [];
1870
2045
  if (!stack || typeof stack !== "object") return { version: 1, entries };
1871
2046
  const owdByObject = /* @__PURE__ */ new Map();
1872
- for (const obj of asArray16(stack.objects)) {
2047
+ for (const obj of asArray17(stack.objects)) {
1873
2048
  const name = typeof obj.name === "string" ? obj.name : "";
1874
2049
  if (!name) continue;
1875
2050
  const owd = obj.sharingModel ?? obj.security?.sharingModel;
1876
2051
  if (typeof owd === "string") owdByObject.set(name, owd);
1877
2052
  }
1878
- for (const ps of asArray16(stack.permissions)) {
2053
+ for (const ps of asArray17(stack.permissions)) {
1879
2054
  const psName = typeof ps.name === "string" ? ps.name : "";
1880
2055
  if (!psName) continue;
1881
2056
  const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
@@ -1946,14 +2121,17 @@ function diffAccessMatrix(before, after) {
1946
2121
  return lines;
1947
2122
  }
1948
2123
  export {
2124
+ APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
2125
+ APPROVAL_APPROVER_TYPE_DEPRECATED,
1949
2126
  APPROVAL_APPROVER_TYPE_UNKNOWN,
1950
2127
  APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
1951
- APPROVAL_ROLE_NOT_MEMBERSHIP_TIER,
1952
2128
  CAPABILITY_REFERENCE_UNKNOWN,
1953
2129
  CHART_CONFIG_MISSING,
1954
2130
  CHART_FIELD_UNKNOWN,
1955
2131
  FIELD_GROUP_EMPTY,
1956
2132
  FIELD_GROUP_UNDECLARED,
2133
+ FLOW_DRAFT_STATUS_AMBIGUOUS,
2134
+ FLOW_TRIGGER_UNKNOWN_OBJECT,
1957
2135
  FORM_COLSPAN_ABSOLUTE,
1958
2136
  FORM_FIELD_UNKNOWN,
1959
2137
  LIST_VIEW_FILTERS_IN_VIEWS_MODE,
@@ -1979,6 +2157,7 @@ export {
1979
2157
  TABLE_COUNT_ONLY,
1980
2158
  TITLE_FORMAT_RETIRED,
1981
2159
  TITLE_UNRESOLVABLE,
2160
+ VIEW_CONTAINER_SHAPE,
1982
2161
  VISIBILITY_ALIAS_DEPRECATED,
1983
2162
  VISIBILITY_ROOT_MISLAYERED,
1984
2163
  WIDGET_DATASET_UNKNOWN,
@@ -1988,6 +2167,7 @@ export {
1988
2167
  diffAccessMatrix,
1989
2168
  validateApprovalApprovers,
1990
2169
  validateCapabilityReferences,
2170
+ validateFlowTriggerReadiness,
1991
2171
  validateFormLayout,
1992
2172
  validateJsxPages,
1993
2173
  validateListViewMode,
@@ -1999,6 +2179,7 @@ export {
1999
2179
  validateSecurityPosture,
2000
2180
  validateSemanticRoles,
2001
2181
  validateStackExpressions,
2182
+ validateViewContainers,
2002
2183
  validateVisibilityPredicates,
2003
2184
  validateWidgetBindings
2004
2185
  };