@objectstack/lint 17.0.0-rc.1 → 17.0.0-rc.2
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 +688 -0
- package/dist/index.cjs +2597 -343
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +610 -108
- package/dist/index.d.ts +610 -108
- package/dist/index.js +2556 -340
- package/dist/index.js.map +1 -1
- package/dist/runtime-Cs64ShwN.d.cts +254 -0
- package/dist/runtime-Cs64ShwN.d.ts +254 -0
- package/dist/runtime.cjs +7740 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +1 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +7733 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +12 -6
package/dist/index.js
CHANGED
|
@@ -176,7 +176,7 @@ function validateWidgetBindings(stack) {
|
|
|
176
176
|
const where = `dashboard "${dashName}" \u203A widget "${widgetId}"`;
|
|
177
177
|
const path = `dashboards[${i}].widgets[${j}]`;
|
|
178
178
|
const suppressed = (rule) => Array.isArray(w.suppressWarnings) && w.suppressWarnings.includes(rule);
|
|
179
|
-
const
|
|
179
|
+
const push2 = (f) => {
|
|
180
180
|
if (f.severity === "warning" && suppressed(f.rule)) return;
|
|
181
181
|
findings.push({ ...f, where, path });
|
|
182
182
|
};
|
|
@@ -188,14 +188,14 @@ function validateWidgetBindings(stack) {
|
|
|
188
188
|
const plural = legacyUsed.length > 1;
|
|
189
189
|
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\`).`;
|
|
190
190
|
if (!hasDataSource) {
|
|
191
|
-
|
|
191
|
+
push2({
|
|
192
192
|
severity: "error",
|
|
193
193
|
rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
194
194
|
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.`,
|
|
195
195
|
hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
|
|
196
196
|
});
|
|
197
197
|
} else {
|
|
198
|
-
|
|
198
|
+
push2({
|
|
199
199
|
severity: "warning",
|
|
200
200
|
rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
201
201
|
message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
|
|
@@ -206,7 +206,7 @@ function validateWidgetBindings(stack) {
|
|
|
206
206
|
const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
|
|
207
207
|
const dataset = dsName ? datasets.get(dsName) : void 0;
|
|
208
208
|
if (dsName && !dataset) {
|
|
209
|
-
|
|
209
|
+
push2({
|
|
210
210
|
severity: "error",
|
|
211
211
|
rule: WIDGET_DATASET_UNKNOWN,
|
|
212
212
|
message: `dataset "${dsName}" does not resolve to a declared dataset.`,
|
|
@@ -214,7 +214,7 @@ function validateWidgetBindings(stack) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
if (!dsName) {
|
|
217
|
-
|
|
217
|
+
push2({
|
|
218
218
|
severity: "error",
|
|
219
219
|
rule: WIDGET_DATASET_UNKNOWN,
|
|
220
220
|
message: `binds no \`dataset\` \u2014 the ADR-0021 widget shape requires one, so this widget resolves no data and renders empty.`,
|
|
@@ -233,7 +233,7 @@ function validateWidgetBindings(stack) {
|
|
|
233
233
|
const field = eff.field;
|
|
234
234
|
if (field.includes(".")) continue;
|
|
235
235
|
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
|
|
236
|
-
|
|
236
|
+
push2({
|
|
237
237
|
severity: "error",
|
|
238
238
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
239
239
|
message: eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.` : `inherits dashboard filter \`${def.name}(${field})\`, but object \`${datasetObject}\` (dataset "${dsName}") has no field \`${field}\`.`,
|
|
@@ -253,7 +253,7 @@ function validateWidgetBindings(stack) {
|
|
|
253
253
|
const dims = asStrings(w.dimensions);
|
|
254
254
|
for (let k = 0; k < dims.length; k++) {
|
|
255
255
|
if (dimensionNames.has(dims[k])) continue;
|
|
256
|
-
|
|
256
|
+
push2({
|
|
257
257
|
severity: "error",
|
|
258
258
|
rule: WIDGET_DIMENSION_UNKNOWN,
|
|
259
259
|
message: `dimensions[${k}] "${dims[k]}" is not a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
|
|
@@ -263,7 +263,7 @@ function validateWidgetBindings(stack) {
|
|
|
263
263
|
const values = asStrings(w.values);
|
|
264
264
|
for (let k = 0; k < values.length; k++) {
|
|
265
265
|
if (measures.has(values[k])) continue;
|
|
266
|
-
|
|
266
|
+
push2({
|
|
267
267
|
severity: "error",
|
|
268
268
|
rule: WIDGET_MEASURE_UNKNOWN,
|
|
269
269
|
message: `values[${k}] "${values[k]}" is not a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
|
|
@@ -276,7 +276,7 @@ function validateWidgetBindings(stack) {
|
|
|
276
276
|
const selectedValues = new Set(values.filter((v) => measures.has(v)));
|
|
277
277
|
const xAxis = chartConfig.xAxis && typeof chartConfig.xAxis === "object" ? chartConfig.xAxis : void 0;
|
|
278
278
|
if (xAxis && typeof xAxis.field === "string" && !dimensionNames.has(xAxis.field) && !dims.includes(xAxis.field)) {
|
|
279
|
-
|
|
279
|
+
push2({
|
|
280
280
|
severity: "error",
|
|
281
281
|
rule: CHART_FIELD_UNKNOWN,
|
|
282
282
|
message: `chartConfig.xAxis.field "${xAxis.field}" does not resolve to a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
|
|
@@ -286,7 +286,7 @@ function validateWidgetBindings(stack) {
|
|
|
286
286
|
const measureField = (label2, field) => {
|
|
287
287
|
if (values.includes(field)) return;
|
|
288
288
|
const declaredButUnselected = measures.has(field);
|
|
289
|
-
|
|
289
|
+
push2({
|
|
290
290
|
severity: "error",
|
|
291
291
|
rule: CHART_FIELD_UNKNOWN,
|
|
292
292
|
message: declaredButUnselected ? `chartConfig.${label2} "${field}" is a measure of dataset "${dsName}" but is not selected in the widget's values (${list(values)}), so the query result will not contain it.` : `chartConfig.${label2} "${field}" does not resolve to a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
|
|
@@ -304,7 +304,7 @@ function validateWidgetBindings(stack) {
|
|
|
304
304
|
if (typeof name === "string") measureField(`series[${k}].name`, name);
|
|
305
305
|
}
|
|
306
306
|
} else if (isChartType) {
|
|
307
|
-
|
|
307
|
+
push2({
|
|
308
308
|
severity: "warning",
|
|
309
309
|
rule: CHART_CONFIG_MISSING,
|
|
310
310
|
message: `chart-type widget ('${w.type}') has no chartConfig \u2014 the renderer cannot determine which measure to plot, so the series renders empty.`,
|
|
@@ -318,7 +318,7 @@ function validateWidgetBindings(stack) {
|
|
|
318
318
|
if (resolved.some((m) => !m)) continue;
|
|
319
319
|
const countOnly = resolved.every((m) => m.aggregate === "count" && !m.derived);
|
|
320
320
|
if (!countOnly) continue;
|
|
321
|
-
|
|
321
|
+
push2({
|
|
322
322
|
severity: "warning",
|
|
323
323
|
rule: TABLE_COUNT_ONLY,
|
|
324
324
|
message: `a '${w.type}' widget bound to dataset "${dsName}" selects only count measure(s) (${values.join(", ")}) and no dimensions, so it renders a single summary row \u2014 not a per-record list.`,
|
|
@@ -332,6 +332,226 @@ function validateWidgetBindings(stack) {
|
|
|
332
332
|
// src/validate-expressions.ts
|
|
333
333
|
import { validateExpression } from "@objectstack/formula";
|
|
334
334
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
335
|
+
|
|
336
|
+
// src/validate-null-guards.ts
|
|
337
|
+
import { Environment } from "@marcbachmann/cel-js";
|
|
338
|
+
var NULL_GUARD_HINT = `Guard it with '!= null' \u2014 'has(x)' does NOT do that: a declared field holding null is still PRESENT, so has(x) is true.`;
|
|
339
|
+
var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
|
|
340
|
+
var DEFAULT_RECORD_ROOTS = ["record", "previous"];
|
|
341
|
+
var parseEnv;
|
|
342
|
+
function getParseEnv() {
|
|
343
|
+
if (!parseEnv) {
|
|
344
|
+
parseEnv = new Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
|
|
345
|
+
}
|
|
346
|
+
return parseEnv;
|
|
347
|
+
}
|
|
348
|
+
function isNode(v) {
|
|
349
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
350
|
+
}
|
|
351
|
+
function isNullLiteral(node) {
|
|
352
|
+
return isNode(node) && node.op === "value" && node.args === null;
|
|
353
|
+
}
|
|
354
|
+
function fieldOf(node, roots) {
|
|
355
|
+
if (!isNode(node)) return null;
|
|
356
|
+
if (node.op !== "." && node.op !== ".?") return null;
|
|
357
|
+
const args = node.args;
|
|
358
|
+
if (!Array.isArray(args) || args.length < 2) return null;
|
|
359
|
+
const [recv, seg] = args;
|
|
360
|
+
if (typeof seg !== "string") return null;
|
|
361
|
+
if (!isNode(recv) || recv.op !== "id") return null;
|
|
362
|
+
const root = recv.args;
|
|
363
|
+
if (typeof root !== "string" || !roots.includes(root)) return null;
|
|
364
|
+
return { operand: `${root}.${seg}`, field: seg };
|
|
365
|
+
}
|
|
366
|
+
function childNodes(node) {
|
|
367
|
+
const args = node.args;
|
|
368
|
+
if (isNode(args)) return [args];
|
|
369
|
+
if (!Array.isArray(args)) return [];
|
|
370
|
+
const out = [];
|
|
371
|
+
for (const a of args) {
|
|
372
|
+
if (isNode(a)) out.push(a);
|
|
373
|
+
else if (Array.isArray(a)) {
|
|
374
|
+
for (const b of a) if (isNode(b)) out.push(b);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
379
|
+
function callName(node) {
|
|
380
|
+
if (node.op !== "call") return null;
|
|
381
|
+
const args = node.args;
|
|
382
|
+
if (!Array.isArray(args) || typeof args[0] !== "string") return null;
|
|
383
|
+
return args[0];
|
|
384
|
+
}
|
|
385
|
+
function callArgs(node) {
|
|
386
|
+
const args = node.args;
|
|
387
|
+
if (!Array.isArray(args) || !Array.isArray(args[1])) return [];
|
|
388
|
+
return args[1];
|
|
389
|
+
}
|
|
390
|
+
function union(a, b) {
|
|
391
|
+
return /* @__PURE__ */ new Set([...a, ...b]);
|
|
392
|
+
}
|
|
393
|
+
function intersect(a, b) {
|
|
394
|
+
const out = /* @__PURE__ */ new Set();
|
|
395
|
+
for (const v of a) if (b.has(v)) out.add(v);
|
|
396
|
+
return out;
|
|
397
|
+
}
|
|
398
|
+
function truthGuards(node, roots) {
|
|
399
|
+
if (!isNode(node)) return /* @__PURE__ */ new Set();
|
|
400
|
+
switch (node.op) {
|
|
401
|
+
case "!=": {
|
|
402
|
+
const [l, r] = node.args ?? [];
|
|
403
|
+
if (isNullLiteral(r)) {
|
|
404
|
+
const f = fieldOf(l, roots);
|
|
405
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
406
|
+
}
|
|
407
|
+
if (isNullLiteral(l)) {
|
|
408
|
+
const f = fieldOf(r, roots);
|
|
409
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
410
|
+
}
|
|
411
|
+
return /* @__PURE__ */ new Set();
|
|
412
|
+
}
|
|
413
|
+
case "&&": {
|
|
414
|
+
const [l, r] = node.args;
|
|
415
|
+
return union(truthGuards(l, roots), truthGuards(r, roots));
|
|
416
|
+
}
|
|
417
|
+
case "||": {
|
|
418
|
+
const [l, r] = node.args;
|
|
419
|
+
return intersect(truthGuards(l, roots), truthGuards(r, roots));
|
|
420
|
+
}
|
|
421
|
+
case "!_":
|
|
422
|
+
return falseGuards(node.args, roots);
|
|
423
|
+
case "?:": {
|
|
424
|
+
const [, t, f] = node.args;
|
|
425
|
+
return intersect(truthGuards(t, roots), truthGuards(f, roots));
|
|
426
|
+
}
|
|
427
|
+
default:
|
|
428
|
+
return /* @__PURE__ */ new Set();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function falseGuards(node, roots) {
|
|
432
|
+
if (!isNode(node)) return /* @__PURE__ */ new Set();
|
|
433
|
+
switch (node.op) {
|
|
434
|
+
case "==": {
|
|
435
|
+
const [l, r] = node.args ?? [];
|
|
436
|
+
if (isNullLiteral(r)) {
|
|
437
|
+
const f = fieldOf(l, roots);
|
|
438
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
439
|
+
}
|
|
440
|
+
if (isNullLiteral(l)) {
|
|
441
|
+
const f = fieldOf(r, roots);
|
|
442
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
443
|
+
}
|
|
444
|
+
return /* @__PURE__ */ new Set();
|
|
445
|
+
}
|
|
446
|
+
case "||": {
|
|
447
|
+
const [l, r] = node.args;
|
|
448
|
+
return union(falseGuards(l, roots), falseGuards(r, roots));
|
|
449
|
+
}
|
|
450
|
+
case "&&": {
|
|
451
|
+
const [l, r] = node.args;
|
|
452
|
+
return intersect(falseGuards(l, roots), falseGuards(r, roots));
|
|
453
|
+
}
|
|
454
|
+
case "!_":
|
|
455
|
+
return truthGuards(node.args, roots);
|
|
456
|
+
case "call": {
|
|
457
|
+
if (callName(node) !== "isBlank") return /* @__PURE__ */ new Set();
|
|
458
|
+
const [only] = callArgs(node);
|
|
459
|
+
const f = fieldOf(only, roots);
|
|
460
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
461
|
+
}
|
|
462
|
+
default:
|
|
463
|
+
return /* @__PURE__ */ new Set();
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function collectHasOperands(node, roots, out) {
|
|
467
|
+
if (!isNode(node)) return;
|
|
468
|
+
if (callName(node) === "has") {
|
|
469
|
+
for (const a of callArgs(node)) {
|
|
470
|
+
const f = fieldOf(a, roots);
|
|
471
|
+
if (f) out.add(f.operand);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
for (const child of childNodes(node)) collectHasOperands(child, roots, out);
|
|
475
|
+
}
|
|
476
|
+
function findUnguardedNullableOperands(source, opts) {
|
|
477
|
+
if (typeof source !== "string" || !source.trim()) return [];
|
|
478
|
+
if (opts.nullableFields.size === 0) return [];
|
|
479
|
+
const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
|
|
480
|
+
let ast;
|
|
481
|
+
try {
|
|
482
|
+
ast = getParseEnv().parse(source).ast;
|
|
483
|
+
} catch {
|
|
484
|
+
return [];
|
|
485
|
+
}
|
|
486
|
+
const hasOperands = /* @__PURE__ */ new Set();
|
|
487
|
+
collectHasOperands(ast, roots, hasOperands);
|
|
488
|
+
const findings = [];
|
|
489
|
+
const seen = /* @__PURE__ */ new Set();
|
|
490
|
+
const report = (operandNode, operator, guards) => {
|
|
491
|
+
const f = fieldOf(operandNode, roots);
|
|
492
|
+
if (!f) return;
|
|
493
|
+
if (!opts.nullableFields.has(f.field)) return;
|
|
494
|
+
if (guards.has(f.operand)) return;
|
|
495
|
+
const key = `${f.operand}\0${operator}`;
|
|
496
|
+
if (seen.has(key)) return;
|
|
497
|
+
seen.add(key);
|
|
498
|
+
findings.push({
|
|
499
|
+
operand: f.operand,
|
|
500
|
+
field: f.field,
|
|
501
|
+
operator,
|
|
502
|
+
hasOnlyGuard: hasOperands.has(f.operand)
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
const visit = (node, guards) => {
|
|
506
|
+
if (!isNode(node)) return;
|
|
507
|
+
const op = node.op;
|
|
508
|
+
if (op === "&&") {
|
|
509
|
+
const [l, r] = node.args;
|
|
510
|
+
visit(l, guards);
|
|
511
|
+
visit(r, union(guards, truthGuards(l, roots)));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (op === "||") {
|
|
515
|
+
const [l, r] = node.args;
|
|
516
|
+
visit(l, guards);
|
|
517
|
+
visit(r, union(guards, falseGuards(l, roots)));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (op === "?:") {
|
|
521
|
+
const [c, t, f] = node.args;
|
|
522
|
+
visit(c, guards);
|
|
523
|
+
visit(t, union(guards, truthGuards(c, roots)));
|
|
524
|
+
visit(f, union(guards, falseGuards(c, roots)));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (op === "call" && callName(node) === "has") {
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (FAULTING_BINARY_OPS.has(op)) {
|
|
531
|
+
const [l, r] = node.args;
|
|
532
|
+
report(l, op, guards);
|
|
533
|
+
report(r, op, guards);
|
|
534
|
+
visit(l, guards);
|
|
535
|
+
visit(r, guards);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
if (op === "-_") {
|
|
539
|
+
report(node.args, "-", guards);
|
|
540
|
+
visit(node.args, guards);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
for (const child of childNodes(node)) visit(child, guards);
|
|
544
|
+
};
|
|
545
|
+
visit(ast, /* @__PURE__ */ new Set());
|
|
546
|
+
return findings;
|
|
547
|
+
}
|
|
548
|
+
function nullGuardMessage(subject, objectName, finding) {
|
|
549
|
+
const owner = objectName ? `'${objectName}'` : "this object";
|
|
550
|
+
const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
|
|
551
|
+
return `${subject} applies \`${finding.operator}\` to \`${finding.operand}\`, which ${owner} declares as nullable (no \`required: true\`, no \`defaultValue\`).${hasNote} At runtime the operand is null, CEL has no \`${finding.operator}\` overload for null, and the whole predicate aborts \u2014 so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763). The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/validate-expressions.ts
|
|
335
555
|
function asArray2(v) {
|
|
336
556
|
if (Array.isArray(v)) return v;
|
|
337
557
|
if (v && typeof v === "object") {
|
|
@@ -375,11 +595,84 @@ function buildFieldTypeIndex(objects) {
|
|
|
375
595
|
}
|
|
376
596
|
return idx;
|
|
377
597
|
}
|
|
598
|
+
function fieldEntries(obj) {
|
|
599
|
+
const fields = obj.fields;
|
|
600
|
+
if (Array.isArray(fields)) {
|
|
601
|
+
return fields.filter((f) => f && typeof f === "object" && typeof f.name === "string").map((f) => [f.name, f]);
|
|
602
|
+
}
|
|
603
|
+
if (fields && typeof fields === "object") {
|
|
604
|
+
return Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]);
|
|
605
|
+
}
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
function isNullableField(def) {
|
|
609
|
+
if (def.required === true) return false;
|
|
610
|
+
if (def.defaultValue !== void 0 && def.defaultValue !== null) return false;
|
|
611
|
+
if (def.type === "autonumber") return false;
|
|
612
|
+
const options = def.options;
|
|
613
|
+
if (Array.isArray(options) && options.some((o) => !!o && typeof o === "object" && o.default === true)) {
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
return true;
|
|
617
|
+
}
|
|
618
|
+
function buildNullableFieldIndex(objects) {
|
|
619
|
+
const idx = /* @__PURE__ */ new Map();
|
|
620
|
+
for (const obj of objects) {
|
|
621
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
622
|
+
if (!name) continue;
|
|
623
|
+
const nullable = /* @__PURE__ */ new Set();
|
|
624
|
+
for (const [fname, def] of fieldEntries(obj)) {
|
|
625
|
+
if (isNullableField(def)) nullable.add(fname);
|
|
626
|
+
}
|
|
627
|
+
idx.set(name, nullable);
|
|
628
|
+
}
|
|
629
|
+
return idx;
|
|
630
|
+
}
|
|
631
|
+
function celSourceOf(raw) {
|
|
632
|
+
if (typeof raw === "string") return raw;
|
|
633
|
+
if (raw && typeof raw === "object") {
|
|
634
|
+
const rec = raw;
|
|
635
|
+
if (typeof rec.dialect === "string" && rec.dialect !== "cel") return void 0;
|
|
636
|
+
if (typeof rec.source === "string") return rec.source;
|
|
637
|
+
}
|
|
638
|
+
return void 0;
|
|
639
|
+
}
|
|
640
|
+
function rulePredicates(rule, path) {
|
|
641
|
+
const out = [];
|
|
642
|
+
const name = typeof rule.name === "string" ? rule.name : "?";
|
|
643
|
+
const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
|
|
644
|
+
const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
|
|
645
|
+
if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
|
|
646
|
+
if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
|
|
647
|
+
for (const branch of ["then", "otherwise"]) {
|
|
648
|
+
const nested = rule[branch];
|
|
649
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
650
|
+
out.push(...rulePredicates(nested, `${here} ${branch}`));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return out;
|
|
654
|
+
}
|
|
378
655
|
function validateStackExpressions(stack) {
|
|
379
656
|
const issues = [];
|
|
380
657
|
const objects = asArray2(stack.objects);
|
|
381
658
|
const fieldIndex = buildFieldIndex(objects);
|
|
382
659
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
660
|
+
const nullableIndex = buildNullableFieldIndex(objects);
|
|
661
|
+
const checkNullGuards = (where, subject, raw, objectName) => {
|
|
662
|
+
if (!objectName) return;
|
|
663
|
+
const nullableFields = nullableIndex.get(objectName);
|
|
664
|
+
if (!nullableFields || nullableFields.size === 0) return;
|
|
665
|
+
const source = celSourceOf(raw);
|
|
666
|
+
if (!source) return;
|
|
667
|
+
for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
668
|
+
issues.push({
|
|
669
|
+
where,
|
|
670
|
+
message: nullGuardMessage(subject, objectName, finding),
|
|
671
|
+
source,
|
|
672
|
+
severity: "error"
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
};
|
|
383
676
|
const check = (where, raw, objectName, scope = "flattened") => {
|
|
384
677
|
if (raw == null) return;
|
|
385
678
|
const fields = objectName ? fieldIndex.get(objectName) : void 0;
|
|
@@ -420,17 +713,17 @@ function validateStackExpressions(stack) {
|
|
|
420
713
|
if (node.type === "script") {
|
|
421
714
|
const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
|
|
422
715
|
const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
|
|
423
|
-
const
|
|
424
|
-
if (
|
|
716
|
+
const retired = ["actionType", "template", "recipients", "variables", "script"].filter((k) => cfg[k] != null);
|
|
717
|
+
if (retired.length > 0) {
|
|
425
718
|
issues.push({
|
|
426
719
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
427
|
-
message: `script node
|
|
720
|
+
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. `) + `Run \`os migrate meta --from 16\` to rewrite it automatically.`,
|
|
428
721
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
429
722
|
});
|
|
430
|
-
} else if (
|
|
723
|
+
} else if (!fn) {
|
|
431
724
|
issues.push({
|
|
432
725
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
433
|
-
message: `script node
|
|
726
|
+
message: `script node declares no \`function\` \u2014 it would do nothing at runtime. Name a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
|
|
434
727
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
435
728
|
});
|
|
436
729
|
}
|
|
@@ -448,6 +741,9 @@ function validateStackExpressions(stack) {
|
|
|
448
741
|
const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
|
|
449
742
|
check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
|
|
450
743
|
check(`${where} when`, rule.when, objectName, "record");
|
|
744
|
+
for (const p of rulePredicates(rule, "")) {
|
|
745
|
+
checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
|
|
746
|
+
}
|
|
451
747
|
}
|
|
452
748
|
const fields = obj.fields;
|
|
453
749
|
const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
|
|
@@ -500,6 +796,12 @@ function validateStackExpressions(stack) {
|
|
|
500
796
|
const hookName = hook.name ?? "?";
|
|
501
797
|
if (typeof hook.object === "string") {
|
|
502
798
|
check(`hook '${hookName}' (${hook.object}) condition`, hook.condition, hook.object, "record");
|
|
799
|
+
checkNullGuards(
|
|
800
|
+
`hook '${hookName}' (${hook.object}) condition`,
|
|
801
|
+
`hook '${hookName}' condition`,
|
|
802
|
+
hook.condition,
|
|
803
|
+
hook.object
|
|
804
|
+
);
|
|
503
805
|
continue;
|
|
504
806
|
}
|
|
505
807
|
const targets = Array.isArray(hook.object) ? hook.object.filter((o) => typeof o === "string" && o !== "*") : [];
|
|
@@ -513,6 +815,12 @@ function validateStackExpressions(stack) {
|
|
|
513
815
|
for (const target of targets) {
|
|
514
816
|
const mark = issues.length;
|
|
515
817
|
check(`hook '${hookName}' (${target}) condition`, hook.condition, target, "record");
|
|
818
|
+
checkNullGuards(
|
|
819
|
+
`hook '${hookName}' (${target}) condition`,
|
|
820
|
+
`hook '${hookName}' condition`,
|
|
821
|
+
hook.condition,
|
|
822
|
+
target
|
|
823
|
+
);
|
|
516
824
|
for (let i = mark; i < issues.length; i++) {
|
|
517
825
|
const issue = issues[i];
|
|
518
826
|
const key = `${issue.message}\0${issue.source ?? ""}`;
|
|
@@ -594,6 +902,76 @@ function validateListViewMode(stack) {
|
|
|
594
902
|
return out;
|
|
595
903
|
}
|
|
596
904
|
|
|
905
|
+
// src/validate-functional-completeness.ts
|
|
906
|
+
import {
|
|
907
|
+
checkFieldCompleteness,
|
|
908
|
+
checkViewCompleteness,
|
|
909
|
+
checkWebhookCompleteness
|
|
910
|
+
} from "@objectstack/spec/kernel";
|
|
911
|
+
var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
912
|
+
function entriesOf(v) {
|
|
913
|
+
if (Array.isArray(v)) {
|
|
914
|
+
return v.flatMap(
|
|
915
|
+
(def, i) => isRec(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
if (isRec(v)) {
|
|
919
|
+
return Object.entries(v).flatMap(
|
|
920
|
+
([name, def]) => isRec(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
function push(out, found, where, basePath) {
|
|
926
|
+
for (const f of found) {
|
|
927
|
+
out.push({
|
|
928
|
+
severity: f.severity,
|
|
929
|
+
rule: f.rule,
|
|
930
|
+
where,
|
|
931
|
+
path: `${basePath}.${f.path}`,
|
|
932
|
+
message: f.message,
|
|
933
|
+
hint: f.fix
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function validateFunctionalCompleteness(stack) {
|
|
938
|
+
const out = [];
|
|
939
|
+
if (!isRec(stack)) return out;
|
|
940
|
+
for (const [oi, obj] of entriesOf(stack.objects).entries()) {
|
|
941
|
+
for (const field of entriesOf(obj.def.fields)) {
|
|
942
|
+
push(
|
|
943
|
+
out,
|
|
944
|
+
checkFieldCompleteness(field.def),
|
|
945
|
+
`object "${obj.name}" \u203A fields.${field.name}`,
|
|
946
|
+
`objects[${oi}].fields${field.key}`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
for (const [vi, container] of entriesOf(stack.views).entries()) {
|
|
951
|
+
const where = container.def.object ? `view container "${container.name}"` : `view container [${vi}]`;
|
|
952
|
+
if (isRec(container.def.list)) {
|
|
953
|
+
push(out, checkViewCompleteness(container.def.list), `${where} \u203A list`, `views[${vi}].list`);
|
|
954
|
+
}
|
|
955
|
+
for (const lv of entriesOf(container.def.listViews)) {
|
|
956
|
+
push(
|
|
957
|
+
out,
|
|
958
|
+
checkViewCompleteness(lv.def),
|
|
959
|
+
`${where} \u203A listViews.${lv.name}`,
|
|
960
|
+
`views[${vi}].listViews${lv.key}`
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
for (const hook of entriesOf(stack.webhooks)) {
|
|
965
|
+
push(
|
|
966
|
+
out,
|
|
967
|
+
checkWebhookCompleteness(hook.def),
|
|
968
|
+
`webhook "${hook.name}"`,
|
|
969
|
+
`webhooks${hook.key}`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
return out;
|
|
973
|
+
}
|
|
974
|
+
|
|
597
975
|
// src/validate-flow-trigger-readiness.ts
|
|
598
976
|
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
599
977
|
var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
@@ -693,7 +1071,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
693
1071
|
|
|
694
1072
|
// src/flow-walk.ts
|
|
695
1073
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
696
|
-
function
|
|
1074
|
+
function isRec2(v) {
|
|
697
1075
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
698
1076
|
}
|
|
699
1077
|
function strName(v) {
|
|
@@ -708,7 +1086,7 @@ function flowNodeLabel(node, index) {
|
|
|
708
1086
|
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
709
1087
|
}
|
|
710
1088
|
function stripRegions(config) {
|
|
711
|
-
if (!
|
|
1089
|
+
if (!isRec2(config)) return void 0;
|
|
712
1090
|
let out;
|
|
713
1091
|
for (const key of Object.keys(config)) {
|
|
714
1092
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -719,11 +1097,11 @@ function stripRegions(config) {
|
|
|
719
1097
|
}
|
|
720
1098
|
function walkFlowNodes(flow, flowPath) {
|
|
721
1099
|
const out = [];
|
|
722
|
-
if (!
|
|
1100
|
+
if (!isRec2(flow)) return out;
|
|
723
1101
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
724
1102
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
725
1103
|
nodes.forEach((raw, index) => {
|
|
726
|
-
if (!
|
|
1104
|
+
if (!isRec2(raw)) return;
|
|
727
1105
|
const path = `${basePath}[${index}]`;
|
|
728
1106
|
out.push({
|
|
729
1107
|
node: raw,
|
|
@@ -734,7 +1112,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
734
1112
|
});
|
|
735
1113
|
const type = strName(raw.type);
|
|
736
1114
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
737
|
-
if (!slots || !
|
|
1115
|
+
if (!slots || !isRec2(raw.config)) return;
|
|
738
1116
|
const config = raw.config;
|
|
739
1117
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
740
1118
|
for (const slot of slots) {
|
|
@@ -742,7 +1120,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
742
1120
|
if (slot === "branches") {
|
|
743
1121
|
if (!Array.isArray(value)) continue;
|
|
744
1122
|
value.forEach((branch, b) => {
|
|
745
|
-
if (!
|
|
1123
|
+
if (!isRec2(branch)) return;
|
|
746
1124
|
const branchName = strName(branch.name) ?? `#${b}`;
|
|
747
1125
|
visitList(
|
|
748
1126
|
branch.nodes,
|
|
@@ -753,7 +1131,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
753
1131
|
});
|
|
754
1132
|
continue;
|
|
755
1133
|
}
|
|
756
|
-
if (!
|
|
1134
|
+
if (!isRec2(value)) continue;
|
|
757
1135
|
visitList(
|
|
758
1136
|
value.nodes,
|
|
759
1137
|
`${path}.config.${slot}.nodes`,
|
|
@@ -1500,11 +1878,24 @@ function validateReactPages(stack) {
|
|
|
1500
1878
|
|
|
1501
1879
|
// src/validate-react-page-props.ts
|
|
1502
1880
|
import { createRequire as createRequire2 } from "module";
|
|
1503
|
-
import {
|
|
1881
|
+
import {
|
|
1882
|
+
REACT_BLOCKS,
|
|
1883
|
+
RECORD_CONTEXT_BLOCK_TAGS,
|
|
1884
|
+
REACT_RECORD_BLOCK_ALTERNATIVES,
|
|
1885
|
+
chartAggregateResultKeys,
|
|
1886
|
+
isRecordContextBlockType
|
|
1887
|
+
} from "@objectstack/spec/ui";
|
|
1504
1888
|
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
1505
1889
|
|
|
1506
1890
|
// src/validate-searchable-fields.ts
|
|
1891
|
+
import {
|
|
1892
|
+
resolveSearchFieldResolution,
|
|
1893
|
+
SEARCHABLE_TEXTUAL_TYPES,
|
|
1894
|
+
SEARCHABLE_ENUM_TYPES,
|
|
1895
|
+
SEARCH_AUTO_EXCLUDED_FIELDS
|
|
1896
|
+
} from "@objectstack/spec/data";
|
|
1507
1897
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1898
|
+
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1508
1899
|
function asArray10(v) {
|
|
1509
1900
|
if (Array.isArray(v)) return v;
|
|
1510
1901
|
if (v && typeof v === "object") {
|
|
@@ -1512,21 +1903,50 @@ function asArray10(v) {
|
|
|
1512
1903
|
}
|
|
1513
1904
|
return [];
|
|
1514
1905
|
}
|
|
1515
|
-
function
|
|
1906
|
+
function isRec3(v) {
|
|
1516
1907
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1517
1908
|
}
|
|
1518
1909
|
function strName2(v) {
|
|
1519
1910
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1520
1911
|
}
|
|
1521
|
-
function
|
|
1912
|
+
function declaredFieldTarget(obj) {
|
|
1522
1913
|
const fields = obj.fields;
|
|
1523
1914
|
if (!fields || typeof fields !== "object") return null;
|
|
1524
1915
|
const names = /* @__PURE__ */ new Set();
|
|
1916
|
+
const metas = {};
|
|
1525
1917
|
for (const f of asArray10(fields)) {
|
|
1526
1918
|
const n = strName2(f.name);
|
|
1527
|
-
if (n)
|
|
1919
|
+
if (!n) continue;
|
|
1920
|
+
names.add(n);
|
|
1921
|
+
metas[n] = {
|
|
1922
|
+
type: typeof f.type === "string" ? f.type : void 0,
|
|
1923
|
+
hidden: f.hidden === true
|
|
1924
|
+
};
|
|
1528
1925
|
}
|
|
1529
|
-
|
|
1926
|
+
if (names.size === 0) return null;
|
|
1927
|
+
const searchableFields = Array.isArray(obj.searchableFields) ? obj.searchableFields.filter((e) => typeof e === "string") : void 0;
|
|
1928
|
+
return {
|
|
1929
|
+
names,
|
|
1930
|
+
fields: metas,
|
|
1931
|
+
searchableFields,
|
|
1932
|
+
displayField: strName2(obj.nameField) ?? strName2(obj.displayNameField)
|
|
1933
|
+
};
|
|
1934
|
+
}
|
|
1935
|
+
function resolveAllowedSet(target) {
|
|
1936
|
+
let fields = target.fields;
|
|
1937
|
+
const systemDeclared = (target.searchableFields ?? []).filter(
|
|
1938
|
+
(f) => !target.names.has(f) && SYSTEM_FIELDS.has(f)
|
|
1939
|
+
);
|
|
1940
|
+
if (systemDeclared.length > 0) {
|
|
1941
|
+
fields = { ...fields };
|
|
1942
|
+
for (const f of systemDeclared) fields[f] = {};
|
|
1943
|
+
}
|
|
1944
|
+
const { allowed, source } = resolveSearchFieldResolution({
|
|
1945
|
+
fields,
|
|
1946
|
+
searchableFields: target.searchableFields,
|
|
1947
|
+
displayField: target.displayField
|
|
1948
|
+
});
|
|
1949
|
+
return { allowed: new Set(allowed), source, declaredList: allowed };
|
|
1530
1950
|
}
|
|
1531
1951
|
function suggest2(target, known) {
|
|
1532
1952
|
let best;
|
|
@@ -1559,50 +1979,87 @@ function distance(a, b) {
|
|
|
1559
1979
|
}
|
|
1560
1980
|
function indexObjectSearchTargets(stack) {
|
|
1561
1981
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1562
|
-
if (!
|
|
1982
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1563
1983
|
for (const obj of asArray10(stack.objects)) {
|
|
1564
1984
|
const name = strName2(obj.name);
|
|
1565
|
-
if (name) fieldsByObject.set(name,
|
|
1985
|
+
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
1566
1986
|
}
|
|
1567
1987
|
return fieldsByObject;
|
|
1568
1988
|
}
|
|
1569
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
|
|
1989
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
1570
1990
|
const findings = [];
|
|
1571
1991
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1572
1992
|
if (!objectName) return findings;
|
|
1573
1993
|
if (!fieldsByObject.has(objectName)) return findings;
|
|
1574
|
-
const
|
|
1575
|
-
if (!
|
|
1994
|
+
const target = fieldsByObject.get(objectName);
|
|
1995
|
+
if (!target) return findings;
|
|
1996
|
+
const known = target.names;
|
|
1997
|
+
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
1576
1998
|
for (let i = 0; i < declared.length; i++) {
|
|
1577
1999
|
const entry = declared[i];
|
|
1578
2000
|
const name = strName2(entry);
|
|
1579
2001
|
if (!name) continue;
|
|
1580
|
-
if (known.has(name)
|
|
1581
|
-
|
|
2002
|
+
if (!known.has(name) && !SYSTEM_FIELDS.has(name)) {
|
|
2003
|
+
const dotted = name.includes(".");
|
|
2004
|
+
findings.push({
|
|
2005
|
+
severity: "error",
|
|
2006
|
+
rule: SEARCHABLE_FIELD_UNKNOWN,
|
|
2007
|
+
where,
|
|
2008
|
+
path: `${path}[${i}]`,
|
|
2009
|
+
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 ? "" : suggest2(name, known)),
|
|
2010
|
+
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 formula 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(", ")}.` : "")
|
|
2011
|
+
});
|
|
2012
|
+
continue;
|
|
2013
|
+
}
|
|
2014
|
+
if (!resolution || resolution.allowed.has(name)) continue;
|
|
2015
|
+
if (!known.has(name)) continue;
|
|
2016
|
+
const meta = target.fields[name];
|
|
2017
|
+
if (resolution.source === "declared") {
|
|
2018
|
+
findings.push({
|
|
2019
|
+
severity: "error",
|
|
2020
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
2021
|
+
where,
|
|
2022
|
+
path: `${path}[${i}]`,
|
|
2023
|
+
message: `${subject} entry "${name}" is outside object "${objectName}"'s declared searchableFields (${resolution.declaredList.join(", ")}) \u2014 the set 'search' scans. Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses an entry outside the allowed set: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
2024
|
+
hint: `Add "${name}" to ${objectName}.searchableFields, or drop it from this view \u2014 a view narrows the object's searchable set, never widens it (ADR-0061).`
|
|
2025
|
+
});
|
|
2026
|
+
continue;
|
|
2027
|
+
}
|
|
2028
|
+
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
2029
|
+
let why;
|
|
2030
|
+
if (SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
2031
|
+
why = "a system/audit column, which the auto-default set never includes";
|
|
2032
|
+
} else if (meta?.hidden) {
|
|
2033
|
+
why = "hidden";
|
|
2034
|
+
} else if (typeof meta?.type === "string") {
|
|
2035
|
+
why = `of type '${meta.type}', which 'search' cannot scan`;
|
|
2036
|
+
} else {
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
1582
2039
|
findings.push({
|
|
1583
2040
|
severity: "error",
|
|
1584
|
-
rule:
|
|
2041
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1585
2042
|
where,
|
|
1586
2043
|
path: `${path}[${i}]`,
|
|
1587
|
-
message: `${subject} entry "${name}"
|
|
1588
|
-
hint: (
|
|
2044
|
+
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).`,
|
|
2045
|
+
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/formula 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.`
|
|
1589
2046
|
});
|
|
1590
2047
|
}
|
|
1591
2048
|
return findings;
|
|
1592
2049
|
}
|
|
1593
2050
|
function validateSearchableFields(stack) {
|
|
1594
2051
|
const findings = [];
|
|
1595
|
-
if (!
|
|
2052
|
+
if (!isRec3(stack)) return findings;
|
|
1596
2053
|
const objects = asArray10(stack.objects);
|
|
1597
2054
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1598
|
-
const check = (declared, objectName, where, path, subject) => {
|
|
2055
|
+
const check = (declared, objectName, where, path, subject, role) => {
|
|
1599
2056
|
findings.push(
|
|
1600
|
-
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
|
|
2057
|
+
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
|
|
1601
2058
|
);
|
|
1602
2059
|
};
|
|
1603
2060
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1604
2061
|
const obj = objects[oi];
|
|
1605
|
-
if (!
|
|
2062
|
+
if (!isRec3(obj)) continue;
|
|
1606
2063
|
const objName = strName2(obj.name);
|
|
1607
2064
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1608
2065
|
check(
|
|
@@ -1610,11 +2067,12 @@ function validateSearchableFields(stack) {
|
|
|
1610
2067
|
objName,
|
|
1611
2068
|
label2,
|
|
1612
2069
|
`objects[${oi}].searchableFields`,
|
|
1613
|
-
"searchableFields"
|
|
2070
|
+
"searchableFields",
|
|
2071
|
+
"canonical"
|
|
1614
2072
|
);
|
|
1615
|
-
if (
|
|
2073
|
+
if (isRec3(obj.listViews)) {
|
|
1616
2074
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1617
|
-
if (!
|
|
2075
|
+
if (!isRec3(lv)) continue;
|
|
1618
2076
|
check(
|
|
1619
2077
|
lv.searchableFields,
|
|
1620
2078
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1622,7 +2080,8 @@ function validateSearchableFields(stack) {
|
|
|
1622
2080
|
listViewObject(lv) ?? objName,
|
|
1623
2081
|
`${label2} \u203A listViews.${key}`,
|
|
1624
2082
|
`objects[${oi}].listViews.${key}.searchableFields`,
|
|
1625
|
-
"list-view searchableFields"
|
|
2083
|
+
"list-view searchableFields",
|
|
2084
|
+
"narrowing"
|
|
1626
2085
|
);
|
|
1627
2086
|
}
|
|
1628
2087
|
}
|
|
@@ -1630,27 +2089,29 @@ function validateSearchableFields(stack) {
|
|
|
1630
2089
|
const views = asArray10(stack.views);
|
|
1631
2090
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1632
2091
|
const view = views[vi];
|
|
1633
|
-
if (!
|
|
2092
|
+
if (!isRec3(view)) continue;
|
|
1634
2093
|
const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
1635
2094
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
1636
|
-
if (
|
|
2095
|
+
if (isRec3(view.list)) {
|
|
1637
2096
|
check(
|
|
1638
2097
|
view.list.searchableFields,
|
|
1639
2098
|
listViewObject(view.list) ?? viewObject,
|
|
1640
2099
|
`view "${viewLabel}" \u203A list`,
|
|
1641
2100
|
`views[${vi}].list.searchableFields`,
|
|
1642
|
-
"list-view searchableFields"
|
|
2101
|
+
"list-view searchableFields",
|
|
2102
|
+
"narrowing"
|
|
1643
2103
|
);
|
|
1644
2104
|
}
|
|
1645
|
-
if (
|
|
2105
|
+
if (isRec3(view.listViews)) {
|
|
1646
2106
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1647
|
-
if (!
|
|
2107
|
+
if (!isRec3(lv)) continue;
|
|
1648
2108
|
check(
|
|
1649
2109
|
lv.searchableFields,
|
|
1650
2110
|
listViewObject(lv) ?? viewObject,
|
|
1651
2111
|
`view "${viewLabel}" \u203A listViews.${key}`,
|
|
1652
2112
|
`views[${vi}].listViews.${key}.searchableFields`,
|
|
1653
|
-
"list-view searchableFields"
|
|
2113
|
+
"list-view searchableFields",
|
|
2114
|
+
"narrowing"
|
|
1654
2115
|
);
|
|
1655
2116
|
}
|
|
1656
2117
|
}
|
|
@@ -1659,11 +2120,11 @@ function validateSearchableFields(stack) {
|
|
|
1659
2120
|
}
|
|
1660
2121
|
function listViewObject(listView) {
|
|
1661
2122
|
const data = listView.data;
|
|
1662
|
-
return
|
|
2123
|
+
return isRec3(data) ? strName2(data.object) : void 0;
|
|
1663
2124
|
}
|
|
1664
2125
|
|
|
1665
2126
|
// src/page-walk.ts
|
|
1666
|
-
function
|
|
2127
|
+
function isRec4(v) {
|
|
1667
2128
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1668
2129
|
}
|
|
1669
2130
|
function strName3(v) {
|
|
@@ -1676,19 +2137,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1676
2137
|
}
|
|
1677
2138
|
function walkPageComponents(page, pagePath) {
|
|
1678
2139
|
const out = [];
|
|
1679
|
-
if (!
|
|
2140
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1680
2141
|
const pageObject = strName3(page.object);
|
|
1681
2142
|
const visit = (node, path, inheritedObject) => {
|
|
1682
|
-
if (!
|
|
1683
|
-
const props =
|
|
1684
|
-
const dataSource =
|
|
2143
|
+
if (!isRec4(node)) return;
|
|
2144
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2145
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1685
2146
|
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
1686
2147
|
out.push({ component: node, path, objectName });
|
|
1687
2148
|
if (!props) return;
|
|
1688
2149
|
if (Array.isArray(props.items)) {
|
|
1689
2150
|
for (let i = 0; i < props.items.length; i++) {
|
|
1690
2151
|
const item = props.items[i];
|
|
1691
|
-
if (!
|
|
2152
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1692
2153
|
for (let c = 0; c < item.children.length; c++) {
|
|
1693
2154
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1694
2155
|
}
|
|
@@ -1710,12 +2171,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1710
2171
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1711
2172
|
for (let r = 0; r < regions.length; r++) {
|
|
1712
2173
|
const region = regions[r];
|
|
1713
|
-
if (!
|
|
2174
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1714
2175
|
for (let c = 0; c < region.components.length; c++) {
|
|
1715
2176
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1716
2177
|
}
|
|
1717
2178
|
}
|
|
1718
|
-
const slots =
|
|
2179
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1719
2180
|
if (slots) {
|
|
1720
2181
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1721
2182
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -1740,7 +2201,7 @@ function asArray11(v) {
|
|
|
1740
2201
|
function strName4(v) {
|
|
1741
2202
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1742
2203
|
}
|
|
1743
|
-
function
|
|
2204
|
+
function isRec5(v) {
|
|
1744
2205
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1745
2206
|
}
|
|
1746
2207
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -1751,7 +2212,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
1751
2212
|
out.push({ name: bare, path });
|
|
1752
2213
|
return;
|
|
1753
2214
|
}
|
|
1754
|
-
if (!
|
|
2215
|
+
if (!isRec5(v)) return;
|
|
1755
2216
|
const named = strName4(v.field) ?? strName4(v.name);
|
|
1756
2217
|
if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
|
|
1757
2218
|
};
|
|
@@ -1793,15 +2254,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
1793
2254
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
1794
2255
|
for (let si = 0; si < sections.length; si++) {
|
|
1795
2256
|
const section = sections[si];
|
|
1796
|
-
if (!
|
|
2257
|
+
if (!isRec5(section)) continue;
|
|
1797
2258
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
1798
2259
|
}
|
|
1799
2260
|
}
|
|
1800
2261
|
return refs;
|
|
1801
2262
|
}
|
|
1802
2263
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
1803
|
-
const add =
|
|
1804
|
-
const picker = add &&
|
|
2264
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2265
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
1805
2266
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
1806
2267
|
return {
|
|
1807
2268
|
relatedObject: strName4(props.objectName),
|
|
@@ -1822,7 +2283,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
1822
2283
|
}
|
|
1823
2284
|
function indexObjectFields(stack) {
|
|
1824
2285
|
const objectFields = /* @__PURE__ */ new Map();
|
|
1825
|
-
if (!
|
|
2286
|
+
if (!isRec5(stack)) return objectFields;
|
|
1826
2287
|
for (const obj of asArray11(stack.objects)) {
|
|
1827
2288
|
const name = strName4(obj.name);
|
|
1828
2289
|
if (!name) continue;
|
|
@@ -1868,7 +2329,7 @@ function validatePageFieldBindings(stack) {
|
|
|
1868
2329
|
};
|
|
1869
2330
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
1870
2331
|
const type = strName4(component.type);
|
|
1871
|
-
const props =
|
|
2332
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
1872
2333
|
if (!type || !props) continue;
|
|
1873
2334
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
1874
2335
|
const base = `${path}.properties`;
|
|
@@ -1883,7 +2344,7 @@ function validatePageFieldBindings(stack) {
|
|
|
1883
2344
|
if (!refs) continue;
|
|
1884
2345
|
checkRefs(refs, objectName, where);
|
|
1885
2346
|
}
|
|
1886
|
-
const cfg =
|
|
2347
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
1887
2348
|
if (cfg) {
|
|
1888
2349
|
const cfgObject = strName4(cfg.source) ?? strName4(page.object);
|
|
1889
2350
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -1892,7 +2353,7 @@ function validatePageFieldBindings(stack) {
|
|
|
1892
2353
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
1893
2354
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
1894
2355
|
];
|
|
1895
|
-
const userFilters =
|
|
2356
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
1896
2357
|
if (userFilters) {
|
|
1897
2358
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
1898
2359
|
}
|
|
@@ -2007,28 +2468,28 @@ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
|
2007
2468
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2008
2469
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2009
2470
|
var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
|
|
2010
|
-
var
|
|
2471
|
+
var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2011
2472
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2012
2473
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
2013
2474
|
const { values, where, path } = attrs;
|
|
2014
|
-
const
|
|
2475
|
+
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2015
2476
|
if (values.has("data")) return;
|
|
2016
2477
|
const aggregate = values.get("aggregate");
|
|
2017
2478
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2018
|
-
if (!
|
|
2479
|
+
if (!isRec6(aggregate)) return;
|
|
2019
2480
|
const fn = strOf(aggregate.function);
|
|
2020
2481
|
const field = strOf(aggregate.field);
|
|
2021
2482
|
const groupBy = aggregate.groupBy;
|
|
2022
|
-
const groupByField = strOf(groupBy) ?? (
|
|
2483
|
+
const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2023
2484
|
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2024
|
-
|
|
2485
|
+
push2(
|
|
2025
2486
|
"error",
|
|
2026
2487
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2027
2488
|
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2028
2489
|
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2029
2490
|
);
|
|
2030
2491
|
} else if (fn && fn !== "count" && !field) {
|
|
2031
|
-
|
|
2492
|
+
push2(
|
|
2032
2493
|
"error",
|
|
2033
2494
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2034
2495
|
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
@@ -2042,7 +2503,7 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2042
2503
|
if (!name) return;
|
|
2043
2504
|
if (name.includes(".")) return;
|
|
2044
2505
|
if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
|
|
2045
|
-
|
|
2506
|
+
push2(
|
|
2046
2507
|
"error",
|
|
2047
2508
|
REACT_CHART_FIELD_UNKNOWN,
|
|
2048
2509
|
`aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
|
|
@@ -2059,7 +2520,7 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2059
2520
|
if (!name) return;
|
|
2060
2521
|
if (columns.includes(name)) return;
|
|
2061
2522
|
if (keys.comparison && name === keys.comparison) return;
|
|
2062
|
-
|
|
2523
|
+
push2(
|
|
2063
2524
|
"error",
|
|
2064
2525
|
REACT_CHART_AXIS_UNKNOWN,
|
|
2065
2526
|
`"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
|
|
@@ -2067,24 +2528,24 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2067
2528
|
);
|
|
2068
2529
|
};
|
|
2069
2530
|
const xAxisRaw = values.get("xAxis");
|
|
2070
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
2531
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
2071
2532
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
2072
2533
|
axisRef(categoryAxis, categoryProp);
|
|
2073
2534
|
const yAxisRaw = values.get("yAxis");
|
|
2074
2535
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
2075
2536
|
for (const a of yAxisList) {
|
|
2076
|
-
axisRef(strOf(a) ?? (
|
|
2537
|
+
axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
2077
2538
|
}
|
|
2078
2539
|
const series = values.get("series");
|
|
2079
2540
|
if (Array.isArray(series)) {
|
|
2080
2541
|
for (const s of series) {
|
|
2081
|
-
if (!
|
|
2542
|
+
if (!isRec6(s)) continue;
|
|
2082
2543
|
const dataKey = strOf(s.dataKey);
|
|
2083
2544
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
2084
2545
|
}
|
|
2085
2546
|
}
|
|
2086
2547
|
if (categoryAxis && keys.category && categoryAxis !== keys.category && categoryAxis === keys.value) {
|
|
2087
|
-
|
|
2548
|
+
push2(
|
|
2088
2549
|
"error",
|
|
2089
2550
|
REACT_CHART_AXIS_UNKNOWN,
|
|
2090
2551
|
`${categoryProp} "${categoryAxis}" is the aggregate's VALUE column, not its category column.`,
|
|
@@ -2134,7 +2595,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
2134
2595
|
if (!Array.isArray(value)) return { child, parent };
|
|
2135
2596
|
for (let i = 0; i < value.length; i++) {
|
|
2136
2597
|
const sub = value[i];
|
|
2137
|
-
if (!
|
|
2598
|
+
if (!isRec6(sub)) continue;
|
|
2138
2599
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2139
2600
|
child.push({
|
|
2140
2601
|
objectName: strOf(sub.childObject),
|
|
@@ -2179,20 +2640,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
2179
2640
|
}
|
|
2180
2641
|
for (const key of spec.nestedFields ?? []) {
|
|
2181
2642
|
const v = readable(key);
|
|
2182
|
-
if (
|
|
2643
|
+
if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2183
2644
|
}
|
|
2184
2645
|
for (const key of spec.sections ?? []) {
|
|
2185
2646
|
const v = readable(key);
|
|
2186
2647
|
if (!Array.isArray(v)) continue;
|
|
2187
2648
|
for (let i = 0; i < v.length; i++) {
|
|
2188
2649
|
const section = v[i];
|
|
2189
|
-
if (!
|
|
2650
|
+
if (!isRec6(section)) continue;
|
|
2190
2651
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2191
2652
|
}
|
|
2192
2653
|
}
|
|
2193
2654
|
for (const key of spec.keyedByField ?? []) {
|
|
2194
2655
|
const v = readable(key);
|
|
2195
|
-
if (!
|
|
2656
|
+
if (!isRec6(v)) continue;
|
|
2196
2657
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2197
2658
|
}
|
|
2198
2659
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -2218,25 +2679,41 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
2218
2679
|
out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
|
|
2219
2680
|
}
|
|
2220
2681
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
2221
|
-
if (schemaType) {
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
|
|
2231
|
-
objectName,
|
|
2232
|
-
objectFields,
|
|
2233
|
-
where
|
|
2234
|
-
)
|
|
2235
|
-
);
|
|
2236
|
-
}
|
|
2682
|
+
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
2683
|
+
out.push(
|
|
2684
|
+
...checkFieldRefs(
|
|
2685
|
+
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
2686
|
+
objectName,
|
|
2687
|
+
objectFields,
|
|
2688
|
+
where
|
|
2689
|
+
)
|
|
2690
|
+
);
|
|
2237
2691
|
}
|
|
2238
2692
|
return out;
|
|
2239
2693
|
}
|
|
2694
|
+
var REACT_BLOCK_NEEDS_RECORD_CONTEXT = "react-block-needs-record-context";
|
|
2695
|
+
var RECORD_BLOCK_GENERIC_FIX = "author the page as `type:'record'` \u2014 a record page mounts the record context these blocks render from.";
|
|
2696
|
+
function recordContextFinding(tag, schemaType, where, path) {
|
|
2697
|
+
return {
|
|
2698
|
+
severity: "error",
|
|
2699
|
+
rule: REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
2700
|
+
where,
|
|
2701
|
+
path,
|
|
2702
|
+
message: `<${tag}> renders "${schemaType}", which reads its record from the record context a record page mounts \u2014 a kind:'react' page never mounts one, so the block renders empty no matter how it is bound (its objectName/recordId are not read by the renderer).`,
|
|
2703
|
+
hint: `On a react page bind the record yourself: ${REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function localComponentNames(tsc, sf) {
|
|
2707
|
+
const names = /* @__PURE__ */ new Set();
|
|
2708
|
+
const walk = (node) => {
|
|
2709
|
+
if (tsc.isFunctionDeclaration(node) && node.name) names.add(node.name.text);
|
|
2710
|
+
else if (tsc.isVariableDeclaration(node) && tsc.isIdentifier(node.name)) names.add(node.name.text);
|
|
2711
|
+
else if (tsc.isImportSpecifier(node)) names.add(node.name.text);
|
|
2712
|
+
tsc.forEachChild(node, walk);
|
|
2713
|
+
};
|
|
2714
|
+
walk(sf);
|
|
2715
|
+
return names;
|
|
2716
|
+
}
|
|
2240
2717
|
function validateReactPageProps(stack) {
|
|
2241
2718
|
const findings = [];
|
|
2242
2719
|
const objectFields = indexObjectFields(stack);
|
|
@@ -2255,9 +2732,18 @@ function validateReactPageProps(stack) {
|
|
|
2255
2732
|
} catch {
|
|
2256
2733
|
continue;
|
|
2257
2734
|
}
|
|
2735
|
+
const locals = localComponentNames(tsc, sf);
|
|
2258
2736
|
const visit = (node) => {
|
|
2259
2737
|
if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
|
|
2260
2738
|
const tag = node.tagName.getText(sf);
|
|
2739
|
+
const where = `page "${name}" \u203A <${tag}>`;
|
|
2740
|
+
const path = `pages[${p}].source`;
|
|
2741
|
+
const recordType = RECORD_CONTEXT_BLOCK_TAGS.get(tag);
|
|
2742
|
+
if (recordType && !locals.has(tag)) {
|
|
2743
|
+
findings.push(recordContextFinding(tag, recordType, where, path));
|
|
2744
|
+
tsc.forEachChild(node, visit);
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2261
2747
|
const block = BLOCKS.get(tag);
|
|
2262
2748
|
if (block) {
|
|
2263
2749
|
let hasSpread = false;
|
|
@@ -2277,8 +2763,14 @@ function validateReactPageProps(stack) {
|
|
|
2277
2763
|
);
|
|
2278
2764
|
}
|
|
2279
2765
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2766
|
+
if (tag === "Block") {
|
|
2767
|
+
const blockType = strOf(values.get("type"));
|
|
2768
|
+
if (blockType && isRecordContextBlockType(blockType)) {
|
|
2769
|
+
findings.push(recordContextFinding(tag, blockType, where, path));
|
|
2770
|
+
tsc.forEachChild(node, visit);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2282
2774
|
if (!hasSpread) {
|
|
2283
2775
|
for (const req of block.requiredBindings) {
|
|
2284
2776
|
if (!used.has(req)) {
|
|
@@ -3996,16 +4488,86 @@ function validateObjectReferences(stack) {
|
|
|
3996
4488
|
return findings;
|
|
3997
4489
|
}
|
|
3998
4490
|
|
|
4491
|
+
// src/validate-nav-target-refs.ts
|
|
4492
|
+
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4493
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4494
|
+
function asArray25(v) {
|
|
4495
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
4496
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
4497
|
+
return [];
|
|
4498
|
+
}
|
|
4499
|
+
function strName7(v) {
|
|
4500
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4501
|
+
}
|
|
4502
|
+
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
4503
|
+
var NAV_TARGETS = [
|
|
4504
|
+
["page", "pageName", "pages", "page"],
|
|
4505
|
+
["report", "reportName", "reports", "report"],
|
|
4506
|
+
["dashboard", "dashboardName", "dashboards", "dashboard"]
|
|
4507
|
+
];
|
|
4508
|
+
function namesOf(collection) {
|
|
4509
|
+
const out = /* @__PURE__ */ new Set();
|
|
4510
|
+
for (const entry of asArray25(collection)) {
|
|
4511
|
+
const n = strName7(entry.name);
|
|
4512
|
+
if (n) out.add(n);
|
|
4513
|
+
}
|
|
4514
|
+
return out;
|
|
4515
|
+
}
|
|
4516
|
+
function validateNavTargetRefs(stack) {
|
|
4517
|
+
const findings = [];
|
|
4518
|
+
if (!isRec7(stack)) return findings;
|
|
4519
|
+
const apps = asArray25(stack.apps);
|
|
4520
|
+
if (apps.length === 0) return findings;
|
|
4521
|
+
const declared = /* @__PURE__ */ new Map();
|
|
4522
|
+
for (const [, , collection] of NAV_TARGETS) {
|
|
4523
|
+
declared.set(collection, namesOf(stack[collection]));
|
|
4524
|
+
}
|
|
4525
|
+
for (const [ai, app] of apps.entries()) {
|
|
4526
|
+
const appName = strName7(app.name) ?? `#${ai}`;
|
|
4527
|
+
const walk = (items, basePath) => {
|
|
4528
|
+
if (!Array.isArray(items)) return;
|
|
4529
|
+
for (const [ni, raw] of items.entries()) {
|
|
4530
|
+
if (!isRec7(raw)) continue;
|
|
4531
|
+
const nav = raw;
|
|
4532
|
+
const navPath = `${basePath}[${ni}]`;
|
|
4533
|
+
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
4534
|
+
if (nav.type !== type) continue;
|
|
4535
|
+
const target = strName7(nav[prop]);
|
|
4536
|
+
if (!target || isInterpolated2(target)) continue;
|
|
4537
|
+
const known = declared.get(collection);
|
|
4538
|
+
if (known.has(target)) continue;
|
|
4539
|
+
const emptyCollection = known.size === 0;
|
|
4540
|
+
findings.push({
|
|
4541
|
+
severity: "warning",
|
|
4542
|
+
rule: NAV_TARGET_UNRESOLVED,
|
|
4543
|
+
where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? strName7(nav.label) ?? `#${ni}`}"`,
|
|
4544
|
+
path: `${navPath}.${prop}`,
|
|
4545
|
+
message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
|
|
4546
|
+
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
4547
|
+
});
|
|
4548
|
+
}
|
|
4549
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
4550
|
+
}
|
|
4551
|
+
};
|
|
4552
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4553
|
+
for (const [ari, area] of asArray25(app.areas).entries()) {
|
|
4554
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
4555
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
return findings;
|
|
4559
|
+
}
|
|
4560
|
+
|
|
3999
4561
|
// src/validate-action-name-refs.ts
|
|
4000
4562
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
4001
|
-
function
|
|
4563
|
+
function asArray26(v) {
|
|
4002
4564
|
if (Array.isArray(v)) return v;
|
|
4003
4565
|
if (v && typeof v === "object") {
|
|
4004
4566
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4005
4567
|
}
|
|
4006
4568
|
return [];
|
|
4007
4569
|
}
|
|
4008
|
-
function
|
|
4570
|
+
function strName8(v) {
|
|
4009
4571
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4010
4572
|
}
|
|
4011
4573
|
function strList(v) {
|
|
@@ -4042,14 +4604,14 @@ function suggest4(target, known) {
|
|
|
4042
4604
|
}
|
|
4043
4605
|
function collectActionNames(stack) {
|
|
4044
4606
|
const names = /* @__PURE__ */ new Set();
|
|
4045
|
-
for (const action of
|
|
4046
|
-
const n =
|
|
4607
|
+
for (const action of asArray26(stack.actions)) {
|
|
4608
|
+
const n = strName8(action?.name);
|
|
4047
4609
|
if (n) names.add(n);
|
|
4048
4610
|
}
|
|
4049
|
-
for (const obj of
|
|
4611
|
+
for (const obj of asArray26(stack.objects)) {
|
|
4050
4612
|
if (!obj || typeof obj !== "object") continue;
|
|
4051
|
-
for (const action of
|
|
4052
|
-
const n =
|
|
4613
|
+
for (const action of asArray26(obj.actions)) {
|
|
4614
|
+
const n = strName8(action?.name);
|
|
4053
4615
|
if (n) names.add(n);
|
|
4054
4616
|
}
|
|
4055
4617
|
}
|
|
@@ -4059,7 +4621,7 @@ function validateActionNameRefs(stack) {
|
|
|
4059
4621
|
const findings = [];
|
|
4060
4622
|
if (!stack || typeof stack !== "object") return findings;
|
|
4061
4623
|
const known = collectActionNames(stack);
|
|
4062
|
-
const check = (name, where, path, surface) => {
|
|
4624
|
+
const check = (name, where, path, surface, placement = "with the location this surface needs") => {
|
|
4063
4625
|
if (known.has(name)) return;
|
|
4064
4626
|
findings.push({
|
|
4065
4627
|
severity: "error",
|
|
@@ -4067,42 +4629,72 @@ function validateActionNameRefs(stack) {
|
|
|
4067
4629
|
where,
|
|
4068
4630
|
path,
|
|
4069
4631
|
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
|
|
4070
|
-
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`)
|
|
4632
|
+
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) ${placement}, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
|
|
4071
4633
|
});
|
|
4072
4634
|
};
|
|
4073
|
-
const
|
|
4635
|
+
const SELECTION_BAR_PLACEMENT = "(no `locations` entry needed \u2014 the selection bar places it by name)";
|
|
4636
|
+
const checkListContainer = (container, owner, label2, path) => {
|
|
4637
|
+
if (!container || typeof container !== "object") return;
|
|
4638
|
+
const list3 = container;
|
|
4639
|
+
for (const key of ["rowActions", "bulkActions"]) {
|
|
4640
|
+
const names = strList(list3[key]);
|
|
4641
|
+
for (let ai = 0; ai < names.length; ai++) {
|
|
4642
|
+
check(
|
|
4643
|
+
names[ai],
|
|
4644
|
+
`${owner} \xB7 ${label2} \xB7 ${key}`,
|
|
4645
|
+
`${path}.${key}[${ai}]`,
|
|
4646
|
+
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu",
|
|
4647
|
+
key === "bulkActions" ? SELECTION_BAR_PLACEMENT : void 0
|
|
4648
|
+
);
|
|
4649
|
+
}
|
|
4650
|
+
}
|
|
4651
|
+
const defs = Array.isArray(list3.bulkActionDefs) ? list3.bulkActionDefs : [];
|
|
4652
|
+
for (let di = 0; di < defs.length; di++) {
|
|
4653
|
+
const def = defs[di];
|
|
4654
|
+
if (!def || typeof def !== "object") continue;
|
|
4655
|
+
if (def.execution !== "aggregate") continue;
|
|
4656
|
+
if (def.actionDef !== void 0) continue;
|
|
4657
|
+
const name = strName8(def.name);
|
|
4658
|
+
if (!name) continue;
|
|
4659
|
+
check(
|
|
4660
|
+
name,
|
|
4661
|
+
`${owner} \xB7 ${label2} \xB7 bulkActionDefs[${di}]`,
|
|
4662
|
+
`${path}.bulkActionDefs[${di}].name`,
|
|
4663
|
+
"Aggregate bulk action",
|
|
4664
|
+
SELECTION_BAR_PLACEMENT
|
|
4665
|
+
);
|
|
4666
|
+
}
|
|
4667
|
+
};
|
|
4668
|
+
const views = asArray26(stack.views);
|
|
4074
4669
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4075
4670
|
const view = views[vi];
|
|
4076
4671
|
if (!view || typeof view !== "object") continue;
|
|
4077
|
-
const viewName =
|
|
4078
|
-
const
|
|
4079
|
-
|
|
4080
|
-
const list3 = container;
|
|
4081
|
-
for (const key of ["rowActions", "bulkActions"]) {
|
|
4082
|
-
const names = strList(list3[key]);
|
|
4083
|
-
for (let ai = 0; ai < names.length; ai++) {
|
|
4084
|
-
check(
|
|
4085
|
-
names[ai],
|
|
4086
|
-
`view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
|
|
4087
|
-
`${path}.${key}[${ai}]`,
|
|
4088
|
-
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
4089
|
-
);
|
|
4090
|
-
}
|
|
4091
|
-
}
|
|
4092
|
-
};
|
|
4093
|
-
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
4672
|
+
const viewName = strName8(view.name) ?? strName8(view.object) ?? `#${vi}`;
|
|
4673
|
+
const owner = `view "${viewName}"`;
|
|
4674
|
+
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
4094
4675
|
const listViews = view.listViews;
|
|
4095
4676
|
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
4096
4677
|
for (const [key, lv] of Object.entries(listViews)) {
|
|
4097
|
-
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
4678
|
+
checkListContainer(lv, owner, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
4098
4679
|
}
|
|
4099
4680
|
}
|
|
4100
4681
|
}
|
|
4101
|
-
const
|
|
4682
|
+
const objects = asArray26(stack.objects);
|
|
4683
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
4684
|
+
const obj = objects[oi];
|
|
4685
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4686
|
+
const objListViews = obj.listViews;
|
|
4687
|
+
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
4688
|
+
const owner = `object "${strName8(obj.name) ?? `#${oi}`}"`;
|
|
4689
|
+
for (const [key, lv] of Object.entries(objListViews)) {
|
|
4690
|
+
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
4691
|
+
}
|
|
4692
|
+
}
|
|
4693
|
+
const pages = asArray26(stack.pages);
|
|
4102
4694
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4103
4695
|
const page = pages[pi];
|
|
4104
4696
|
if (!page || typeof page !== "object") continue;
|
|
4105
|
-
const pageName =
|
|
4697
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
4106
4698
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4107
4699
|
const props = component.properties;
|
|
4108
4700
|
if (!props || typeof props !== "object") continue;
|
|
@@ -4110,30 +4702,30 @@ function validateActionNameRefs(stack) {
|
|
|
4110
4702
|
for (let ai = 0; ai < names.length; ai++) {
|
|
4111
4703
|
check(
|
|
4112
4704
|
names[ai],
|
|
4113
|
-
`page "${pageName}" \xB7 component "${
|
|
4705
|
+
`page "${pageName}" \xB7 component "${strName8(component.type) ?? "?"}"`,
|
|
4114
4706
|
`${path}.properties.actionNames[${ai}]`,
|
|
4115
4707
|
"Quick-actions bar"
|
|
4116
4708
|
);
|
|
4117
4709
|
}
|
|
4118
4710
|
}
|
|
4119
4711
|
}
|
|
4120
|
-
const apps =
|
|
4712
|
+
const apps = asArray26(stack.apps);
|
|
4121
4713
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4122
4714
|
const app = apps[ai];
|
|
4123
4715
|
if (!app || typeof app !== "object") continue;
|
|
4124
|
-
const appName =
|
|
4716
|
+
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4125
4717
|
const walkNav = (items, basePath) => {
|
|
4126
|
-
const navItems =
|
|
4718
|
+
const navItems = asArray26(items);
|
|
4127
4719
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4128
4720
|
const nav = navItems[ni];
|
|
4129
4721
|
if (!nav || typeof nav !== "object") continue;
|
|
4130
4722
|
const navPath = `${basePath}[${ni}]`;
|
|
4131
4723
|
const actionDef = nav.actionDef;
|
|
4132
|
-
const actionName =
|
|
4724
|
+
const actionName = strName8(actionDef?.actionName);
|
|
4133
4725
|
if (nav.type === "action" && actionName) {
|
|
4134
4726
|
check(
|
|
4135
4727
|
actionName,
|
|
4136
|
-
`app "${appName}" \xB7 nav "${
|
|
4728
|
+
`app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
|
|
4137
4729
|
`${navPath}.actionDef.actionName`,
|
|
4138
4730
|
"Navigation action item"
|
|
4139
4731
|
);
|
|
@@ -4142,7 +4734,7 @@ function validateActionNameRefs(stack) {
|
|
|
4142
4734
|
}
|
|
4143
4735
|
};
|
|
4144
4736
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4145
|
-
const areas =
|
|
4737
|
+
const areas = asArray26(app.areas);
|
|
4146
4738
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4147
4739
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4148
4740
|
}
|
|
@@ -4150,46 +4742,120 @@ function validateActionNameRefs(stack) {
|
|
|
4150
4742
|
return findings;
|
|
4151
4743
|
}
|
|
4152
4744
|
|
|
4153
|
-
// src/validate-
|
|
4154
|
-
var
|
|
4155
|
-
|
|
4156
|
-
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
4157
|
-
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
4158
|
-
function asArray26(v) {
|
|
4745
|
+
// src/validate-action-locations.ts
|
|
4746
|
+
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
4747
|
+
function asArray27(v) {
|
|
4159
4748
|
if (Array.isArray(v)) return v;
|
|
4160
4749
|
if (v && typeof v === "object") {
|
|
4161
4750
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4162
4751
|
}
|
|
4163
4752
|
return [];
|
|
4164
4753
|
}
|
|
4165
|
-
function
|
|
4754
|
+
function strName9(v) {
|
|
4166
4755
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4167
4756
|
}
|
|
4168
4757
|
function strList2(v) {
|
|
4169
4758
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
4170
4759
|
}
|
|
4171
|
-
function
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
if (n === 0) return m;
|
|
4179
|
-
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
4180
|
-
for (let i = 1; i <= m; i++) {
|
|
4181
|
-
const curr = [i, ...new Array(n).fill(0)];
|
|
4182
|
-
for (let j = 1; j <= n; j++) {
|
|
4183
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4184
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
4760
|
+
function collectNamePlacedActions(stack) {
|
|
4761
|
+
const placed = /* @__PURE__ */ new Set();
|
|
4762
|
+
const harvest = (container) => {
|
|
4763
|
+
if (!container || typeof container !== "object") return;
|
|
4764
|
+
const list3 = container;
|
|
4765
|
+
for (const key of ["rowActions", "bulkActions"]) {
|
|
4766
|
+
for (const n of strList2(list3[key])) placed.add(n);
|
|
4185
4767
|
}
|
|
4186
|
-
|
|
4768
|
+
for (const def of asArray27(list3.bulkActionDefs)) {
|
|
4769
|
+
const n = strName9(def?.name);
|
|
4770
|
+
if (n) placed.add(n);
|
|
4771
|
+
}
|
|
4772
|
+
};
|
|
4773
|
+
const harvestListViews = (listViews) => {
|
|
4774
|
+
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
4775
|
+
for (const lv of Object.values(listViews)) harvest(lv);
|
|
4776
|
+
};
|
|
4777
|
+
for (const view of asArray27(stack.views)) {
|
|
4778
|
+
if (!view || typeof view !== "object") continue;
|
|
4779
|
+
harvest(view.list);
|
|
4780
|
+
harvestListViews(view.listViews);
|
|
4187
4781
|
}
|
|
4188
|
-
|
|
4782
|
+
for (const obj of asArray27(stack.objects)) {
|
|
4783
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4784
|
+
harvestListViews(obj.listViews);
|
|
4785
|
+
}
|
|
4786
|
+
return placed;
|
|
4189
4787
|
}
|
|
4190
|
-
function
|
|
4191
|
-
|
|
4192
|
-
|
|
4788
|
+
function validateActionLocations(stack) {
|
|
4789
|
+
const findings = [];
|
|
4790
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
4791
|
+
const namePlaced = collectNamePlacedActions(stack);
|
|
4792
|
+
const check = (action, path) => {
|
|
4793
|
+
if (!action || typeof action !== "object") return;
|
|
4794
|
+
if ("locations" in action) return;
|
|
4795
|
+
const name = strName9(action.name);
|
|
4796
|
+
if (!name) return;
|
|
4797
|
+
if (namePlaced.has(name)) return;
|
|
4798
|
+
findings.push({
|
|
4799
|
+
severity: "warning",
|
|
4800
|
+
rule: ACTION_NO_PLACEMENT,
|
|
4801
|
+
where: `action "${name}"`,
|
|
4802
|
+
path,
|
|
4803
|
+
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.`,
|
|
4804
|
+
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`, `global_nav`); 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."
|
|
4805
|
+
});
|
|
4806
|
+
};
|
|
4807
|
+
const actions = asArray27(stack.actions);
|
|
4808
|
+
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
4809
|
+
const objects = asArray27(stack.objects);
|
|
4810
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
4811
|
+
const obj = objects[oi];
|
|
4812
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4813
|
+
const own = asArray27(obj.actions);
|
|
4814
|
+
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
4815
|
+
}
|
|
4816
|
+
return findings;
|
|
4817
|
+
}
|
|
4818
|
+
|
|
4819
|
+
// src/validate-chart-bindings.ts
|
|
4820
|
+
var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
4821
|
+
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
4822
|
+
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
4823
|
+
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
4824
|
+
function asArray28(v) {
|
|
4825
|
+
if (Array.isArray(v)) return v;
|
|
4826
|
+
if (v && typeof v === "object") {
|
|
4827
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4828
|
+
}
|
|
4829
|
+
return [];
|
|
4830
|
+
}
|
|
4831
|
+
function strName10(v) {
|
|
4832
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4833
|
+
}
|
|
4834
|
+
function strList3(v) {
|
|
4835
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
4836
|
+
}
|
|
4837
|
+
function isRec8(v) {
|
|
4838
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4839
|
+
}
|
|
4840
|
+
function distance4(a, b) {
|
|
4841
|
+
const m = a.length;
|
|
4842
|
+
const n = b.length;
|
|
4843
|
+
if (m === 0) return n;
|
|
4844
|
+
if (n === 0) return m;
|
|
4845
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
4846
|
+
for (let i = 1; i <= m; i++) {
|
|
4847
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
4848
|
+
for (let j = 1; j <= n; j++) {
|
|
4849
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4850
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
4851
|
+
}
|
|
4852
|
+
prev = curr;
|
|
4853
|
+
}
|
|
4854
|
+
return prev[n];
|
|
4855
|
+
}
|
|
4856
|
+
function suggest5(target, known) {
|
|
4857
|
+
let best;
|
|
4858
|
+
let bestScore = Infinity;
|
|
4193
4859
|
for (const c of known) {
|
|
4194
4860
|
const d = distance4(target, c);
|
|
4195
4861
|
if (d < bestScore) {
|
|
@@ -4206,17 +4872,17 @@ function list2(names) {
|
|
|
4206
4872
|
}
|
|
4207
4873
|
function indexDatasets(stack) {
|
|
4208
4874
|
const out = /* @__PURE__ */ new Map();
|
|
4209
|
-
for (const ds of
|
|
4210
|
-
const name =
|
|
4875
|
+
for (const ds of asArray28(stack.datasets)) {
|
|
4876
|
+
const name = strName10(ds.name);
|
|
4211
4877
|
if (!name) continue;
|
|
4212
4878
|
const dimensions = /* @__PURE__ */ new Set();
|
|
4213
|
-
for (const d of
|
|
4214
|
-
const n =
|
|
4879
|
+
for (const d of asArray28(ds.dimensions)) {
|
|
4880
|
+
const n = strName10(d.name);
|
|
4215
4881
|
if (n) dimensions.add(n);
|
|
4216
4882
|
}
|
|
4217
4883
|
const measures = /* @__PURE__ */ new Set();
|
|
4218
|
-
for (const m of
|
|
4219
|
-
const n =
|
|
4884
|
+
for (const m of asArray28(ds.measures)) {
|
|
4885
|
+
const n = strName10(m.name);
|
|
4220
4886
|
if (n) measures.add(n);
|
|
4221
4887
|
}
|
|
4222
4888
|
out.set(name, { dimensions, measures });
|
|
@@ -4294,75 +4960,75 @@ function validateChartBindings(stack) {
|
|
|
4294
4960
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
4295
4961
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
4296
4962
|
};
|
|
4297
|
-
const reports =
|
|
4963
|
+
const reports = asArray28(stack.reports);
|
|
4298
4964
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
4299
4965
|
const report = reports[ri];
|
|
4300
|
-
if (!
|
|
4301
|
-
const reportName =
|
|
4966
|
+
if (!isRec8(report)) continue;
|
|
4967
|
+
const reportName = strName10(report.name) ?? `#${ri}`;
|
|
4302
4968
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
4303
|
-
if (!
|
|
4969
|
+
if (!isRec8(chart)) return;
|
|
4304
4970
|
check({
|
|
4305
4971
|
dataset,
|
|
4306
4972
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
4307
4973
|
// it in lets the yAxis "declared but not selected" check work without
|
|
4308
4974
|
// reporting the selection itself twice.
|
|
4309
4975
|
values: { names: values, path: `${path}.values` },
|
|
4310
|
-
xAxis:
|
|
4311
|
-
yAxis:
|
|
4312
|
-
series:
|
|
4976
|
+
xAxis: strName10(chart.xAxis) ? { name: strName10(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
4977
|
+
yAxis: strName10(chart.yAxis) ? { name: strName10(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
4978
|
+
series: asArray28(chart.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
4313
4979
|
where,
|
|
4314
4980
|
path: `${path}.chart`
|
|
4315
4981
|
});
|
|
4316
4982
|
};
|
|
4317
4983
|
checkReportChart(
|
|
4318
4984
|
report.chart,
|
|
4319
|
-
|
|
4320
|
-
|
|
4985
|
+
strName10(report.dataset),
|
|
4986
|
+
strList3(report.values),
|
|
4321
4987
|
`report "${reportName}" \xB7 chart`,
|
|
4322
4988
|
`reports[${ri}]`
|
|
4323
4989
|
);
|
|
4324
4990
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
4325
4991
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
4326
4992
|
const block = blocks[bi];
|
|
4327
|
-
if (!
|
|
4993
|
+
if (!isRec8(block)) continue;
|
|
4328
4994
|
checkReportChart(
|
|
4329
4995
|
block.chart,
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
`report "${reportName}" \xB7 block "${
|
|
4996
|
+
strName10(block.dataset),
|
|
4997
|
+
strList3(block.values),
|
|
4998
|
+
`report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
|
|
4333
4999
|
`reports[${ri}].blocks[${bi}]`
|
|
4334
5000
|
);
|
|
4335
5001
|
}
|
|
4336
5002
|
}
|
|
4337
5003
|
const checkListChart = (container, where, path) => {
|
|
4338
|
-
if (!
|
|
5004
|
+
if (!isRec8(container)) return;
|
|
4339
5005
|
const chart = container.chart;
|
|
4340
|
-
if (!
|
|
5006
|
+
if (!isRec8(chart)) return;
|
|
4341
5007
|
check({
|
|
4342
|
-
dataset:
|
|
4343
|
-
dimensions: { names:
|
|
4344
|
-
values: { names:
|
|
5008
|
+
dataset: strName10(chart.dataset),
|
|
5009
|
+
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5010
|
+
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
4345
5011
|
where,
|
|
4346
5012
|
path: `${path}.chart`
|
|
4347
5013
|
});
|
|
4348
5014
|
};
|
|
4349
|
-
const views =
|
|
5015
|
+
const views = asArray28(stack.views);
|
|
4350
5016
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4351
5017
|
const view = views[vi];
|
|
4352
|
-
if (!
|
|
4353
|
-
const viewName =
|
|
5018
|
+
if (!isRec8(view)) continue;
|
|
5019
|
+
const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
|
|
4354
5020
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
4355
|
-
if (
|
|
5021
|
+
if (isRec8(view.listViews)) {
|
|
4356
5022
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
4357
5023
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
4358
5024
|
}
|
|
4359
5025
|
}
|
|
4360
5026
|
}
|
|
4361
|
-
const objects =
|
|
5027
|
+
const objects = asArray28(stack.objects);
|
|
4362
5028
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4363
5029
|
const obj = objects[oi];
|
|
4364
|
-
if (!
|
|
4365
|
-
const objName =
|
|
5030
|
+
if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
|
|
5031
|
+
const objName = strName10(obj.name) ?? `#${oi}`;
|
|
4366
5032
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
4367
5033
|
checkListChart(
|
|
4368
5034
|
lv,
|
|
@@ -4371,22 +5037,22 @@ function validateChartBindings(stack) {
|
|
|
4371
5037
|
);
|
|
4372
5038
|
}
|
|
4373
5039
|
}
|
|
4374
|
-
const pages =
|
|
5040
|
+
const pages = asArray28(stack.pages);
|
|
4375
5041
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4376
5042
|
const page = pages[pi];
|
|
4377
|
-
if (!
|
|
4378
|
-
const pageName =
|
|
5043
|
+
if (!isRec8(page)) continue;
|
|
5044
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
4379
5045
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4380
|
-
const props =
|
|
4381
|
-
if (!props || !
|
|
4382
|
-
const axisRefs =
|
|
4383
|
-
const seriesRefs =
|
|
5046
|
+
const props = isRec8(component.properties) ? component.properties : void 0;
|
|
5047
|
+
if (!props || !strName10(props.dataset)) continue;
|
|
5048
|
+
const axisRefs = asArray28(props.yAxis).map((a, ai) => ({ name: strName10(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
5049
|
+
const seriesRefs = asArray28(props.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
4384
5050
|
check({
|
|
4385
|
-
dataset:
|
|
4386
|
-
dimensions: { names:
|
|
4387
|
-
values: { names:
|
|
5051
|
+
dataset: strName10(props.dataset),
|
|
5052
|
+
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5053
|
+
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
4388
5054
|
series: [...axisRefs, ...seriesRefs],
|
|
4389
|
-
where: `page "${pageName}" \xB7 ${
|
|
5055
|
+
where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
|
|
4390
5056
|
path: `${path}.properties`
|
|
4391
5057
|
});
|
|
4392
5058
|
}
|
|
@@ -4398,7 +5064,7 @@ function validateChartBindings(stack) {
|
|
|
4398
5064
|
import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
|
|
4399
5065
|
|
|
4400
5066
|
// src/build-access-matrix.ts
|
|
4401
|
-
function
|
|
5067
|
+
function asArray29(v) {
|
|
4402
5068
|
if (Array.isArray(v)) return v;
|
|
4403
5069
|
if (v && typeof v === "object") {
|
|
4404
5070
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4409,13 +5075,13 @@ function buildAccessMatrix(stack) {
|
|
|
4409
5075
|
const entries = [];
|
|
4410
5076
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
4411
5077
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
4412
|
-
for (const obj of
|
|
5078
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4413
5079
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
4414
5080
|
if (!name) continue;
|
|
4415
5081
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
4416
5082
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
4417
5083
|
}
|
|
4418
|
-
for (const ps of
|
|
5084
|
+
for (const ps of asArray29(stack.permissions)) {
|
|
4419
5085
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
4420
5086
|
if (!psName) continue;
|
|
4421
5087
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -4488,34 +5154,34 @@ function diffAccessMatrix(before, after) {
|
|
|
4488
5154
|
|
|
4489
5155
|
// src/validate-nav-access.ts
|
|
4490
5156
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
4491
|
-
function
|
|
5157
|
+
function asArray30(v) {
|
|
4492
5158
|
if (Array.isArray(v)) return v;
|
|
4493
5159
|
if (v && typeof v === "object") {
|
|
4494
5160
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4495
5161
|
}
|
|
4496
5162
|
return [];
|
|
4497
5163
|
}
|
|
4498
|
-
function
|
|
5164
|
+
function strName11(v) {
|
|
4499
5165
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4500
5166
|
}
|
|
4501
5167
|
function collectNavExposures(stack) {
|
|
4502
5168
|
const out = [];
|
|
4503
|
-
const apps =
|
|
5169
|
+
const apps = asArray30(stack.apps);
|
|
4504
5170
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4505
5171
|
const app = apps[ai];
|
|
4506
5172
|
if (!app || typeof app !== "object") continue;
|
|
4507
|
-
const appName =
|
|
5173
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
4508
5174
|
const walk = (items, basePath) => {
|
|
4509
|
-
const navItems =
|
|
5175
|
+
const navItems = asArray30(items);
|
|
4510
5176
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4511
5177
|
const nav = navItems[ni];
|
|
4512
5178
|
if (!nav || typeof nav !== "object") continue;
|
|
4513
5179
|
const navPath = `${basePath}[${ni}]`;
|
|
4514
|
-
const objectName =
|
|
5180
|
+
const objectName = strName11(nav.objectName);
|
|
4515
5181
|
if (nav.type === "object" && objectName) {
|
|
4516
5182
|
out.push({
|
|
4517
5183
|
objectName,
|
|
4518
|
-
where: `app "${appName}" \xB7 nav "${
|
|
5184
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
|
|
4519
5185
|
path: `${navPath}.objectName`
|
|
4520
5186
|
});
|
|
4521
5187
|
}
|
|
@@ -4523,7 +5189,7 @@ function collectNavExposures(stack) {
|
|
|
4523
5189
|
}
|
|
4524
5190
|
};
|
|
4525
5191
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4526
|
-
const areas =
|
|
5192
|
+
const areas = asArray30(app.areas);
|
|
4527
5193
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4528
5194
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4529
5195
|
}
|
|
@@ -4533,13 +5199,13 @@ function collectNavExposures(stack) {
|
|
|
4533
5199
|
function validateNavAccess(stack) {
|
|
4534
5200
|
const findings = [];
|
|
4535
5201
|
if (!stack || typeof stack !== "object") return findings;
|
|
4536
|
-
const permissionSets =
|
|
5202
|
+
const permissionSets = asArray30(stack.permissions);
|
|
4537
5203
|
if (permissionSets.length === 0) return findings;
|
|
4538
5204
|
const exposures = collectNavExposures(stack);
|
|
4539
5205
|
if (exposures.length === 0) return findings;
|
|
4540
5206
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4541
|
-
for (const obj of
|
|
4542
|
-
const n =
|
|
5207
|
+
for (const obj of asArray30(stack.objects)) {
|
|
5208
|
+
const n = strName11(obj.name);
|
|
4543
5209
|
if (n) ownObjects.add(n);
|
|
4544
5210
|
}
|
|
4545
5211
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -4571,15 +5237,15 @@ function validateNavAccess(stack) {
|
|
|
4571
5237
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
4572
5238
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
4573
5239
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
4574
|
-
function
|
|
5240
|
+
function isRec9(v) {
|
|
4575
5241
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4576
5242
|
}
|
|
4577
|
-
function
|
|
5243
|
+
function asArray31(v) {
|
|
4578
5244
|
if (Array.isArray(v)) return v;
|
|
4579
|
-
if (
|
|
5245
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
|
|
4580
5246
|
return [];
|
|
4581
5247
|
}
|
|
4582
|
-
function
|
|
5248
|
+
function strName12(v) {
|
|
4583
5249
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4584
5250
|
}
|
|
4585
5251
|
function distance5(a, b) {
|
|
@@ -4637,20 +5303,20 @@ function collectViewRecord(view, factsFor) {
|
|
|
4637
5303
|
const addView = (objectName, name) => {
|
|
4638
5304
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
4639
5305
|
};
|
|
4640
|
-
const listBinding =
|
|
4641
|
-
if (
|
|
4642
|
-
addView(recordObject ?? listBinding,
|
|
5306
|
+
const listBinding = isRec9(view.list) ? bindingOf(view.list) : void 0;
|
|
5307
|
+
if (isRec9(view.list)) addView(listBinding, strName12(view.list.name));
|
|
5308
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
4643
5309
|
for (const key of ["listViews", "formViews"]) {
|
|
4644
5310
|
const container = view[key];
|
|
4645
|
-
if (!
|
|
5311
|
+
if (!isRec9(container)) continue;
|
|
4646
5312
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
4647
|
-
if (!
|
|
5313
|
+
if (!isRec9(sub)) continue;
|
|
4648
5314
|
const binding = bindingOf(sub) ?? listBinding;
|
|
4649
5315
|
addView(binding, subKey);
|
|
4650
|
-
addView(binding,
|
|
5316
|
+
addView(binding, strName12(sub.name));
|
|
4651
5317
|
if (binding) {
|
|
4652
|
-
for (const section of
|
|
4653
|
-
const sectionName =
|
|
5318
|
+
for (const section of asArray31(sub.sections)) {
|
|
5319
|
+
const sectionName = strName12(section.name);
|
|
4654
5320
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
4655
5321
|
}
|
|
4656
5322
|
}
|
|
@@ -4658,14 +5324,14 @@ function collectViewRecord(view, factsFor) {
|
|
|
4658
5324
|
}
|
|
4659
5325
|
const sectionBinding = recordObject ?? listBinding;
|
|
4660
5326
|
if (sectionBinding) {
|
|
4661
|
-
for (const section of
|
|
4662
|
-
const sectionName =
|
|
5327
|
+
for (const section of asArray31(view.sections)) {
|
|
5328
|
+
const sectionName = strName12(section.name);
|
|
4663
5329
|
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
4664
5330
|
}
|
|
4665
5331
|
}
|
|
4666
5332
|
}
|
|
4667
5333
|
function viewObjectName(view) {
|
|
4668
|
-
return
|
|
5334
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
|
|
4669
5335
|
}
|
|
4670
5336
|
function readOptions(field) {
|
|
4671
5337
|
const raw = field.options;
|
|
@@ -4677,14 +5343,14 @@ function readOptions(field) {
|
|
|
4677
5343
|
values.add(opt);
|
|
4678
5344
|
continue;
|
|
4679
5345
|
}
|
|
4680
|
-
if (!
|
|
4681
|
-
const value =
|
|
5346
|
+
if (!isRec9(opt)) continue;
|
|
5347
|
+
const value = strName12(opt.value);
|
|
4682
5348
|
if (!value) continue;
|
|
4683
5349
|
values.add(value);
|
|
4684
|
-
const label2 =
|
|
5350
|
+
const label2 = strName12(opt.label);
|
|
4685
5351
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
4686
5352
|
}
|
|
4687
|
-
} else if (
|
|
5353
|
+
} else if (isRec9(raw)) {
|
|
4688
5354
|
for (const [value, label2] of Object.entries(raw)) {
|
|
4689
5355
|
values.add(value);
|
|
4690
5356
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -4704,48 +5370,48 @@ function buildUniverse(stack) {
|
|
|
4704
5370
|
}
|
|
4705
5371
|
return facts;
|
|
4706
5372
|
};
|
|
4707
|
-
for (const obj of
|
|
4708
|
-
const objectName =
|
|
5373
|
+
for (const obj of asArray31(stack.objects)) {
|
|
5374
|
+
const objectName = strName12(obj.name);
|
|
4709
5375
|
if (!objectName) continue;
|
|
4710
5376
|
const facts = factsFor(objectName);
|
|
4711
|
-
for (const field of
|
|
4712
|
-
const fieldName =
|
|
5377
|
+
for (const field of asArray31(obj.fields)) {
|
|
5378
|
+
const fieldName = strName12(field.name);
|
|
4713
5379
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
4714
5380
|
}
|
|
4715
|
-
for (const action of
|
|
4716
|
-
const actionName =
|
|
5381
|
+
for (const action of asArray31(obj.actions)) {
|
|
5382
|
+
const actionName = strName12(action.name);
|
|
4717
5383
|
if (actionName) facts.actions.set(actionName, action);
|
|
4718
5384
|
}
|
|
4719
|
-
for (const view of
|
|
4720
|
-
collectViewRecord({ ...view, object:
|
|
5385
|
+
for (const view of asArray31(obj.views)) {
|
|
5386
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
4721
5387
|
}
|
|
4722
5388
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
4723
|
-
for (const group of
|
|
4724
|
-
const key =
|
|
5389
|
+
for (const group of asArray31(obj.fieldGroups)) {
|
|
5390
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
4725
5391
|
if (key) facts.sections.add(key);
|
|
4726
5392
|
}
|
|
4727
5393
|
}
|
|
4728
|
-
for (const view of
|
|
5394
|
+
for (const view of asArray31(stack.views)) {
|
|
4729
5395
|
collectViewRecord(view, factsFor);
|
|
4730
5396
|
}
|
|
4731
|
-
const pages =
|
|
5397
|
+
const pages = asArray31(stack.pages);
|
|
4732
5398
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4733
5399
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
4734
5400
|
if (!walked.objectName) continue;
|
|
4735
|
-
const props =
|
|
5401
|
+
const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
|
|
4736
5402
|
if (!props) continue;
|
|
4737
|
-
for (const section of
|
|
4738
|
-
const sectionName =
|
|
5403
|
+
for (const section of asArray31(props.sections)) {
|
|
5404
|
+
const sectionName = strName12(section.name);
|
|
4739
5405
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
4740
5406
|
}
|
|
4741
5407
|
}
|
|
4742
5408
|
}
|
|
4743
5409
|
const globalActions = /* @__PURE__ */ new Map();
|
|
4744
5410
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
4745
|
-
for (const action of
|
|
4746
|
-
const actionName =
|
|
5411
|
+
for (const action of asArray31(stack.actions)) {
|
|
5412
|
+
const actionName = strName12(action.name);
|
|
4747
5413
|
if (!actionName) continue;
|
|
4748
|
-
const owner =
|
|
5414
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
4749
5415
|
if (owner) {
|
|
4750
5416
|
factsFor(owner).actions.set(actionName, action);
|
|
4751
5417
|
actionOwners.set(actionName, owner);
|
|
@@ -4759,41 +5425,41 @@ function buildUniverse(stack) {
|
|
|
4759
5425
|
}
|
|
4760
5426
|
}
|
|
4761
5427
|
const apps = /* @__PURE__ */ new Map();
|
|
4762
|
-
for (const app of
|
|
4763
|
-
const appName =
|
|
5428
|
+
for (const app of asArray31(stack.apps)) {
|
|
5429
|
+
const appName = strName12(app.name);
|
|
4764
5430
|
if (!appName) continue;
|
|
4765
5431
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
4766
5432
|
const walkNav = (items) => {
|
|
4767
|
-
for (const item of
|
|
4768
|
-
const id =
|
|
5433
|
+
for (const item of asArray31(items)) {
|
|
5434
|
+
const id = strName12(item.id);
|
|
4769
5435
|
if (id) navIds.add(id);
|
|
4770
5436
|
if (item.children) walkNav(item.children);
|
|
4771
5437
|
}
|
|
4772
5438
|
};
|
|
4773
5439
|
walkNav(app.navigation);
|
|
4774
|
-
for (const area of
|
|
4775
|
-
const areaId =
|
|
5440
|
+
for (const area of asArray31(app.areas)) {
|
|
5441
|
+
const areaId = strName12(area.id);
|
|
4776
5442
|
if (areaId) navIds.add(areaId);
|
|
4777
5443
|
walkNav(area.navigation);
|
|
4778
5444
|
}
|
|
4779
5445
|
apps.set(appName, navIds);
|
|
4780
5446
|
}
|
|
4781
5447
|
const dashboards = /* @__PURE__ */ new Map();
|
|
4782
|
-
for (const dash of
|
|
4783
|
-
const dashName =
|
|
5448
|
+
for (const dash of asArray31(stack.dashboards)) {
|
|
5449
|
+
const dashName = strName12(dash.name);
|
|
4784
5450
|
if (!dashName) continue;
|
|
4785
5451
|
const widgets = /* @__PURE__ */ new Set();
|
|
4786
|
-
for (const widget of
|
|
4787
|
-
const id =
|
|
5452
|
+
for (const widget of asArray31(dash.widgets)) {
|
|
5453
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
4788
5454
|
if (id) widgets.add(id);
|
|
4789
5455
|
}
|
|
4790
5456
|
const actions = /* @__PURE__ */ new Set();
|
|
4791
5457
|
const headerActions = [
|
|
4792
|
-
...
|
|
4793
|
-
...
|
|
5458
|
+
...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
|
|
5459
|
+
...asArray31(dash.actions)
|
|
4794
5460
|
];
|
|
4795
5461
|
for (const action of headerActions) {
|
|
4796
|
-
const key =
|
|
5462
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
4797
5463
|
if (key) actions.add(key);
|
|
4798
5464
|
}
|
|
4799
5465
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -4805,7 +5471,7 @@ function localePath(bundleIndex, locale) {
|
|
|
4805
5471
|
}
|
|
4806
5472
|
function validateTranslationReferences(stack) {
|
|
4807
5473
|
const findings = [];
|
|
4808
|
-
if (!
|
|
5474
|
+
if (!isRec9(stack)) return findings;
|
|
4809
5475
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
4810
5476
|
if (bundles.length === 0) return findings;
|
|
4811
5477
|
const universe = buildUniverse(stack);
|
|
@@ -4814,13 +5480,13 @@ function validateTranslationReferences(stack) {
|
|
|
4814
5480
|
};
|
|
4815
5481
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
4816
5482
|
const bundle = bundles[bi];
|
|
4817
|
-
if (!
|
|
5483
|
+
if (!isRec9(bundle)) continue;
|
|
4818
5484
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
4819
|
-
if (!
|
|
5485
|
+
if (!isRec9(rawData)) continue;
|
|
4820
5486
|
const base = localePath(bi, locale);
|
|
4821
5487
|
const inLocale = `locale "${locale}"`;
|
|
4822
5488
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
4823
|
-
if (!
|
|
5489
|
+
if (!isRec9(rawNode)) continue;
|
|
4824
5490
|
const objPath = `${base}.objects.${objectName}`;
|
|
4825
5491
|
const facts = universe.objects.get(objectName);
|
|
4826
5492
|
if (!facts) {
|
|
@@ -4846,7 +5512,7 @@ function validateTranslationReferences(stack) {
|
|
|
4846
5512
|
);
|
|
4847
5513
|
continue;
|
|
4848
5514
|
}
|
|
4849
|
-
if (!
|
|
5515
|
+
if (!isRec9(rawField)) continue;
|
|
4850
5516
|
checkOptionKeys(findings, {
|
|
4851
5517
|
optionMap: rawField.options,
|
|
4852
5518
|
field,
|
|
@@ -4928,7 +5594,7 @@ function validateTranslationReferences(stack) {
|
|
|
4928
5594
|
);
|
|
4929
5595
|
continue;
|
|
4930
5596
|
}
|
|
4931
|
-
if (!
|
|
5597
|
+
if (!isRec9(rawApp)) continue;
|
|
4932
5598
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
4933
5599
|
if (navIds.has(navId)) continue;
|
|
4934
5600
|
orphan(
|
|
@@ -4951,7 +5617,7 @@ function validateTranslationReferences(stack) {
|
|
|
4951
5617
|
);
|
|
4952
5618
|
continue;
|
|
4953
5619
|
}
|
|
4954
|
-
if (!
|
|
5620
|
+
if (!isRec9(rawDash)) continue;
|
|
4955
5621
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
4956
5622
|
if (dash.widgets.has(widgetId)) continue;
|
|
4957
5623
|
orphan(
|
|
@@ -4976,7 +5642,7 @@ function validateTranslationReferences(stack) {
|
|
|
4976
5642
|
return findings;
|
|
4977
5643
|
}
|
|
4978
5644
|
function asRecord(v) {
|
|
4979
|
-
return
|
|
5645
|
+
return isRec9(v) ? v : {};
|
|
4980
5646
|
}
|
|
4981
5647
|
function checkOptionKeys(findings, ctx) {
|
|
4982
5648
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -4988,7 +5654,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
4988
5654
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
4989
5655
|
where: ctx.where,
|
|
4990
5656
|
path: ctx.path,
|
|
4991
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
5657
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
4992
5658
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
4993
5659
|
});
|
|
4994
5660
|
return;
|
|
@@ -5007,11 +5673,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5007
5673
|
}
|
|
5008
5674
|
}
|
|
5009
5675
|
function checkActionParams(findings, ctx) {
|
|
5010
|
-
const rawParams = Object.keys(asRecord(
|
|
5676
|
+
const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
5011
5677
|
if (rawParams.length === 0) return;
|
|
5012
5678
|
const declared = /* @__PURE__ */ new Set();
|
|
5013
|
-
for (const param of
|
|
5014
|
-
const name =
|
|
5679
|
+
for (const param of asArray31(ctx.action.params)) {
|
|
5680
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
5015
5681
|
if (name) declared.add(name);
|
|
5016
5682
|
}
|
|
5017
5683
|
for (const paramName of rawParams) {
|
|
@@ -5029,14 +5695,14 @@ function checkActionParams(findings, ctx) {
|
|
|
5029
5695
|
|
|
5030
5696
|
// src/validate-ai-surface-affinity.ts
|
|
5031
5697
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
5032
|
-
function
|
|
5698
|
+
function asArray32(v) {
|
|
5033
5699
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5034
5700
|
if (v && typeof v === "object") {
|
|
5035
5701
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5036
5702
|
}
|
|
5037
5703
|
return [];
|
|
5038
5704
|
}
|
|
5039
|
-
function
|
|
5705
|
+
function strName13(v) {
|
|
5040
5706
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5041
5707
|
}
|
|
5042
5708
|
function surfaceOf(v) {
|
|
@@ -5046,18 +5712,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5046
5712
|
const findings = [];
|
|
5047
5713
|
if (!stack || typeof stack !== "object") return findings;
|
|
5048
5714
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
5049
|
-
for (const skill of
|
|
5050
|
-
const n =
|
|
5715
|
+
for (const skill of asArray32(stack.skills)) {
|
|
5716
|
+
const n = strName13(skill.name);
|
|
5051
5717
|
if (n) skillsByName.set(n, skill);
|
|
5052
5718
|
}
|
|
5053
|
-
const agents =
|
|
5719
|
+
const agents = asArray32(stack.agents);
|
|
5054
5720
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5055
5721
|
const agent = agents[ai];
|
|
5056
|
-
const agentName =
|
|
5722
|
+
const agentName = strName13(agent.name) ?? `#${ai}`;
|
|
5057
5723
|
const agentSurface = surfaceOf(agent.surface);
|
|
5058
5724
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
5059
5725
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
5060
|
-
const ref =
|
|
5726
|
+
const ref = strName13(skillRefs[si]);
|
|
5061
5727
|
if (!ref) continue;
|
|
5062
5728
|
const skill = skillsByName.get(ref);
|
|
5063
5729
|
if (!skill) continue;
|
|
@@ -5079,14 +5745,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5079
5745
|
// src/validate-ai-tool-references.ts
|
|
5080
5746
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
5081
5747
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
5082
|
-
function
|
|
5748
|
+
function asArray33(v) {
|
|
5083
5749
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5084
5750
|
if (v && typeof v === "object") {
|
|
5085
5751
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5086
5752
|
}
|
|
5087
5753
|
return [];
|
|
5088
5754
|
}
|
|
5089
|
-
function
|
|
5755
|
+
function strName14(v) {
|
|
5090
5756
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5091
5757
|
}
|
|
5092
5758
|
function distance6(a, b) {
|
|
@@ -5127,26 +5793,26 @@ function materialisesAsTool(action) {
|
|
|
5127
5793
|
if (!ai || typeof ai !== "object") return false;
|
|
5128
5794
|
const aiRec = ai;
|
|
5129
5795
|
if (aiRec.exposed !== true) return false;
|
|
5130
|
-
if (!
|
|
5131
|
-
const type =
|
|
5796
|
+
if (!strName14(aiRec.description)) return false;
|
|
5797
|
+
const type = strName14(action.type);
|
|
5132
5798
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
5133
5799
|
if (type === "script") return Boolean(action.target || action.body);
|
|
5134
5800
|
return Boolean(action.target);
|
|
5135
5801
|
}
|
|
5136
5802
|
function collectToolUniverse(stack) {
|
|
5137
5803
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
5138
|
-
for (const tool of
|
|
5139
|
-
const n =
|
|
5804
|
+
for (const tool of asArray33(stack.tools)) {
|
|
5805
|
+
const n = strName14(tool.name);
|
|
5140
5806
|
if (n) universe.add(n);
|
|
5141
5807
|
}
|
|
5142
5808
|
const addActionFamily = (actions) => {
|
|
5143
|
-
for (const action of
|
|
5144
|
-
const n =
|
|
5809
|
+
for (const action of asArray33(actions)) {
|
|
5810
|
+
const n = strName14(action.name);
|
|
5145
5811
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
5146
5812
|
}
|
|
5147
5813
|
};
|
|
5148
5814
|
addActionFamily(stack.actions);
|
|
5149
|
-
for (const obj of
|
|
5815
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5150
5816
|
addActionFamily(obj.actions);
|
|
5151
5817
|
}
|
|
5152
5818
|
return universe;
|
|
@@ -5154,13 +5820,13 @@ function collectToolUniverse(stack) {
|
|
|
5154
5820
|
function collectUnexposedActionNames(stack) {
|
|
5155
5821
|
const names = /* @__PURE__ */ new Set();
|
|
5156
5822
|
const scan = (actions) => {
|
|
5157
|
-
for (const action of
|
|
5158
|
-
const n =
|
|
5823
|
+
for (const action of asArray33(actions)) {
|
|
5824
|
+
const n = strName14(action.name);
|
|
5159
5825
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
5160
5826
|
}
|
|
5161
5827
|
};
|
|
5162
5828
|
scan(stack.actions);
|
|
5163
|
-
for (const obj of
|
|
5829
|
+
for (const obj of asArray33(stack.objects)) scan(obj.actions);
|
|
5164
5830
|
return names;
|
|
5165
5831
|
}
|
|
5166
5832
|
function validateAiToolReferences(stack) {
|
|
@@ -5178,13 +5844,13 @@ function validateAiToolReferences(stack) {
|
|
|
5178
5844
|
}
|
|
5179
5845
|
return universe.has(ref);
|
|
5180
5846
|
};
|
|
5181
|
-
const skills =
|
|
5847
|
+
const skills = asArray33(stack.skills);
|
|
5182
5848
|
for (let si = 0; si < skills.length; si++) {
|
|
5183
5849
|
const skill = skills[si];
|
|
5184
|
-
const skillName =
|
|
5850
|
+
const skillName = strName14(skill.name) ?? `#${si}`;
|
|
5185
5851
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
5186
5852
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
5187
|
-
const ref =
|
|
5853
|
+
const ref = strName14(refs[ti]);
|
|
5188
5854
|
if (!ref || resolves(ref)) continue;
|
|
5189
5855
|
const isPattern = ref.endsWith("*");
|
|
5190
5856
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -5203,24 +5869,24 @@ function validateAiToolReferences(stack) {
|
|
|
5203
5869
|
|
|
5204
5870
|
// src/validate-ai-agent-authoring.ts
|
|
5205
5871
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
5206
|
-
function
|
|
5872
|
+
function asArray34(v) {
|
|
5207
5873
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5208
5874
|
if (v && typeof v === "object") {
|
|
5209
5875
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5210
5876
|
}
|
|
5211
5877
|
return [];
|
|
5212
5878
|
}
|
|
5213
|
-
function
|
|
5879
|
+
function strName15(v) {
|
|
5214
5880
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5215
5881
|
}
|
|
5216
5882
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
5217
5883
|
function validateAiAgentAuthoring(stack) {
|
|
5218
5884
|
const findings = [];
|
|
5219
5885
|
if (!stack || typeof stack !== "object") return findings;
|
|
5220
|
-
const agents =
|
|
5886
|
+
const agents = asArray34(stack.agents);
|
|
5221
5887
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5222
5888
|
const agent = agents[ai];
|
|
5223
|
-
const name =
|
|
5889
|
+
const name = strName15(agent.name) ?? `#${ai}`;
|
|
5224
5890
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
5225
5891
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
5226
5892
|
findings.push({
|
|
@@ -5324,24 +5990,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
5324
5990
|
"owner",
|
|
5325
5991
|
"record_type"
|
|
5326
5992
|
]);
|
|
5327
|
-
var
|
|
5328
|
-
function
|
|
5329
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5330
|
-
if (
|
|
5993
|
+
var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5994
|
+
function asArray35(v) {
|
|
5995
|
+
if (Array.isArray(v)) return v.filter((x) => isRec10(x));
|
|
5996
|
+
if (isRec10(v)) {
|
|
5331
5997
|
return Object.entries(v).map(([name, def]) => ({
|
|
5332
5998
|
name,
|
|
5333
|
-
...
|
|
5999
|
+
...isRec10(def) ? def : {}
|
|
5334
6000
|
}));
|
|
5335
6001
|
}
|
|
5336
6002
|
return [];
|
|
5337
6003
|
}
|
|
5338
6004
|
function indexObjectFields2(stack) {
|
|
5339
6005
|
const out = /* @__PURE__ */ new Map();
|
|
5340
|
-
for (const obj of
|
|
6006
|
+
for (const obj of asArray35(stack.objects)) {
|
|
5341
6007
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
5342
6008
|
if (!name) continue;
|
|
5343
6009
|
const names = /* @__PURE__ */ new Set();
|
|
5344
|
-
for (const f of
|
|
6010
|
+
for (const f of asArray35(obj.fields)) {
|
|
5345
6011
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
5346
6012
|
}
|
|
5347
6013
|
out.set(name, names);
|
|
@@ -5471,12 +6137,12 @@ ${source}
|
|
|
5471
6137
|
}
|
|
5472
6138
|
function validateHookBodyWrites(stack) {
|
|
5473
6139
|
const findings = [];
|
|
5474
|
-
const hooks =
|
|
6140
|
+
const hooks = asArray35(stack.hooks);
|
|
5475
6141
|
if (hooks.length === 0) return findings;
|
|
5476
6142
|
let objectFields = null;
|
|
5477
6143
|
hooks.forEach((hook, hookIndex) => {
|
|
5478
6144
|
const body = hook.body;
|
|
5479
|
-
if (!
|
|
6145
|
+
if (!isRec10(body) || body.language !== "js") return;
|
|
5480
6146
|
const source = body.source;
|
|
5481
6147
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
5482
6148
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -5557,13 +6223,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
5557
6223
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
5558
6224
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
5559
6225
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
5560
|
-
var
|
|
5561
|
-
function
|
|
5562
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5563
|
-
if (
|
|
6226
|
+
var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6227
|
+
function asArray36(v) {
|
|
6228
|
+
if (Array.isArray(v)) return v.filter((x) => isRec11(x));
|
|
6229
|
+
if (isRec11(v)) {
|
|
5564
6230
|
return Object.entries(v).map(([name, def]) => ({
|
|
5565
6231
|
name,
|
|
5566
|
-
...
|
|
6232
|
+
...isRec11(def) ? def : {}
|
|
5567
6233
|
}));
|
|
5568
6234
|
}
|
|
5569
6235
|
return [];
|
|
@@ -5577,9 +6243,11 @@ function collectActionBodies(stack) {
|
|
|
5577
6243
|
const sites = [];
|
|
5578
6244
|
const seen = /* @__PURE__ */ new Set();
|
|
5579
6245
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
5580
|
-
|
|
6246
|
+
asArray36(actions).forEach((action, index) => {
|
|
6247
|
+
const type = typeof action.type === "string" ? action.type : "script";
|
|
6248
|
+
if (type !== "script") return;
|
|
5581
6249
|
const body = action.body;
|
|
5582
|
-
if (!
|
|
6250
|
+
if (!isRec11(body) || body.language !== "js") return;
|
|
5583
6251
|
const source = body.source;
|
|
5584
6252
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
5585
6253
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -5590,7 +6258,7 @@ function collectActionBodies(stack) {
|
|
|
5590
6258
|
});
|
|
5591
6259
|
};
|
|
5592
6260
|
collect(stack.actions, "actions");
|
|
5593
|
-
|
|
6261
|
+
asArray36(stack.objects).forEach((obj, objIndex) => {
|
|
5594
6262
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
5595
6263
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
5596
6264
|
});
|
|
@@ -5598,7 +6266,7 @@ function collectActionBodies(stack) {
|
|
|
5598
6266
|
}
|
|
5599
6267
|
function validateActionBodyWrites(stack) {
|
|
5600
6268
|
const findings = [];
|
|
5601
|
-
if (!
|
|
6269
|
+
if (!isRec11(stack)) return findings;
|
|
5602
6270
|
const sites = collectActionBodies(stack);
|
|
5603
6271
|
if (sites.length === 0) return findings;
|
|
5604
6272
|
let objectFields = null;
|
|
@@ -5657,13 +6325,13 @@ import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSu
|
|
|
5657
6325
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5658
6326
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
5659
6327
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
5660
|
-
var
|
|
5661
|
-
function
|
|
5662
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5663
|
-
if (
|
|
6328
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6329
|
+
function asArray37(v) {
|
|
6330
|
+
if (Array.isArray(v)) return v.filter((x) => isRec12(x));
|
|
6331
|
+
if (isRec12(v)) {
|
|
5664
6332
|
return Object.entries(v).map(([name, def]) => ({
|
|
5665
6333
|
name,
|
|
5666
|
-
...
|
|
6334
|
+
...isRec12(def) ? def : {}
|
|
5667
6335
|
}));
|
|
5668
6336
|
}
|
|
5669
6337
|
return [];
|
|
@@ -5676,8 +6344,8 @@ function readLiteralObjectName2(config) {
|
|
|
5676
6344
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
5677
6345
|
function validateFlowNodeWrites(stack) {
|
|
5678
6346
|
const findings = [];
|
|
5679
|
-
if (!
|
|
5680
|
-
const flows =
|
|
6347
|
+
if (!isRec12(stack)) return findings;
|
|
6348
|
+
const flows = asArray37(stack.flows);
|
|
5681
6349
|
if (flows.length === 0) return findings;
|
|
5682
6350
|
let objectFields = null;
|
|
5683
6351
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -5685,10 +6353,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
5685
6353
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
5686
6354
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
5687
6355
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
5688
|
-
const config =
|
|
6356
|
+
const config = isRec12(node.config) ? node.config : void 0;
|
|
5689
6357
|
if (!config) return;
|
|
5690
6358
|
const fields = config.fields;
|
|
5691
|
-
if (!
|
|
6359
|
+
if (!isRec12(fields)) return;
|
|
5692
6360
|
const written = Object.keys(fields);
|
|
5693
6361
|
if (written.length === 0) return;
|
|
5694
6362
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -5727,6 +6395,13 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5727
6395
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5728
6396
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
5729
6397
|
{ name: "validateNavAccess", run: validateNavAccess },
|
|
6398
|
+
// Nav targets that are NOT object names — page/report/dashboard. Restores the
|
|
6399
|
+
// coverage `defineStack`'s own cross-reference block switches off whenever the
|
|
6400
|
+
// stack declares none of that collection (`pageNames.size > 0 && …`), which is
|
|
6401
|
+
// exactly the state a stack is in when the target was never written.
|
|
6402
|
+
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
6403
|
+
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
6404
|
+
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5730
6405
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
5731
6406
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
5732
6407
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
@@ -5774,12 +6449,20 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5774
6449
|
{ name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
|
|
5775
6450
|
// The `kind:'react'` page surface. Every prop a react block binds BY FIELD
|
|
5776
6451
|
// NAME is resolved against the object it names (#4340) — `<ListView columns>`,
|
|
5777
|
-
// `<ObjectForm fields>`,
|
|
6452
|
+
// `<ObjectForm fields>`, `<Block type="element:…">` through the SAME
|
|
5778
6453
|
// `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
|
|
5779
6454
|
// over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
|
|
5780
6455
|
// `searchableFields` (#4329). Squarely the charter's question, on the surface
|
|
5781
6456
|
// where it had no answer at all.
|
|
5782
6457
|
//
|
|
6458
|
+
// It also carries `react-block-needs-record-context` (#4413) — a BINDING
|
|
6459
|
+
// question rather than a resolution one: the `record:*` family reads its
|
|
6460
|
+
// record from a record page's context, so on THIS surface the binding does
|
|
6461
|
+
// not exist at all and the props the contract published for it were read by
|
|
6462
|
+
// no renderer. This rule used to resolve those props' field names against
|
|
6463
|
+
// the object they named — lint standing guard over a binding that never ran.
|
|
6464
|
+
// It rejects the blocks now, out of the same parse.
|
|
6465
|
+
//
|
|
5783
6466
|
// It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
|
|
5784
6467
|
// accepted a react page whose every field binding was stale — including the
|
|
5785
6468
|
// gating ones (a missing required binding, a filter position naming no field:
|
|
@@ -5803,12 +6486,1494 @@ function validateReferenceIntegrity(stack) {
|
|
|
5803
6486
|
}
|
|
5804
6487
|
return findings;
|
|
5805
6488
|
}
|
|
6489
|
+
|
|
6490
|
+
// src/lint-flow-patterns.ts
|
|
6491
|
+
function asArray38(v) {
|
|
6492
|
+
if (Array.isArray(v)) return v;
|
|
6493
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6494
|
+
return [];
|
|
6495
|
+
}
|
|
6496
|
+
function conditionSource(raw) {
|
|
6497
|
+
if (typeof raw === "string") return raw;
|
|
6498
|
+
if (raw && typeof raw === "object" && typeof raw.source === "string") return raw.source;
|
|
6499
|
+
return "";
|
|
6500
|
+
}
|
|
6501
|
+
var TIME_FNS = "daysFromNow|daysAgo|today|now|date|datetime";
|
|
6502
|
+
var TIME_FN_RE = new RegExp(`\\b(?:${TIME_FNS})\\s*\\(`);
|
|
6503
|
+
var DATE_EQ = new RegExp(
|
|
6504
|
+
`(?:(?:${TIME_FNS})\\s*\\([^)]*\\)\\s*(?:==|!=))|(?:(?:==|!=)\\s*(?:${TIME_FNS})\\s*\\()`
|
|
6505
|
+
);
|
|
6506
|
+
var FLOW_TIME_RELATIVE_ANTIPATTERN = "flow-time-relative-antipattern";
|
|
6507
|
+
var FLOW_DATE_EQUALITY_FILTER = "flow-date-equality-filter";
|
|
6508
|
+
var FLOW_PHANTOM_AGGREGATION = "flow-phantom-aggregation";
|
|
6509
|
+
var FLOW_DOUBLE_BRACE_INTERP = "flow-double-brace-interpolation";
|
|
6510
|
+
var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
|
|
6511
|
+
var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
|
|
6512
|
+
var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
|
|
6513
|
+
var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
|
|
6514
|
+
var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
|
|
6515
|
+
var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
|
|
6516
|
+
var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
|
|
6517
|
+
var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
|
|
6518
|
+
var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
|
|
6519
|
+
var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
|
|
6520
|
+
var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
|
|
6521
|
+
var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6522
|
+
"decision",
|
|
6523
|
+
"assignment",
|
|
6524
|
+
"loop",
|
|
6525
|
+
"parallel",
|
|
6526
|
+
"try_catch",
|
|
6527
|
+
"create_record",
|
|
6528
|
+
"update_record",
|
|
6529
|
+
"delete_record",
|
|
6530
|
+
"get_record",
|
|
6531
|
+
"http",
|
|
6532
|
+
"notify",
|
|
6533
|
+
"script",
|
|
6534
|
+
"screen",
|
|
6535
|
+
"wait",
|
|
6536
|
+
"subflow",
|
|
6537
|
+
"map",
|
|
6538
|
+
"connector_action",
|
|
6539
|
+
"approval",
|
|
6540
|
+
"end"
|
|
6541
|
+
]);
|
|
6542
|
+
var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
|
|
6543
|
+
var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
|
|
6544
|
+
var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
|
|
6545
|
+
function isScheduleTriggered(flow, startCfg) {
|
|
6546
|
+
if (flow.type === "schedule") return true;
|
|
6547
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "schedule") return true;
|
|
6548
|
+
return startCfg.schedule != null;
|
|
6549
|
+
}
|
|
6550
|
+
function userLessTriggerKind(flow, startCfg) {
|
|
6551
|
+
if (isScheduleTriggered(flow, startCfg)) return "schedule";
|
|
6552
|
+
if (startCfg.timeRelative != null) return "time-relative";
|
|
6553
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "time_relative") return "time-relative";
|
|
6554
|
+
if (flow.type === "api") return "api";
|
|
6555
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "api") return "api";
|
|
6556
|
+
return null;
|
|
6557
|
+
}
|
|
6558
|
+
var PHANTOM_AGG_KEYS = /* @__PURE__ */ new Set(["aggregations", "aggregate", "groupBy", "rollup", "having"]);
|
|
6559
|
+
function celTimeSource(v) {
|
|
6560
|
+
if (v && typeof v === "object" && v.dialect === "cel") {
|
|
6561
|
+
const src = v.source;
|
|
6562
|
+
if (typeof src === "string" && TIME_FN_RE.test(src)) return src;
|
|
6563
|
+
}
|
|
6564
|
+
return null;
|
|
6565
|
+
}
|
|
6566
|
+
var RANGE_OPS = /* @__PURE__ */ new Set(["$gte", "$gt", "$lte", "$lt", "$ne"]);
|
|
6567
|
+
function scanFilterForDateEquality(filter, where, findings) {
|
|
6568
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return;
|
|
6569
|
+
for (const [key, val] of Object.entries(filter)) {
|
|
6570
|
+
if (key === "$or" || key === "$and") {
|
|
6571
|
+
if (Array.isArray(val)) for (const sub of val) scanFilterForDateEquality(sub, where, findings);
|
|
6572
|
+
continue;
|
|
6573
|
+
}
|
|
6574
|
+
const direct = celTimeSource(val);
|
|
6575
|
+
let hit = direct ? { op: "==", src: direct } : null;
|
|
6576
|
+
if (!hit && val && typeof val === "object" && val.dialect !== "cel") {
|
|
6577
|
+
for (const [op, operand] of Object.entries(val)) {
|
|
6578
|
+
if (RANGE_OPS.has(op)) continue;
|
|
6579
|
+
if (op === "$eq") {
|
|
6580
|
+
const s = celTimeSource(operand);
|
|
6581
|
+
if (s) {
|
|
6582
|
+
hit = { op: "$eq", src: s };
|
|
6583
|
+
break;
|
|
6584
|
+
}
|
|
6585
|
+
} else if (op === "$in" && Array.isArray(operand)) {
|
|
6586
|
+
for (const item of operand) {
|
|
6587
|
+
const s = celTimeSource(item);
|
|
6588
|
+
if (s) {
|
|
6589
|
+
hit = { op: "$in", src: s };
|
|
6590
|
+
break;
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
if (hit) break;
|
|
6594
|
+
}
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
if (hit) {
|
|
6598
|
+
findings.push({
|
|
6599
|
+
where,
|
|
6600
|
+
message: `filter matches \`${key}\` by ${hit.op} against a time value (\`${hit.src}\`) \u2014 a date field carries a time component, so exact equality against \`${hit.src}\` (re-computed each run) silently matches nothing.`,
|
|
6601
|
+
hint: `Use a one-day window instead: \`${key}: { $gte: daysFromNow(N), $lt: daysFromNow(N+1) }\` (wrap multiple tiers in \`$or\`). The abutting windows tile the timeline so each row matches exactly once. (#1874)`,
|
|
6602
|
+
rule: FLOW_DATE_EQUALITY_FILTER
|
|
6603
|
+
});
|
|
6604
|
+
}
|
|
6605
|
+
}
|
|
6606
|
+
}
|
|
6607
|
+
var DOUBLE_BRACE = /\{\{\s*[\w$][\w$.\s]*\}\}/;
|
|
6608
|
+
var BARE_DOLLAR_REF = /(?:^|[^{])\$[A-Za-z_]\w*\.[A-Za-z_]/;
|
|
6609
|
+
var CEL_KEYS = /* @__PURE__ */ new Set(["condition", "expression", "conditions"]);
|
|
6610
|
+
function collectTemplateStrings(value, key, out) {
|
|
6611
|
+
if (key && CEL_KEYS.has(key)) return;
|
|
6612
|
+
if (typeof value === "string") {
|
|
6613
|
+
out.push(value);
|
|
6614
|
+
return;
|
|
6615
|
+
}
|
|
6616
|
+
if (Array.isArray(value)) {
|
|
6617
|
+
for (const v of value) collectTemplateStrings(v, key, out);
|
|
6618
|
+
return;
|
|
6619
|
+
}
|
|
6620
|
+
if (value && typeof value === "object") {
|
|
6621
|
+
for (const [k, v] of Object.entries(value)) collectTemplateStrings(v, k, out);
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
function edgeLabelOf(e) {
|
|
6625
|
+
return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6626
|
+
}
|
|
6627
|
+
function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
|
|
6628
|
+
const typeById = /* @__PURE__ */ new Map();
|
|
6629
|
+
for (const n of nodes) {
|
|
6630
|
+
if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
|
|
6631
|
+
}
|
|
6632
|
+
for (const e of edges) {
|
|
6633
|
+
const label2 = typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6634
|
+
if (!ERROR_LABELS.has(label2)) continue;
|
|
6635
|
+
if (e.type === "fault") continue;
|
|
6636
|
+
if (e.condition) continue;
|
|
6637
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6638
|
+
if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
|
|
6639
|
+
findings.push({
|
|
6640
|
+
where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6641
|
+
message: `edge is labelled '${String(e.label)}' but its type is '${String(e.type ?? "default")}', not 'fault' \u2014 so it is an ORDINARY out-edge. Unconditional out-edges all run in parallel, so '${String(e.target)}' executes on every SUCCESSFUL run of '${src}' and never on a failure. The error path the label describes does not exist, and the run still aborts when '${src}' fails.`,
|
|
6642
|
+
hint: `Add \`type: 'fault'\` to this edge. Only runtime failures route \u2014 a guard refusal (a filter token that resolved to nothing, a missing required config key, an unscoped run) stays fatal by design and must be fixed in the metadata, not handled. (#3863)`,
|
|
6643
|
+
rule: FLOW_ERROR_LABEL_NOT_FAULT
|
|
6644
|
+
});
|
|
6645
|
+
}
|
|
6646
|
+
}
|
|
6647
|
+
function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
6648
|
+
const outEdgesBySource = /* @__PURE__ */ new Map();
|
|
6649
|
+
for (const e of edges) {
|
|
6650
|
+
if (e.type === "fault") continue;
|
|
6651
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6652
|
+
if (!src) continue;
|
|
6653
|
+
if (!outEdgesBySource.has(src)) outEdgesBySource.set(src, []);
|
|
6654
|
+
outEdgesBySource.get(src).push(e);
|
|
6655
|
+
}
|
|
6656
|
+
for (const [src, outs] of outEdgesBySource) {
|
|
6657
|
+
for (const e of outs) {
|
|
6658
|
+
if (e.isDefault === true && e.condition) {
|
|
6659
|
+
findings.push({
|
|
6660
|
+
where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6661
|
+
message: `edge sets \`isDefault: true\` AND a \`condition\` \u2014 contradictory. \`isDefault\` means "take this edge when NO sibling condition matched"; a condition makes it an ordinary guarded branch. The condition wins and the default marker routes nothing.`,
|
|
6662
|
+
hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
|
|
6663
|
+
rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
6664
|
+
// Gating: the two keys contradict, the condition always wins, and the
|
|
6665
|
+
// marker never routes. No reading makes it do what it says.
|
|
6666
|
+
severity: "error"
|
|
6667
|
+
});
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
|
|
6671
|
+
if (defaults.length > 1) {
|
|
6672
|
+
findings.push({
|
|
6673
|
+
where: `flow '${flowName}' \xB7 node '${src}'`,
|
|
6674
|
+
message: `${defaults.length} out-edges are marked \`isDefault: true\` (${defaults.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 a node has at most ONE default path. All of them are traversed together when no condition matches, which is a parallel fan-out, not an "otherwise".`,
|
|
6675
|
+
hint: `Keep \`isDefault: true\` on the single fallback edge and give the others a \`condition\` (or leave them unconditional if the fan-out really is intended). (#4414)`,
|
|
6676
|
+
rule: FLOW_MULTIPLE_DEFAULT_EDGES
|
|
6677
|
+
});
|
|
6678
|
+
}
|
|
6679
|
+
}
|
|
6680
|
+
for (const node of nodes) {
|
|
6681
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
6682
|
+
if (!INERT_CONDITION_NODE_TYPES.has(nodeType)) continue;
|
|
6683
|
+
const cfg = node.config ?? {};
|
|
6684
|
+
if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
|
|
6685
|
+
findings.push({
|
|
6686
|
+
where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
6687
|
+
message: `\`config.condition\` is set but nothing reads it \u2014 the key is the trigger gate on a \`start\` node and is ignored on every other node type, so this predicate never gates anything. (It is still parse-validated at registration, which is why a malformed one is caught and an inert one is not.)`,
|
|
6688
|
+
hint: nodeType === "decision" ? `Branching lives on the OUT-EDGES: give each branch its own \`condition\` and mark the fallback \`isDefault: true\`. If the edges already carry the predicate, delete this copy. (#4414)` : `Delete it, or move the predicate to the incoming edge's \`condition\` if this step was meant to be conditional. (#4414)`,
|
|
6689
|
+
rule: FLOW_INERT_NODE_CONDITION
|
|
6690
|
+
});
|
|
6691
|
+
}
|
|
6692
|
+
for (const node of nodes) {
|
|
6693
|
+
if (node.type !== "decision") continue;
|
|
6694
|
+
const nid = typeof node.id === "string" ? node.id : "";
|
|
6695
|
+
if (!nid) continue;
|
|
6696
|
+
const outs = outEdgesBySource.get(nid) ?? [];
|
|
6697
|
+
if (outs.length === 0) continue;
|
|
6698
|
+
const cfg = node.config ?? {};
|
|
6699
|
+
const declaredLabels = new Set(
|
|
6700
|
+
(Array.isArray(cfg.conditions) ? cfg.conditions : []).map((c) => typeof c?.label === "string" ? c.label.trim().toLowerCase() : "").filter(Boolean)
|
|
6701
|
+
);
|
|
6702
|
+
const edgeLabels = new Set(outs.map(edgeLabelOf).filter(Boolean));
|
|
6703
|
+
const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
|
|
6704
|
+
if (unclaimed.length > 0) {
|
|
6705
|
+
findings.push({
|
|
6706
|
+
where: `flow '${flowName}' \xB7 decision '${nid}'`,
|
|
6707
|
+
message: `declares branch label(s) ${unclaimed.map((l) => `'${l}'`).join(", ")} that no out-edge carries \u2014 out-edge labels are [${[...edgeLabels].map((l) => `'${l}'`).join(", ") || "none"}]. Traversal cannot honour a label nothing claims, so it falls back to considering EVERY out-edge and the branch the decision computed is ignored.`,
|
|
6708
|
+
hint: `Make an out-edge's \`label\` match the declared branch exactly, or drop \`config.conditions\` and branch on the edges instead (\`condition\` per branch + \`isDefault: true\` on the fallback) \u2014 one mechanism per decision, never both. (#4414)`,
|
|
6709
|
+
rule: FLOW_BRANCH_LABEL_UNMATCHED,
|
|
6710
|
+
// Gating: a label nothing claims cannot route under ANY reading, on
|
|
6711
|
+
// every run. See the severity policy at the top of this file.
|
|
6712
|
+
severity: "error"
|
|
6713
|
+
});
|
|
6714
|
+
}
|
|
6715
|
+
const gated = outs.filter((e) => e.condition || e.isDefault === true);
|
|
6716
|
+
if (gated.length === 0) continue;
|
|
6717
|
+
const ungated = outs.filter(
|
|
6718
|
+
(e) => !e.condition && e.isDefault !== true && !declaredLabels.has(edgeLabelOf(e))
|
|
6719
|
+
);
|
|
6720
|
+
if (ungated.length > 0) {
|
|
6721
|
+
findings.push({
|
|
6722
|
+
where: `flow '${flowName}' \xB7 decision '${nid}'`,
|
|
6723
|
+
message: `has guarded out-edge(s) alongside unconditional one(s) (${ungated.map((e) => `'${String(e.target)}'`).join(", ")}) \u2014 an unconditional out-edge is traversed on EVERY pass, in parallel with whichever guarded branch matched, so the decision does not actually exclude it. A \`label\` alone does not select a path unless the decision declares a matching \`conditions[].label\`.`,
|
|
6724
|
+
hint: `Mark the fallback \`isDefault: true\` so it is taken only when no sibling condition matched (BPMN default flow), or give it its own \`condition\`. (#4414)`,
|
|
6725
|
+
rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
|
|
6726
|
+
});
|
|
6727
|
+
}
|
|
6728
|
+
}
|
|
6729
|
+
}
|
|
6730
|
+
function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
6731
|
+
const approvals = nodes.filter((n) => n.type === "approval");
|
|
6732
|
+
if (approvals.length === 0) return;
|
|
6733
|
+
const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
|
|
6734
|
+
const outEdges = /* @__PURE__ */ new Map();
|
|
6735
|
+
for (const e of edges) {
|
|
6736
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6737
|
+
if (!src) continue;
|
|
6738
|
+
if (!outEdges.has(src)) outEdges.set(src, []);
|
|
6739
|
+
outEdges.get(src).push(e);
|
|
6740
|
+
}
|
|
6741
|
+
for (const a of approvals) {
|
|
6742
|
+
const aid = typeof a.id === "string" ? a.id : "";
|
|
6743
|
+
if (!aid) continue;
|
|
6744
|
+
const reviseTargets = edges.filter((e) => e.source === aid && edgeLabelOf(e) === "revise").map((e) => typeof e.target === "string" ? e.target : "").filter((t) => t && nodeIds.has(t));
|
|
6745
|
+
if (reviseTargets.length === 0) continue;
|
|
6746
|
+
const where = `flow '${flowName}' \xB7 approval '${aid}'`;
|
|
6747
|
+
const cfg = a.config ?? {};
|
|
6748
|
+
if (cfg.maxRevisions === 0) {
|
|
6749
|
+
findings.push({
|
|
6750
|
+
where,
|
|
6751
|
+
message: `declares a 'revise' out-edge but \`maxRevisions: 0\` disables send-back \u2014 every revise auto-rejects, so the revise branch never runs.`,
|
|
6752
|
+
hint: `Set \`maxRevisions\` >= 1 to allow N send-backs before auto-reject, or drop the 'revise' edge if send-back isn't intended (ADR-0044).`,
|
|
6753
|
+
rule: FLOW_APPROVAL_REVISE_DISABLED
|
|
6754
|
+
});
|
|
6755
|
+
}
|
|
6756
|
+
const seen = new Set(reviseTargets);
|
|
6757
|
+
const queue = [...reviseTargets];
|
|
6758
|
+
const returnEdges = [];
|
|
6759
|
+
while (queue.length) {
|
|
6760
|
+
const cur = queue.shift();
|
|
6761
|
+
for (const e of outEdges.get(cur) ?? []) {
|
|
6762
|
+
if (e.target === aid) returnEdges.push(e);
|
|
6763
|
+
const t = typeof e.target === "string" ? e.target : "";
|
|
6764
|
+
if (t && nodeIds.has(t) && !seen.has(t)) {
|
|
6765
|
+
seen.add(t);
|
|
6766
|
+
queue.push(t);
|
|
6767
|
+
}
|
|
6768
|
+
}
|
|
6769
|
+
}
|
|
6770
|
+
if (returnEdges.length === 0) {
|
|
6771
|
+
findings.push({
|
|
6772
|
+
where,
|
|
6773
|
+
message: `has a 'revise' out-edge but no path loops back to it \u2014 the submitter reworks the record with nowhere to resubmit, so the revise branch dead-ends. (registerFlow accepts this \u2014 it's a valid DAG.)`,
|
|
6774
|
+
hint: `Close the loop: the 'revise' edge should reach a wait node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
|
|
6775
|
+
rule: FLOW_APPROVAL_REVISE_DEAD_END
|
|
6776
|
+
});
|
|
6777
|
+
} else if (!returnEdges.some((e) => e.type === "back")) {
|
|
6778
|
+
findings.push({
|
|
6779
|
+
where,
|
|
6780
|
+
message: `has a 'revise' loop that returns to it, but the closing edge isn't declared \`type: 'back'\` \u2014 registerFlow rejects this as an un-declared cycle.`,
|
|
6781
|
+
hint: `Mark the resubmit edge (whose target is '${aid}') \`type: 'back'\` so cycle validation skips it while it still traverses at runtime; \`maxRevisions\` guards the loop (ADR-0044).`,
|
|
6782
|
+
rule: FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
function lintFlowPatterns(stack) {
|
|
6788
|
+
const findings = [];
|
|
6789
|
+
for (const flow of asArray38(stack.flows)) {
|
|
6790
|
+
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
6791
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
6792
|
+
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
6793
|
+
const start = nodes.find((n) => n.type === "start");
|
|
6794
|
+
const startCfg = start?.config ?? {};
|
|
6795
|
+
const triggerType = typeof startCfg.triggerType === "string" ? startCfg.triggerType : "";
|
|
6796
|
+
if (triggerType.startsWith("record-")) {
|
|
6797
|
+
const src = conditionSource(startCfg.condition).trim();
|
|
6798
|
+
if (src && DATE_EQ.test(src)) {
|
|
6799
|
+
findings.push({
|
|
6800
|
+
where: `flow '${flowName}' \xB7 start condition`,
|
|
6801
|
+
message: `record-change trigger uses a date-EQUALITY time condition (\`${src}\`) \u2014 it only fires if the record happens to be written on that exact day, so unattended "N days before" rules never run.`,
|
|
6802
|
+
hint: `Use a SCHEDULE trigger (daily cron) + a range query instead \u2014 e.g. a scheduled flow whose get_record filters \`end_date\` BETWEEN {TODAY()} and {TODAY()+N}. (#1874)`,
|
|
6803
|
+
rule: FLOW_TIME_RELATIVE_ANTIPATTERN
|
|
6804
|
+
});
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
|
|
6808
|
+
const userLessKind = userLessTriggerKind(flow, startCfg);
|
|
6809
|
+
if (userLessKind && runAs !== "system") {
|
|
6810
|
+
const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
|
|
6811
|
+
if (dataNode) {
|
|
6812
|
+
const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
|
|
6813
|
+
findings.push({
|
|
6814
|
+
where: `flow '${flowName}' \xB7 runAs`,
|
|
6815
|
+
message: `${userLessKind}-triggered flow runs as ${declared}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.id}' (${dataNode.type}) has no identity to scope to and will be REFUSED at run time.`,
|
|
6816
|
+
hint: `Declare \`runAs:'system'\` to make the elevation explicit and intended (the run reads/writes every record). A ${userLessKind} flow cannot scope to a user \u2014 there is none. (ADR-0049, ADR-0073 D5, #1888, #3760)`,
|
|
6817
|
+
rule: FLOW_RUNAS_UNSCOPED,
|
|
6818
|
+
severity: "error"
|
|
6819
|
+
});
|
|
6820
|
+
}
|
|
6821
|
+
}
|
|
6822
|
+
for (const node of nodes) {
|
|
6823
|
+
const nodeWhere = `flow '${flowName}' \xB7 node '${node.id}' (${node.type})`;
|
|
6824
|
+
const cfg = node.config ?? {};
|
|
6825
|
+
if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
|
|
6826
|
+
for (const key of Object.keys(cfg)) {
|
|
6827
|
+
if (PHANTOM_AGG_KEYS.has(key)) {
|
|
6828
|
+
findings.push({
|
|
6829
|
+
where: nodeWhere,
|
|
6830
|
+
message: `node config has \`${key}\` \u2014 the automation engine has no aggregate node, so \`${key}\` is silently ignored and this node computes nothing at runtime.`,
|
|
6831
|
+
hint: `Aggregation belongs in the data layer: use \`Field.summary\` for a cross-object rollup (sum/count of children), or \`Field.formula\` for a per-record computed value. (#1870)`,
|
|
6832
|
+
rule: FLOW_PHANTOM_AGGREGATION
|
|
6833
|
+
});
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
const strings = [];
|
|
6837
|
+
collectTemplateStrings(node.config, void 0, strings);
|
|
6838
|
+
for (const str2 of strings) {
|
|
6839
|
+
if (DOUBLE_BRACE.test(str2)) {
|
|
6840
|
+
findings.push({
|
|
6841
|
+
where: nodeWhere,
|
|
6842
|
+
message: `double-brace interpolation \`${str2.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
|
|
6843
|
+
hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
|
|
6844
|
+
rule: FLOW_DOUBLE_BRACE_INTERP
|
|
6845
|
+
});
|
|
6846
|
+
}
|
|
6847
|
+
if (BARE_DOLLAR_REF.test(str2)) {
|
|
6848
|
+
findings.push({
|
|
6849
|
+
where: nodeWhere,
|
|
6850
|
+
message: `\`${str2.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
|
|
6851
|
+
hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
|
|
6852
|
+
rule: FLOW_BARE_DOLLAR_REF
|
|
6853
|
+
});
|
|
6854
|
+
}
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
scanApprovalReviseLoops(flowName, nodes, edges, findings);
|
|
6858
|
+
scanErrorLabelledEdges(flowName, nodes, edges, findings);
|
|
6859
|
+
scanBranchRouting(flowName, nodes, edges, findings);
|
|
6860
|
+
}
|
|
6861
|
+
return findings;
|
|
6862
|
+
}
|
|
6863
|
+
|
|
6864
|
+
// src/lint-liveness-properties.ts
|
|
6865
|
+
import { createRequire as createRequire4 } from "module";
|
|
6866
|
+
import { dirname, join } from "path";
|
|
6867
|
+
import { existsSync, readFileSync } from "fs";
|
|
6868
|
+
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
6869
|
+
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
6870
|
+
function asArray39(v) {
|
|
6871
|
+
if (Array.isArray(v)) return v;
|
|
6872
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6873
|
+
return [];
|
|
6874
|
+
}
|
|
6875
|
+
function resolveLivenessDir() {
|
|
6876
|
+
try {
|
|
6877
|
+
const require2 = createRequire4(import.meta.url);
|
|
6878
|
+
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
6879
|
+
const dir = join(dirname(pkgJson), "liveness");
|
|
6880
|
+
return existsSync(dir) ? dir : null;
|
|
6881
|
+
} catch {
|
|
6882
|
+
return null;
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
function loadWarnMap(dir, type) {
|
|
6886
|
+
const map = /* @__PURE__ */ new Map();
|
|
6887
|
+
const file = join(dir, `${type}.json`);
|
|
6888
|
+
if (!existsSync(file)) return map;
|
|
6889
|
+
let ledger;
|
|
6890
|
+
try {
|
|
6891
|
+
ledger = JSON.parse(readFileSync(file, "utf8"));
|
|
6892
|
+
} catch {
|
|
6893
|
+
return map;
|
|
6894
|
+
}
|
|
6895
|
+
const props = ledger.props || {};
|
|
6896
|
+
for (const [key, entry] of Object.entries(props)) {
|
|
6897
|
+
if (entry?.children) {
|
|
6898
|
+
for (const [ck, centry] of Object.entries(entry.children)) {
|
|
6899
|
+
if (shouldWarn(centry)) map.set(`${key}.${ck}`, centry);
|
|
6900
|
+
}
|
|
6901
|
+
}
|
|
6902
|
+
if (shouldWarn(entry)) map.set(key, entry);
|
|
6903
|
+
}
|
|
6904
|
+
return map;
|
|
6905
|
+
}
|
|
6906
|
+
function shouldWarn(entry) {
|
|
6907
|
+
if (!entry) return false;
|
|
6908
|
+
return entry.authorWarn === true || entry.status === "experimental";
|
|
6909
|
+
}
|
|
6910
|
+
function isAuthored(value) {
|
|
6911
|
+
if (value === void 0 || value === null) return false;
|
|
6912
|
+
if (typeof value === "boolean") return value === true;
|
|
6913
|
+
return true;
|
|
6914
|
+
}
|
|
6915
|
+
function describe(entry) {
|
|
6916
|
+
if (entry.status === "experimental") {
|
|
6917
|
+
return { kind: "is experimental \u2014 declared but NOT enforced at runtime", rule: LIVENESS_EXPERIMENTAL_PROPERTY };
|
|
6918
|
+
}
|
|
6919
|
+
return { kind: "has no runtime effect (liveness: dead)", rule: LIVENESS_DEAD_PROPERTY };
|
|
6920
|
+
}
|
|
6921
|
+
function checkItem(type, item, whereBase, warnMap, findings) {
|
|
6922
|
+
for (const [path, entry] of warnMap) {
|
|
6923
|
+
const values = path.includes(".") ? getNested(item, path) : [item[path]];
|
|
6924
|
+
for (const value of values instanceof Array ? values : [values]) {
|
|
6925
|
+
if (!isAuthored(value)) continue;
|
|
6926
|
+
const { kind, rule } = describe(entry);
|
|
6927
|
+
const hint = entry.authorHint ?? entry.note ?? "Remove it \u2014 it is declared in the spec but not consumed at runtime.";
|
|
6928
|
+
findings.push({
|
|
6929
|
+
where: whereBase,
|
|
6930
|
+
message: `sets \`${path}\` but this ${type} property ${kind}.`,
|
|
6931
|
+
hint,
|
|
6932
|
+
rule
|
|
6933
|
+
});
|
|
6934
|
+
break;
|
|
6935
|
+
}
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
function getNested(obj, path) {
|
|
6939
|
+
let cur = [obj];
|
|
6940
|
+
for (const seg of path.split(".")) {
|
|
6941
|
+
const next = [];
|
|
6942
|
+
for (const c of cur) {
|
|
6943
|
+
if (c === null || typeof c !== "object") continue;
|
|
6944
|
+
const v = Array.isArray(c) ? void 0 : c[seg];
|
|
6945
|
+
if (Array.isArray(c)) {
|
|
6946
|
+
for (const el of c) {
|
|
6947
|
+
if (el && typeof el === "object") next.push(el[seg]);
|
|
6948
|
+
}
|
|
6949
|
+
} else {
|
|
6950
|
+
next.push(v);
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
cur = next;
|
|
6954
|
+
}
|
|
6955
|
+
return cur.flatMap((v) => Array.isArray(v) ? v : [v]);
|
|
6956
|
+
}
|
|
6957
|
+
var TYPE_COLLECTIONS = [
|
|
6958
|
+
{ type: "flow", key: "flows" },
|
|
6959
|
+
{ type: "action", key: "actions" },
|
|
6960
|
+
{ type: "agent", key: "agents" },
|
|
6961
|
+
{ type: "tool", key: "tools" },
|
|
6962
|
+
{ type: "skill", key: "skills" },
|
|
6963
|
+
{ type: "dataset", key: "datasets" },
|
|
6964
|
+
{ type: "permission", key: "permissions" },
|
|
6965
|
+
{ type: "hook", key: "hooks" },
|
|
6966
|
+
{ type: "page", key: "pages" },
|
|
6967
|
+
{ type: "view", key: "views" },
|
|
6968
|
+
{ type: "webhook", key: "webhooks" },
|
|
6969
|
+
// #4487. Note what adding a TYPE costs versus adding a warned property: the
|
|
6970
|
+
// doc below is right that coverage grows by marking entries `authorWarn` —
|
|
6971
|
+
// but only WITHIN a type already listed here. A newly governed type needs its
|
|
6972
|
+
// collection registered or its ledger warns nobody, which would leave the
|
|
6973
|
+
// ledger correct and silent: the exact shape this lint exists to prevent.
|
|
6974
|
+
{ type: "datasource", key: "datasources" },
|
|
6975
|
+
// #4488 — the six newly governed types that carry `authorWarn` entries.
|
|
6976
|
+
// (doc / seed / validation are governed too but warn on nothing today, so
|
|
6977
|
+
// they are not listed; add them here the day one of their entries warns.)
|
|
6978
|
+
{ type: "app", key: "apps" },
|
|
6979
|
+
{ type: "book", key: "books" },
|
|
6980
|
+
{ type: "job", key: "jobs" },
|
|
6981
|
+
{ type: "email_template", key: "emailTemplates" },
|
|
6982
|
+
{ type: "mapping", key: "mappings" },
|
|
6983
|
+
{ type: "translation", key: "translations" }
|
|
6984
|
+
];
|
|
6985
|
+
function lintLivenessProperties(stack) {
|
|
6986
|
+
const dir = resolveLivenessDir();
|
|
6987
|
+
if (!dir) return [];
|
|
6988
|
+
const findings = [];
|
|
6989
|
+
const objectWarn = loadWarnMap(dir, "object");
|
|
6990
|
+
const fieldWarn = loadWarnMap(dir, "field");
|
|
6991
|
+
for (const obj of asArray39(stack.objects)) {
|
|
6992
|
+
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6993
|
+
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
6994
|
+
if (fieldWarn.size > 0) {
|
|
6995
|
+
for (const field of asArray39(obj.fields)) {
|
|
6996
|
+
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
6997
|
+
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
6998
|
+
}
|
|
6999
|
+
}
|
|
7000
|
+
}
|
|
7001
|
+
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7002
|
+
const warnMap = loadWarnMap(dir, type);
|
|
7003
|
+
if (warnMap.size === 0) continue;
|
|
7004
|
+
for (const item of asArray39(stack[key])) {
|
|
7005
|
+
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7006
|
+
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
return findings;
|
|
7010
|
+
}
|
|
7011
|
+
|
|
7012
|
+
// src/lint-autonumber-formats.ts
|
|
7013
|
+
import { parseAutonumberFormat, referencedFields } from "@objectstack/spec/data";
|
|
7014
|
+
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
7015
|
+
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7016
|
+
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7017
|
+
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7018
|
+
function asArray40(v) {
|
|
7019
|
+
if (Array.isArray(v)) return v;
|
|
7020
|
+
if (v && typeof v === "object") {
|
|
7021
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7022
|
+
}
|
|
7023
|
+
return [];
|
|
7024
|
+
}
|
|
7025
|
+
function lintAutonumberFormats(stack) {
|
|
7026
|
+
const findings = [];
|
|
7027
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7028
|
+
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7029
|
+
const fields = asArray40(obj.fields);
|
|
7030
|
+
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7031
|
+
for (const f of fields) {
|
|
7032
|
+
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
7033
|
+
}
|
|
7034
|
+
for (const f of fields) {
|
|
7035
|
+
if (f.type !== "autonumber") continue;
|
|
7036
|
+
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
7037
|
+
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
7038
|
+
if (!fmt) continue;
|
|
7039
|
+
const tokens = parseAutonumberFormat(fmt);
|
|
7040
|
+
const refs = referencedFields(tokens);
|
|
7041
|
+
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
7042
|
+
for (const t of tokens) {
|
|
7043
|
+
if (t.kind !== "literal") continue;
|
|
7044
|
+
const braced = t.text.match(/\{[^{}]*\}/g);
|
|
7045
|
+
if (!braced) continue;
|
|
7046
|
+
for (const tok of braced) {
|
|
7047
|
+
const body = tok.slice(1, -1);
|
|
7048
|
+
const isExtraSeq = /^0+$/.test(body);
|
|
7049
|
+
findings.push({
|
|
7050
|
+
where,
|
|
7051
|
+
message: isExtraSeq ? `format has a second sequence slot \`${tok}\` \u2014 only the first \`{0..0}\` counts; this one renders literally as "${tok}".` : `format has an unrecognized token \`${tok}\` \u2014 it is not a counter/date/{field} token, so it renders literally as "${tok}" in every record number.`,
|
|
7052
|
+
hint: isExtraSeq ? `Use a single \`{0000}\` slot; fold any second number into a literal or a {field} token.` : `Date tokens are case-sensitive and exact: {YYYY} {YY} {MM} {DD} {YYYYMMDD} (no spaces/punctuation inside). For a field value use {field_name}.`,
|
|
7053
|
+
rule: AUTONUMBER_LITERAL_TOKEN,
|
|
7054
|
+
severity: "warning"
|
|
7055
|
+
});
|
|
7056
|
+
}
|
|
7057
|
+
}
|
|
7058
|
+
for (const ref of refs) {
|
|
7059
|
+
if (ref === name) {
|
|
7060
|
+
findings.push({
|
|
7061
|
+
where,
|
|
7062
|
+
message: `format interpolates \`{${ref}}\` \u2014 its own value, which does not exist yet when the number is generated.`,
|
|
7063
|
+
hint: `Reference a DIFFERENT field that is set before create (e.g. \`{plan_no}{000}\`), or drop the token.`,
|
|
7064
|
+
rule: AUTONUMBER_SELF_REFERENCE,
|
|
7065
|
+
severity: "error"
|
|
7066
|
+
});
|
|
7067
|
+
continue;
|
|
7068
|
+
}
|
|
7069
|
+
const meta = fieldMeta.get(ref);
|
|
7070
|
+
if (!meta) {
|
|
7071
|
+
findings.push({
|
|
7072
|
+
where,
|
|
7073
|
+
message: `format interpolates \`{${ref}}\`, but object '${objectName}' has no field named '${ref}' \u2014 generation will always throw.`,
|
|
7074
|
+
hint: `Reference an existing field, or remove the \`{${ref}}\` token from the format.`,
|
|
7075
|
+
rule: AUTONUMBER_UNKNOWN_FIELD,
|
|
7076
|
+
severity: "error"
|
|
7077
|
+
});
|
|
7078
|
+
} else if (!meta.required) {
|
|
7079
|
+
findings.push({
|
|
7080
|
+
where,
|
|
7081
|
+
message: `format interpolates \`{${ref}}\`, but '${ref}' is optional \u2014 any record left blank fails autonumber generation at create time.`,
|
|
7082
|
+
hint: `Mark '${ref}' as \`required: true\` so it is always set before the record number is rendered.`,
|
|
7083
|
+
rule: AUTONUMBER_OPTIONAL_FIELD,
|
|
7084
|
+
severity: "warning"
|
|
7085
|
+
});
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
return findings;
|
|
7091
|
+
}
|
|
7092
|
+
|
|
7093
|
+
// src/lint-view-refs.ts
|
|
7094
|
+
import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
|
|
7095
|
+
function asArray41(v) {
|
|
7096
|
+
if (Array.isArray(v)) return v;
|
|
7097
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7098
|
+
return [];
|
|
7099
|
+
}
|
|
7100
|
+
var VIEW_KEY_COLLISION = "view-key-collision";
|
|
7101
|
+
var VIEW_REF_FORM_TARGET_MISSING = "view-ref-form-target-missing";
|
|
7102
|
+
var VIEW_REF_FORM_TARGET_KIND = "view-ref-form-target-kind";
|
|
7103
|
+
function containerFromObject(obj) {
|
|
7104
|
+
return { list: obj.list, form: obj.form, listViews: obj.listViews, formViews: obj.formViews };
|
|
7105
|
+
}
|
|
7106
|
+
function viewContainerObjectName(item) {
|
|
7107
|
+
if (typeof item.name === "string" && item.name) return item.name;
|
|
7108
|
+
if (typeof item.id === "string" && item.id) return item.id;
|
|
7109
|
+
if (typeof item.object === "string" && item.object) return item.object;
|
|
7110
|
+
if (typeof item.list?.data?.object === "string") return item.list.data.object;
|
|
7111
|
+
if (typeof item.form?.data?.object === "string") return item.form.data.object;
|
|
7112
|
+
return void 0;
|
|
7113
|
+
}
|
|
7114
|
+
function lintViewRefs(stack) {
|
|
7115
|
+
const findings = [];
|
|
7116
|
+
const viewKinds = /* @__PURE__ */ new Map();
|
|
7117
|
+
const indexKind = (name, kind) => {
|
|
7118
|
+
let s = viewKinds.get(name);
|
|
7119
|
+
if (!s) viewKinds.set(name, s = /* @__PURE__ */ new Set());
|
|
7120
|
+
s.add(kind);
|
|
7121
|
+
};
|
|
7122
|
+
const containers = [];
|
|
7123
|
+
for (const v of asArray41(stack.views)) {
|
|
7124
|
+
if (v.viewKind) {
|
|
7125
|
+
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7126
|
+
continue;
|
|
7127
|
+
}
|
|
7128
|
+
if (!isAggregatedViewContainer(v)) continue;
|
|
7129
|
+
const object = viewContainerObjectName(v);
|
|
7130
|
+
if (object) containers.push({ object, container: v });
|
|
7131
|
+
}
|
|
7132
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7133
|
+
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7134
|
+
if (!object) continue;
|
|
7135
|
+
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
7136
|
+
containers.push({ object, container: containerFromObject(obj) });
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
for (const { object, container } of containers) {
|
|
7140
|
+
const { items, collisions } = expandViewContainerWithDiagnostics(object, container);
|
|
7141
|
+
for (const it of items) indexKind(it.name, it.viewKind);
|
|
7142
|
+
for (const col of collisions) {
|
|
7143
|
+
findings.push({
|
|
7144
|
+
where: `object '${object}' \xB7 view key '${col.key}'`,
|
|
7145
|
+
message: `View key collision: the ${col.viewKind} view '${col.requested}' clashes with another view in the same container and was renamed to '${col.renamedTo}'. Anything referencing '${col.requested}' (a form action target, a navigation viewName) resolves to the OTHER view, not this one.`,
|
|
7146
|
+
hint: `Give the ${col.viewKind} view a unique key \u2014 the default list implicitly claims '<object>.default'. Renaming key '${col.key}' fixes both this collision and any reference that targets it.`,
|
|
7147
|
+
rule: VIEW_KEY_COLLISION,
|
|
7148
|
+
severity: "warning"
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
const seenFormTargets = /* @__PURE__ */ new Set();
|
|
7153
|
+
const checkAction = (action, ownerObject) => {
|
|
7154
|
+
if (!action || action.type !== "form") return;
|
|
7155
|
+
const target = action.target;
|
|
7156
|
+
if (typeof target !== "string" || !target) return;
|
|
7157
|
+
if (target.includes("${")) return;
|
|
7158
|
+
if (!target.includes(".")) return;
|
|
7159
|
+
const actionName = typeof action.name === "string" ? action.name : "(unnamed)";
|
|
7160
|
+
const dedupeKey = `${actionName}\0${target}`;
|
|
7161
|
+
if (seenFormTargets.has(dedupeKey)) return;
|
|
7162
|
+
seenFormTargets.add(dedupeKey);
|
|
7163
|
+
const where = ownerObject ? `action '${actionName}' on object '${ownerObject}'` : `action '${actionName}'`;
|
|
7164
|
+
const kinds = viewKinds.get(target);
|
|
7165
|
+
if (!kinds) {
|
|
7166
|
+
findings.push({
|
|
7167
|
+
where,
|
|
7168
|
+
message: `Form action target '${target}' does not resolve to any view. A type:'form' action must point at an existing form view; at runtime this opens a blank/broken form.`,
|
|
7169
|
+
hint: `Check for a typo, or a form view renamed by a key collision. Expected a form view named '<object>.<formViewKey>'.`,
|
|
7170
|
+
rule: VIEW_REF_FORM_TARGET_MISSING,
|
|
7171
|
+
severity: "warning"
|
|
7172
|
+
});
|
|
7173
|
+
return;
|
|
7174
|
+
}
|
|
7175
|
+
if (!kinds.has("form")) {
|
|
7176
|
+
const actual = [...kinds].join("/");
|
|
7177
|
+
findings.push({
|
|
7178
|
+
where,
|
|
7179
|
+
message: `Form action target '${target}' resolves to a ${actual} view, not a form view. Opening a ${actual} view through a form action renders an empty form (and a submit can silently no-op) at runtime.`,
|
|
7180
|
+
hint: `Point target at a form view (viewKind 'form'). If the form view was renamed by a key collision, fix the colliding key so '${target}' names the form again.`,
|
|
7181
|
+
rule: VIEW_REF_FORM_TARGET_KIND,
|
|
7182
|
+
severity: "error"
|
|
7183
|
+
});
|
|
7184
|
+
}
|
|
7185
|
+
};
|
|
7186
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7187
|
+
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7188
|
+
for (const action of asArray41(obj.actions)) checkAction(action, object);
|
|
7189
|
+
}
|
|
7190
|
+
for (const action of asArray41(stack.actions)) checkAction(action);
|
|
7191
|
+
return findings;
|
|
7192
|
+
}
|
|
7193
|
+
|
|
7194
|
+
// src/data-model-rules.ts
|
|
7195
|
+
var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
|
|
7196
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
7197
|
+
"number",
|
|
7198
|
+
"currency",
|
|
7199
|
+
"integer",
|
|
7200
|
+
"decimal",
|
|
7201
|
+
"percent",
|
|
7202
|
+
"float",
|
|
7203
|
+
"double"
|
|
7204
|
+
]);
|
|
7205
|
+
var OPTION_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "radio", "enum"]);
|
|
7206
|
+
var NAME_LIKE_FIELDS = ["name", "title", "subject", "label", "full_name", "display_name", "code"];
|
|
7207
|
+
var LINE_ITEM_RE = /_(line|lines|line_item|line_items|item|items|detail|details|entry|entries)$/;
|
|
7208
|
+
var ASSOCIATION_TOKENS = [
|
|
7209
|
+
"comment",
|
|
7210
|
+
"attachment",
|
|
7211
|
+
"note",
|
|
7212
|
+
"log",
|
|
7213
|
+
"audit",
|
|
7214
|
+
"activity",
|
|
7215
|
+
"activities",
|
|
7216
|
+
"history",
|
|
7217
|
+
"event",
|
|
7218
|
+
"reaction",
|
|
7219
|
+
"like",
|
|
7220
|
+
"mention",
|
|
7221
|
+
"notification",
|
|
7222
|
+
"message"
|
|
7223
|
+
];
|
|
7224
|
+
function isLineItemName(name) {
|
|
7225
|
+
return LINE_ITEM_RE.test(name);
|
|
7226
|
+
}
|
|
7227
|
+
function isAssociationName(name) {
|
|
7228
|
+
const lc = name.toLowerCase();
|
|
7229
|
+
return ASSOCIATION_TOKENS.some((t) => lc === t || lc.endsWith(`_${t}`) || lc.endsWith(`_${t}s`));
|
|
7230
|
+
}
|
|
7231
|
+
function fieldEntries2(fields) {
|
|
7232
|
+
if (!fields) return [];
|
|
7233
|
+
if (Array.isArray(fields)) {
|
|
7234
|
+
return fields.filter((f) => f && f.name != null).map((f) => ({ name: String(f.name), def: f }));
|
|
7235
|
+
}
|
|
7236
|
+
return Object.entries(fields).map(([name, def]) => ({ name, def }));
|
|
7237
|
+
}
|
|
7238
|
+
function refOf2(def) {
|
|
7239
|
+
return def?.reference || def?.reference_to;
|
|
7240
|
+
}
|
|
7241
|
+
var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
|
|
7242
|
+
function uniqueDeclared(u) {
|
|
7243
|
+
return u === true || u === "global";
|
|
7244
|
+
}
|
|
7245
|
+
function lintUniqueDeclarations(objects) {
|
|
7246
|
+
const issues = [];
|
|
7247
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7248
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7249
|
+
const obj = objects[i];
|
|
7250
|
+
if (!obj?.name) continue;
|
|
7251
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
7252
|
+
if (declaredIndexes.length === 0) continue;
|
|
7253
|
+
const singleColumnUniqueIndexes = /* @__PURE__ */ new Map();
|
|
7254
|
+
for (const idx of declaredIndexes) {
|
|
7255
|
+
if (!uniqueDeclared(idx?.unique)) continue;
|
|
7256
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
7257
|
+
if (cols.length !== 1) continue;
|
|
7258
|
+
if (!singleColumnUniqueIndexes.has(cols[0])) singleColumnUniqueIndexes.set(cols[0], idx);
|
|
7259
|
+
}
|
|
7260
|
+
if (singleColumnUniqueIndexes.size === 0) continue;
|
|
7261
|
+
for (const { name, def } of fieldEntries2(obj.fields)) {
|
|
7262
|
+
if (!uniqueDeclared(def?.unique)) continue;
|
|
7263
|
+
if (def.unique === "global") continue;
|
|
7264
|
+
const idx = singleColumnUniqueIndexes.get(name);
|
|
7265
|
+
if (!idx) continue;
|
|
7266
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
7267
|
+
issues.push({
|
|
7268
|
+
severity: "warning",
|
|
7269
|
+
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
7270
|
+
message: `"${obj.name}.${name}" declares field-level \`unique: true\` AND a single-column unique index${indexLabel} on the same column. Since #3696 the field-level form is scoped per tenant \u2014 \`(tenant, ${name})\` \u2014 while a declared index is materialized over exactly its \`fields\`, i.e. platform-wide. On a tenant-scoped object the global index wins and the per-tenant constraint can never be reached; on a tenancy-less object the two are the same index declared twice. Either way one of the two declarations has no effect.`,
|
|
7271
|
+
path: `objects[${i}]`,
|
|
7272
|
+
fix: `Pick the intent: for platform-wide uniqueness set \`unique: 'global'\` on '${name}' and drop the duplicate index; for per-tenant uniqueness drop the index (the field-level declaration already builds the tenant composite), or spell the index out as \`fields: ['organization_id', '${name}']\` if you want it explicit.`
|
|
7273
|
+
});
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
return issues;
|
|
7277
|
+
}
|
|
7278
|
+
function lintDataModel(objects) {
|
|
7279
|
+
const issues = lintUniqueDeclarations(objects);
|
|
7280
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7281
|
+
const childrenByParent = {};
|
|
7282
|
+
for (const child of objects) {
|
|
7283
|
+
if (!child?.name) continue;
|
|
7284
|
+
for (const { name: fieldName, def } of fieldEntries2(child.fields)) {
|
|
7285
|
+
if (!RELATIONSHIP_TYPES.has(def?.type)) continue;
|
|
7286
|
+
const parent = refOf2(def);
|
|
7287
|
+
if (!parent) continue;
|
|
7288
|
+
(childrenByParent[parent] || (childrenByParent[parent] = [])).push({ child, fieldName, def });
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7292
|
+
const obj = objects[i];
|
|
7293
|
+
if (!obj?.name) continue;
|
|
7294
|
+
const objPath = `objects[${i}]`;
|
|
7295
|
+
const fields = fieldEntries2(obj.fields);
|
|
7296
|
+
const hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
|
|
7297
|
+
if (fields.length > 0 && !hasNameField) {
|
|
7298
|
+
issues.push({
|
|
7299
|
+
severity: "suggestion",
|
|
7300
|
+
rule: "object/missing-name-field",
|
|
7301
|
+
message: `Object "${obj.name}" has no name/title field or primaryField \u2014 records will display as raw IDs`,
|
|
7302
|
+
path: `${objPath}.fields`
|
|
7303
|
+
});
|
|
7304
|
+
}
|
|
7305
|
+
for (const { name: fieldName, def } of fields) {
|
|
7306
|
+
if (!def || typeof def !== "object") continue;
|
|
7307
|
+
const fieldPath = `${objPath}.fields.${fieldName}`;
|
|
7308
|
+
const type = def.type;
|
|
7309
|
+
if (OPTION_FIELD_TYPES.has(type)) {
|
|
7310
|
+
const hasOptions = Array.isArray(def.options) && def.options.length > 0 || !!def.optionsFrom || !!def.dataSource || !!def.reference;
|
|
7311
|
+
if (!hasOptions) {
|
|
7312
|
+
issues.push({
|
|
7313
|
+
severity: "warning",
|
|
7314
|
+
rule: "field/select-missing-options",
|
|
7315
|
+
message: `${type} field "${obj.name}.${fieldName}" has no options`,
|
|
7316
|
+
path: `${fieldPath}.options`
|
|
7317
|
+
});
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
if (!RELATIONSHIP_TYPES.has(type)) continue;
|
|
7321
|
+
const parent = refOf2(def);
|
|
7322
|
+
if (!parent) {
|
|
7323
|
+
issues.push({
|
|
7324
|
+
severity: "error",
|
|
7325
|
+
rule: "relationship/missing-reference",
|
|
7326
|
+
message: `${type} field "${obj.name}.${fieldName}" is missing a reference target`,
|
|
7327
|
+
path: `${fieldPath}.reference`
|
|
7328
|
+
});
|
|
7329
|
+
continue;
|
|
7330
|
+
}
|
|
7331
|
+
if (type === "master_detail") {
|
|
7332
|
+
if (def.required !== true) {
|
|
7333
|
+
issues.push({
|
|
7334
|
+
severity: "warning",
|
|
7335
|
+
rule: "relationship/master-detail-required",
|
|
7336
|
+
message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should be required (a detail record cannot exist without its master)`,
|
|
7337
|
+
path: `${fieldPath}.required`,
|
|
7338
|
+
fix: "required: true"
|
|
7339
|
+
});
|
|
7340
|
+
}
|
|
7341
|
+
if (def.deleteBehavior === void 0) {
|
|
7342
|
+
issues.push({
|
|
7343
|
+
severity: "suggestion",
|
|
7344
|
+
rule: "relationship/delete-behavior",
|
|
7345
|
+
message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should declare deleteBehavior (cascade/restrict/set_null)`,
|
|
7346
|
+
path: `${fieldPath}.deleteBehavior`,
|
|
7347
|
+
fix: "deleteBehavior: 'cascade'"
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7350
|
+
if (isLineItemName(obj.name) && def.inlineEdit !== true) {
|
|
7351
|
+
issues.push({
|
|
7352
|
+
severity: "suggestion",
|
|
7353
|
+
rule: "relationship/line-items-inline-edit",
|
|
7354
|
+
message: `"${obj.name}" looks like line items of ${parent}; consider inlineEdit: true on "${fieldName}" so it is entered inline within the ${parent} form`,
|
|
7355
|
+
path: `${fieldPath}.inlineEdit`,
|
|
7356
|
+
fix: "inlineEdit: true"
|
|
7357
|
+
});
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
if (type === "lookup" && isLineItemName(obj.name)) {
|
|
7361
|
+
issues.push({
|
|
7362
|
+
severity: "suggestion",
|
|
7363
|
+
rule: "relationship/line-item-should-be-master-detail",
|
|
7364
|
+
message: `"${obj.name}" looks like line items of ${parent} but uses lookup; master_detail gives ownership + cascade + roll-ups`,
|
|
7365
|
+
path: `${fieldPath}.type`,
|
|
7366
|
+
fix: "type: 'master_detail'"
|
|
7367
|
+
});
|
|
7368
|
+
}
|
|
7369
|
+
if (def.inlineEdit === true && isAssociationName(obj.name)) {
|
|
7370
|
+
issues.push({
|
|
7371
|
+
severity: "warning",
|
|
7372
|
+
rule: "relationship/association-inline-edit",
|
|
7373
|
+
message: `"${obj.name}" is an association (comments/audit/activity), not line items \u2014 inlineEdit clutters the ${parent} entry form; surface it as a detail-page related list instead`,
|
|
7374
|
+
path: `${fieldPath}.inlineEdit`,
|
|
7375
|
+
fix: "remove inlineEdit (use relatedList on the detail page)"
|
|
7376
|
+
});
|
|
7377
|
+
}
|
|
7378
|
+
}
|
|
7379
|
+
const children = childrenByParent[obj.name] || [];
|
|
7380
|
+
const summaryChildObjects = new Set(
|
|
7381
|
+
fields.filter((f) => f.def?.type === "summary").map((f) => f.def?.summaryOperations?.object || f.def?.reference).filter(Boolean)
|
|
7382
|
+
);
|
|
7383
|
+
const seenSuggestedChild = /* @__PURE__ */ new Set();
|
|
7384
|
+
for (const { child, def } of children) {
|
|
7385
|
+
if (def?.type !== "master_detail") continue;
|
|
7386
|
+
if (!child?.name || seenSuggestedChild.has(child.name)) continue;
|
|
7387
|
+
if (summaryChildObjects.has(child.name)) continue;
|
|
7388
|
+
const numericChildField = fieldEntries2(child.fields).find((f) => NUMERIC_TYPES.has(f.def?.type));
|
|
7389
|
+
if (!numericChildField) continue;
|
|
7390
|
+
seenSuggestedChild.add(child.name);
|
|
7391
|
+
issues.push({
|
|
7392
|
+
severity: "suggestion",
|
|
7393
|
+
rule: "rollup/missing-summary",
|
|
7394
|
+
message: `"${obj.name}" owns "${child.name}" (master_detail) with numeric field "${numericChildField.name}" but has no roll-up summary; consider a summary field (count/sum) on ${obj.name}`,
|
|
7395
|
+
path: `${objPath}.fields`,
|
|
7396
|
+
fix: `summary field aggregating ${child.name}.${numericChildField.name}`
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
}
|
|
7400
|
+
return issues;
|
|
7401
|
+
}
|
|
7402
|
+
|
|
7403
|
+
// src/authoring-rules.ts
|
|
7404
|
+
var AUTHORING_COMMANDS = ["validate", "build", "lint"];
|
|
7405
|
+
var AUTHORING_SURFACES = ["cli", "runtime-publish"];
|
|
7406
|
+
var ALL = AUTHORING_COMMANDS;
|
|
7407
|
+
var CLI_ONLY = ["cli"];
|
|
7408
|
+
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
7409
|
+
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.";
|
|
7410
|
+
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.";
|
|
7411
|
+
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.";
|
|
7412
|
+
var EXPRESSION_INVALID = "expression-invalid";
|
|
7413
|
+
var AUTHORING_RULES = [
|
|
7414
|
+
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
7415
|
+
// are parsed for syntax AND checked that each `record.<field>` resolves. This
|
|
7416
|
+
// is what catches a BARE field ref (`done` instead of `record.done`) that
|
|
7417
|
+
// would otherwise silently hide an action on every record (#2183/#2185).
|
|
7418
|
+
{
|
|
7419
|
+
name: "validateStackExpressions",
|
|
7420
|
+
tier: "gating",
|
|
7421
|
+
input: "parsed",
|
|
7422
|
+
commands: ALL,
|
|
7423
|
+
source: "packages/lint/src/validate-expressions.ts",
|
|
7424
|
+
// Runtime publish gate (#4463): the EXPRESSION family. On a flow write it
|
|
7425
|
+
// checks every script-node callable and every declared predicate the flow
|
|
7426
|
+
// carries, against the live object universe — the same parse `os build`
|
|
7427
|
+
// runs, now at the door Studio/REST/MCP authors actually use.
|
|
7428
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7429
|
+
runtimeTypes: ["flow"],
|
|
7430
|
+
run: (stack) => validateStackExpressions(stack).map((i) => ({
|
|
7431
|
+
severity: i.severity ?? "error",
|
|
7432
|
+
rule: EXPRESSION_INVALID,
|
|
7433
|
+
where: i.where,
|
|
7434
|
+
path: i.where,
|
|
7435
|
+
message: i.message,
|
|
7436
|
+
hint: `source: \`${i.source}\``
|
|
7437
|
+
}))
|
|
7438
|
+
},
|
|
7439
|
+
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
7440
|
+
// mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
|
|
7441
|
+
// read the pre-parse tier or the evidence is already gone.
|
|
7442
|
+
{
|
|
7443
|
+
name: "validateListViewMode",
|
|
7444
|
+
tier: "gating",
|
|
7445
|
+
input: "normalized",
|
|
7446
|
+
commands: ALL,
|
|
7447
|
+
source: "packages/lint/src/validate-list-view-mode.ts",
|
|
7448
|
+
surfaces: CLI_ONLY,
|
|
7449
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7450
|
+
run: (stack) => validateListViewMode(stack)
|
|
7451
|
+
},
|
|
7452
|
+
// [ADR-0078] A Zod-VALID instance that silently does nothing: a `summary`
|
|
7453
|
+
// with no `summaryOperations`, a `lookup` with no `reference`, a `select`
|
|
7454
|
+
// with no `options`. Every key is one we know, so #4001's unknown-key
|
|
7455
|
+
// rejection cannot see it, and the liveness ledger cannot either (it is
|
|
7456
|
+
// per-property; the properties ARE live). This is the gate between them.
|
|
7457
|
+
//
|
|
7458
|
+
// `gating` because the error-severity shapes are fully inert — the field
|
|
7459
|
+
// reads 0 forever while authoring reports success, which is the failure the
|
|
7460
|
+
// ADR was written for (cloud#687). Pre-parse so the findings survive an
|
|
7461
|
+
// unrelated schema error elsewhere in the stack.
|
|
7462
|
+
{
|
|
7463
|
+
name: "validateFunctionalCompleteness",
|
|
7464
|
+
tier: "gating",
|
|
7465
|
+
input: "normalized",
|
|
7466
|
+
commands: ALL,
|
|
7467
|
+
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
7468
|
+
surfaces: CLI_ONLY,
|
|
7469
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7470
|
+
run: (stack) => validateFunctionalCompleteness(stack)
|
|
7471
|
+
},
|
|
7472
|
+
// A flat list-view object in `views: []` parses to an EMPTY container
|
|
7473
|
+
// (ViewSchema strips unknown keys): the schema step passes, zero views
|
|
7474
|
+
// register, and the Console renders nothing. Pre-parse for the same reason.
|
|
7475
|
+
{
|
|
7476
|
+
name: "validateViewContainers",
|
|
7477
|
+
tier: "gating",
|
|
7478
|
+
input: "normalized",
|
|
7479
|
+
commands: ALL,
|
|
7480
|
+
source: "packages/lint/src/validate-view-containers.ts",
|
|
7481
|
+
surfaces: CLI_ONLY,
|
|
7482
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7483
|
+
run: (stack) => validateViewContainers(stack)
|
|
7484
|
+
},
|
|
7485
|
+
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
7486
|
+
// chartConfig axis/series must resolve against the declared datasets.
|
|
7487
|
+
{
|
|
7488
|
+
name: "validateWidgetBindings",
|
|
7489
|
+
tier: "gating",
|
|
7490
|
+
input: "parsed",
|
|
7491
|
+
commands: ALL,
|
|
7492
|
+
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
7493
|
+
surfaces: CLI_ONLY,
|
|
7494
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7495
|
+
run: (stack) => validateWidgetBindings(stack)
|
|
7496
|
+
},
|
|
7497
|
+
// ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
|
|
7498
|
+
// target that resolves to no defined action ships a button that renders and
|
|
7499
|
+
// silently does nothing on click. Unresolved `url` routes stay advisory.
|
|
7500
|
+
{
|
|
7501
|
+
name: "validateDashboardActionRefs",
|
|
7502
|
+
tier: "gating",
|
|
7503
|
+
input: "parsed",
|
|
7504
|
+
commands: ALL,
|
|
7505
|
+
source: "packages/lint/src/validate-dashboard-action-refs.ts",
|
|
7506
|
+
surfaces: CLI_ONLY,
|
|
7507
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7508
|
+
run: (stack) => validateDashboardActionRefs(stack)
|
|
7509
|
+
},
|
|
7510
|
+
// #3574 — a filter value like `{current_user}` resolves in no vocabulary,
|
|
7511
|
+
// reaches the data engine as a literal and matches nothing. The surface
|
|
7512
|
+
// renders empty with no error, and a silent zero is indistinguishable from a
|
|
7513
|
+
// genuine one at review time.
|
|
7514
|
+
{
|
|
7515
|
+
name: "validateFilterTokens",
|
|
7516
|
+
tier: "gating",
|
|
7517
|
+
input: "parsed",
|
|
7518
|
+
commands: ALL,
|
|
7519
|
+
source: "packages/lint/src/validate-filter-tokens.ts",
|
|
7520
|
+
surfaces: CLI_ONLY,
|
|
7521
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7522
|
+
run: (stack) => validateFilterTokens(stack)
|
|
7523
|
+
},
|
|
7524
|
+
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
7525
|
+
// rules that answer "does this name resolve to anything?". It reached all
|
|
7526
|
+
// three commands before this file existed; it is an entry here so the two
|
|
7527
|
+
// registries compose instead of competing, and so its members are covered by
|
|
7528
|
+
// the same guard as everything else.
|
|
7529
|
+
{
|
|
7530
|
+
name: "validateReferenceIntegrity",
|
|
7531
|
+
tier: "gating",
|
|
7532
|
+
input: "parsed",
|
|
7533
|
+
commands: ALL,
|
|
7534
|
+
source: "packages/lint/src/reference-integrity-suite.ts",
|
|
7535
|
+
// Runtime publish gate (#4463): the REFERENCE family. On a flow snapshot the
|
|
7536
|
+
// members that answer a flow question run (`validateFlowTemplatePaths`,
|
|
7537
|
+
// `validateFlowNodeWrites`, `validateReadonlyFlowWrites`,
|
|
7538
|
+
// `validateObjectReferences`); the page/nav/AI members see no such
|
|
7539
|
+
// collection in the snapshot and return nothing, and the two that would
|
|
7540
|
+
// load `typescript` need a hook/action/react body the snapshot never
|
|
7541
|
+
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
7542
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7543
|
+
runtimeTypes: ["flow"],
|
|
7544
|
+
run: (stack) => validateReferenceIntegrity(stack)
|
|
7545
|
+
},
|
|
7546
|
+
// ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
|
|
7547
|
+
// `id`), name real CSS properties + design tokens, and carry a `large` base.
|
|
7548
|
+
{
|
|
7549
|
+
name: "validateResponsiveStyles",
|
|
7550
|
+
tier: "gating",
|
|
7551
|
+
input: "parsed",
|
|
7552
|
+
commands: ALL,
|
|
7553
|
+
source: "packages/lint/src/validate-responsive-styles.ts",
|
|
7554
|
+
surfaces: CLI_ONLY,
|
|
7555
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7556
|
+
run: (stack) => validateResponsiveStyles(stack)
|
|
7557
|
+
},
|
|
7558
|
+
// ADR-0080 — a `kind:'jsx'` page's `source` is parsed (never executed) and
|
|
7559
|
+
// compiled to the SDUI tree at save time, so malformed source must fail loudly
|
|
7560
|
+
// here (ADR-0078) instead of being stored and breaking only at render.
|
|
7561
|
+
{
|
|
7562
|
+
name: "validateJsxPages",
|
|
7563
|
+
tier: "gating",
|
|
7564
|
+
input: "parsed",
|
|
7565
|
+
commands: ALL,
|
|
7566
|
+
source: "packages/lint/src/validate-jsx-pages.ts",
|
|
7567
|
+
surfaces: CLI_ONLY,
|
|
7568
|
+
surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
|
|
7569
|
+
run: (stack, ctx) => validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest } : {})
|
|
7570
|
+
},
|
|
7571
|
+
// ADR-0081 — a `kind:'react'` page's `source` is real React executed at
|
|
7572
|
+
// render. Transpiled here (Sucrase, never executed) so a syntax error fails at
|
|
7573
|
+
// author time, not at render. Lazy: only a stack with such a page pays.
|
|
7574
|
+
{
|
|
7575
|
+
name: "validateReactPages",
|
|
7576
|
+
tier: "gating",
|
|
7577
|
+
input: "parsed",
|
|
7578
|
+
commands: ALL,
|
|
7579
|
+
source: "packages/lint/src/validate-react-pages.ts",
|
|
7580
|
+
surfaces: CLI_ONLY,
|
|
7581
|
+
surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
|
|
7582
|
+
run: (stack) => validateReactPages(stack)
|
|
7583
|
+
},
|
|
7584
|
+
// ADR-0065, source tier — Tailwind `className` in a `kind:'html'`/`'react'`
|
|
7585
|
+
// page silently no-ops (the build never scans authored metadata).
|
|
7586
|
+
{
|
|
7587
|
+
name: "validatePageSourceStyling",
|
|
7588
|
+
tier: "advisory",
|
|
7589
|
+
input: "parsed",
|
|
7590
|
+
commands: ALL,
|
|
7591
|
+
source: "packages/lint/src/validate-page-source-styling.ts",
|
|
7592
|
+
surfaces: CLI_ONLY,
|
|
7593
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7594
|
+
run: (stack) => validatePageSourceStyling(stack)
|
|
7595
|
+
},
|
|
7596
|
+
// ADR-0066 ⑨ — a `requiredPermissions` entry naming a capability registered
|
|
7597
|
+
// nowhere fails closed at runtime. Advisory: another installed package may
|
|
7598
|
+
// legitimately provide it.
|
|
7599
|
+
{
|
|
7600
|
+
name: "validateCapabilityReferences",
|
|
7601
|
+
tier: "advisory",
|
|
7602
|
+
input: "parsed",
|
|
7603
|
+
commands: ALL,
|
|
7604
|
+
source: "packages/lint/src/validate-capability-references.ts",
|
|
7605
|
+
surfaces: CLI_ONLY,
|
|
7606
|
+
surfaceReason: 'P2 (#4463): the ONE rule the runtime universe makes strictly stronger \u2014 the advisory hedge ("another installed package may provide it") is decidable against the live capability registry, so it graduates from advisory to gating there rather than merely being ported. That promotion is a severity change on a published rule id and belongs in its own PR, not riding a wiring change.',
|
|
7607
|
+
run: (stack) => validateCapabilityReferences(stack)
|
|
7608
|
+
},
|
|
7609
|
+
// A record-change flow whose start-node objectName matches nothing never
|
|
7610
|
+
// fires — silently. Reads the pre-parse tier so an author sees what they
|
|
7611
|
+
// wrote. Advisory: the object may come from another installed package.
|
|
7612
|
+
{
|
|
7613
|
+
name: "validateFlowTriggerReadiness",
|
|
7614
|
+
tier: "advisory",
|
|
7615
|
+
input: "normalized",
|
|
7616
|
+
commands: ALL,
|
|
7617
|
+
source: "packages/lint/src/validate-flow-trigger-readiness.ts",
|
|
7618
|
+
// Runtime publish gate (#4463): the FLOW family. Advisory at this surface
|
|
7619
|
+
// too — its findings are logged, not thrown (P1 gates on `error` only; P2
|
|
7620
|
+
// puts advisories on the response for Studio to render).
|
|
7621
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7622
|
+
runtimeTypes: ["flow"],
|
|
7623
|
+
run: (stack) => validateFlowTriggerReadiness(stack)
|
|
7624
|
+
},
|
|
7625
|
+
// ADR-0090 D3 fallout — an approval `{ type: 'role' }` resolves against the
|
|
7626
|
+
// better-auth org-membership tier, not positions, so a position name authored
|
|
7627
|
+
// there routes the approval to nobody; and an expression approver that does
|
|
7628
|
+
// not parse can never resolve. The rule whose absence from `os build` and
|
|
7629
|
+
// `os validate` was #4409's worked example: it gates, and it ran on `os lint`
|
|
7630
|
+
// alone, so a broken approval flow built and published green.
|
|
7631
|
+
{
|
|
7632
|
+
name: "validateApprovalApprovers",
|
|
7633
|
+
tier: "gating",
|
|
7634
|
+
input: "parsed",
|
|
7635
|
+
commands: ALL,
|
|
7636
|
+
source: "packages/lint/src/validate-approval-approvers.ts",
|
|
7637
|
+
// Runtime publish gate (#4463): the APPROVAL family, and the issue's worked
|
|
7638
|
+
// example both times. #4409 fixed it for `os build`; a tenant saving the
|
|
7639
|
+
// same broken expression approver from Studio still sailed through, because
|
|
7640
|
+
// `approver.value` is just a string to Zod. It is not just a string here.
|
|
7641
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7642
|
+
runtimeTypes: ["flow"],
|
|
7643
|
+
run: (stack) => validateApprovalApprovers(stack)
|
|
7644
|
+
},
|
|
7645
|
+
// ADR-0079 — `titleFormat` is retired in favour of `nameField`, and an object
|
|
7646
|
+
// with no resolvable title ships records with no meaningful name. Advisory:
|
|
7647
|
+
// auto-provision and the `Record #<id>` floor keep it from ever being fatal.
|
|
7648
|
+
{
|
|
7649
|
+
name: "validateRecordTitle",
|
|
7650
|
+
tier: "advisory",
|
|
7651
|
+
input: "parsed",
|
|
7652
|
+
commands: ALL,
|
|
7653
|
+
source: "packages/lint/src/validate-record-title.ts",
|
|
7654
|
+
surfaces: CLI_ONLY,
|
|
7655
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7656
|
+
run: (stack) => validateRecordTitle(stack)
|
|
7657
|
+
},
|
|
7658
|
+
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
7659
|
+
// into the object's field map; a dangling one is Zod-valid and silently inert
|
|
7660
|
+
// at render. Advisory: every consumer degrades gracefully.
|
|
7661
|
+
{
|
|
7662
|
+
name: "validateSemanticRoles",
|
|
7663
|
+
tier: "advisory",
|
|
7664
|
+
input: "parsed",
|
|
7665
|
+
commands: ALL,
|
|
7666
|
+
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
7667
|
+
surfaces: CLI_ONLY,
|
|
7668
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7669
|
+
run: (stack) => validateSemanticRoles(stack)
|
|
7670
|
+
},
|
|
7671
|
+
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
7672
|
+
// (silently not rendered) and an absolute `colSpan` under a per-surface
|
|
7673
|
+
// derived column count. Advisory: the renderer skips the unknown field and
|
|
7674
|
+
// clamps the span, so nothing is broken — but each is almost certainly an
|
|
7675
|
+
// authoring mistake, and until #4449 this rule ran on no command at all.
|
|
7676
|
+
// Pure structured-metadata walk (no lazy dependency), so wiring it to all
|
|
7677
|
+
// three costs nothing measurable.
|
|
7678
|
+
{
|
|
7679
|
+
name: "validateFormLayout",
|
|
7680
|
+
tier: "advisory",
|
|
7681
|
+
input: "parsed",
|
|
7682
|
+
commands: ALL,
|
|
7683
|
+
source: "packages/lint/src/validate-form-layout.ts",
|
|
7684
|
+
surfaces: CLI_ONLY,
|
|
7685
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7686
|
+
run: (stack) => validateFormLayout(stack)
|
|
7687
|
+
},
|
|
7688
|
+
// ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no
|
|
7689
|
+
// `locations` and that no view places by name renders on no surface at all.
|
|
7690
|
+
// objectui#3142 made that measurable: four renderers used to show an
|
|
7691
|
+
// undeclared action anyway, and now none does. Advisory: a view in another
|
|
7692
|
+
// installed package may be the one placing it, and `locations: []` (the
|
|
7693
|
+
// documented headless shape) is deliberately never flagged.
|
|
7694
|
+
{
|
|
7695
|
+
name: "validateActionLocations",
|
|
7696
|
+
tier: "advisory",
|
|
7697
|
+
input: "parsed",
|
|
7698
|
+
commands: ALL,
|
|
7699
|
+
source: "packages/lint/src/validate-action-locations.ts",
|
|
7700
|
+
surfaces: CLI_ONLY,
|
|
7701
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7702
|
+
run: (stack) => validateActionLocations(stack)
|
|
7703
|
+
},
|
|
7704
|
+
// framework#3434 — seeds replay on every boot, so a `mode: 'insert'` dataset
|
|
7705
|
+
// duplicates its table on every restart.
|
|
7706
|
+
{
|
|
7707
|
+
name: "validateSeedReplaySafety",
|
|
7708
|
+
tier: "advisory",
|
|
7709
|
+
input: "parsed",
|
|
7710
|
+
commands: ALL,
|
|
7711
|
+
source: "packages/lint/src/validate-seed-replay-safety.ts",
|
|
7712
|
+
surfaces: CLI_ONLY,
|
|
7713
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7714
|
+
run: (stack) => validateSeedReplaySafety(stack)
|
|
7715
|
+
},
|
|
7716
|
+
// framework#3433 follow-up — #3433 exempts seed writes from the
|
|
7717
|
+
// `state_machine` rule, so a seeded status the FSM does not declare is no
|
|
7718
|
+
// longer rejected at write time. Re-added at author time; advisory, because
|
|
7719
|
+
// the exemption itself is legitimate.
|
|
7720
|
+
{
|
|
7721
|
+
name: "validateSeedStateMachine",
|
|
7722
|
+
tier: "advisory",
|
|
7723
|
+
input: "parsed",
|
|
7724
|
+
commands: ALL,
|
|
7725
|
+
source: "packages/lint/src/validate-seed-state-machine.ts",
|
|
7726
|
+
surfaces: CLI_ONLY,
|
|
7727
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7728
|
+
run: (stack) => validateSeedStateMachine(stack)
|
|
7729
|
+
},
|
|
7730
|
+
// ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
|
|
7731
|
+
// Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
|
|
7732
|
+
// during parse, so the alias the author wrote is gone from `result.data`.
|
|
7733
|
+
{
|
|
7734
|
+
name: "validateVisibilityPredicates",
|
|
7735
|
+
tier: "advisory",
|
|
7736
|
+
input: "normalized",
|
|
7737
|
+
commands: ALL,
|
|
7738
|
+
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
7739
|
+
surfaces: CLI_ONLY,
|
|
7740
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7741
|
+
run: (stack) => validateVisibilityPredicates(stack)
|
|
7742
|
+
},
|
|
7743
|
+
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
7744
|
+
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
7745
|
+
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
7746
|
+
// `flow-default-edge-with-condition` (#4414 — a declaration that is inert, so
|
|
7747
|
+
// the route silently differs from what the author wrote). The bar for
|
|
7748
|
+
// promoting one is stated at the top of `lint-flow-patterns.ts`.
|
|
7749
|
+
{
|
|
7750
|
+
name: "lintFlowPatterns",
|
|
7751
|
+
tier: "gating",
|
|
7752
|
+
input: "parsed",
|
|
7753
|
+
commands: ALL,
|
|
7754
|
+
source: "packages/lint/src/lint-flow-patterns.ts",
|
|
7755
|
+
// Runtime publish gate (#4463): the FLOW family's anti-pattern half. Its
|
|
7756
|
+
// three `error` rules are the sharpest fit for a publish gate that exists —
|
|
7757
|
+
// `flow-runas-unscoped` is metadata the automation engine REFUSES to
|
|
7758
|
+
// execute, so publishing it can only ever produce a broken flow.
|
|
7759
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7760
|
+
runtimeTypes: ["flow"],
|
|
7761
|
+
run: (stack) => lintFlowPatterns(stack).map((f) => ({
|
|
7762
|
+
severity: f.severity ?? "warning",
|
|
7763
|
+
rule: f.rule,
|
|
7764
|
+
where: f.where,
|
|
7765
|
+
path: f.where,
|
|
7766
|
+
message: f.message,
|
|
7767
|
+
hint: f.hint
|
|
7768
|
+
}))
|
|
7769
|
+
},
|
|
7770
|
+
// The spec-liveness loop on the author side: a property the ledger marks
|
|
7771
|
+
// dead-and-misleading or experimental is set hopefully and does nothing.
|
|
7772
|
+
// Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and
|
|
7773
|
+
// never fatal.
|
|
7774
|
+
{
|
|
7775
|
+
name: "lintLivenessProperties",
|
|
7776
|
+
tier: "advisory",
|
|
7777
|
+
input: "parsed",
|
|
7778
|
+
commands: ALL,
|
|
7779
|
+
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
7780
|
+
surfaces: CLI_ONLY,
|
|
7781
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7782
|
+
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
7783
|
+
severity: "warning",
|
|
7784
|
+
rule: f.rule,
|
|
7785
|
+
where: f.where,
|
|
7786
|
+
path: f.where,
|
|
7787
|
+
message: f.message,
|
|
7788
|
+
hint: f.hint
|
|
7789
|
+
}))
|
|
7790
|
+
},
|
|
7791
|
+
// A format like `{plan_no}{000}` makes the referenced field part of the
|
|
7792
|
+
// counter scope, so it must exist and be set at create time. Unknown field →
|
|
7793
|
+
// broken (error); optional field → fragile (warning).
|
|
7794
|
+
{
|
|
7795
|
+
name: "lintAutonumberFormats",
|
|
7796
|
+
tier: "gating",
|
|
7797
|
+
input: "parsed",
|
|
7798
|
+
commands: ALL,
|
|
7799
|
+
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
7800
|
+
surfaces: CLI_ONLY,
|
|
7801
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7802
|
+
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
7803
|
+
severity: f.severity,
|
|
7804
|
+
rule: f.rule,
|
|
7805
|
+
where: f.where,
|
|
7806
|
+
path: f.where,
|
|
7807
|
+
message: f.message,
|
|
7808
|
+
hint: f.hint
|
|
7809
|
+
}))
|
|
7810
|
+
},
|
|
7811
|
+
// #2554 — a `type:'form'` action target naming a missing or LIST view opens a
|
|
7812
|
+
// broken form at runtime; a list/form view-key collision silently renames one
|
|
7813
|
+
// view so references resolve to the OTHER. Both are broken.
|
|
7814
|
+
{
|
|
7815
|
+
name: "lintViewRefs",
|
|
7816
|
+
tier: "gating",
|
|
7817
|
+
input: "parsed",
|
|
7818
|
+
commands: ALL,
|
|
7819
|
+
source: "packages/lint/src/lint-view-refs.ts",
|
|
7820
|
+
surfaces: CLI_ONLY,
|
|
7821
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7822
|
+
run: (stack) => lintViewRefs(stack).map((f) => ({
|
|
7823
|
+
severity: f.severity,
|
|
7824
|
+
rule: f.rule,
|
|
7825
|
+
where: f.where,
|
|
7826
|
+
path: f.where,
|
|
7827
|
+
message: f.message,
|
|
7828
|
+
hint: f.hint
|
|
7829
|
+
}))
|
|
7830
|
+
},
|
|
7831
|
+
// #3991 — a column carrying BOTH a field-level `unique: true` and a
|
|
7832
|
+
// single-column declared unique index has two intents, of which exactly one
|
|
7833
|
+
// takes effect (the global index wins; the tenant composite is unreachable).
|
|
7834
|
+
{
|
|
7835
|
+
name: "lintUniqueDeclarations",
|
|
7836
|
+
tier: "advisory",
|
|
7837
|
+
input: "parsed",
|
|
7838
|
+
commands: ["validate", "build"],
|
|
7839
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
7840
|
+
surfaces: CLI_ONLY,
|
|
7841
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7842
|
+
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly as R10 of its best-practice sweep \u2014 registering it for `lint` as well would report every finding twice. This is coverage recorded, not coverage missing: all three commands report the rule.",
|
|
7843
|
+
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
7844
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
7845
|
+
rule: f.rule,
|
|
7846
|
+
where: f.path,
|
|
7847
|
+
path: f.path,
|
|
7848
|
+
message: f.message,
|
|
7849
|
+
hint: f.fix ?? ""
|
|
7850
|
+
}))
|
|
7851
|
+
},
|
|
7852
|
+
// ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
|
|
7853
|
+
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
7854
|
+
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
7855
|
+
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
7856
|
+
{
|
|
7857
|
+
name: "validateSecurityPosture",
|
|
7858
|
+
tier: "gating",
|
|
7859
|
+
input: "parsed",
|
|
7860
|
+
commands: ALL,
|
|
7861
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
7862
|
+
surfaces: CLI_ONLY,
|
|
7863
|
+
surfaceReason: "Already gated at this surface by a DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on `object` (`registerAuthoringGate`) that enforces the same OWD posture rules on every runtime write. Running the linter here as well would double-report one refusal in two vocabularies. Consolidating the two onto this table is P2 (#4463), and is a merge, not a hole.",
|
|
7864
|
+
run: (stack) => validateSecurityPosture(stack)
|
|
7865
|
+
},
|
|
7866
|
+
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
7867
|
+
// sharing rule that walks it builds a second permission hierarchy (the
|
|
7868
|
+
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
7869
|
+
// so it grants nothing it appears to.
|
|
7870
|
+
{
|
|
7871
|
+
name: "validateOrgAxisRedLines",
|
|
7872
|
+
tier: "gating",
|
|
7873
|
+
input: "parsed",
|
|
7874
|
+
commands: ALL,
|
|
7875
|
+
source: "packages/lint/src/validate-org-axis-red-lines.ts",
|
|
7876
|
+
surfaces: CLI_ONLY,
|
|
7877
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7878
|
+
run: (stack) => validateOrgAxisRedLines(stack)
|
|
7879
|
+
}
|
|
7880
|
+
];
|
|
7881
|
+
function authoringRulesFor(command) {
|
|
7882
|
+
return AUTHORING_RULES.filter((r) => r.commands.includes(command));
|
|
7883
|
+
}
|
|
7884
|
+
function runAuthoringRules(command, run) {
|
|
7885
|
+
const findings = [];
|
|
7886
|
+
const ctx = { sduiManifest: run.sduiManifest };
|
|
7887
|
+
for (const rule of authoringRulesFor(command)) {
|
|
7888
|
+
const stack = rule.input === "normalized" ? run.normalized : run.parsed ?? run.normalized;
|
|
7889
|
+
findings.push(...rule.run(stack, ctx));
|
|
7890
|
+
}
|
|
7891
|
+
return findings;
|
|
7892
|
+
}
|
|
7893
|
+
function splitBySeverity(findings) {
|
|
7894
|
+
return {
|
|
7895
|
+
errors: findings.filter((f) => f.severity === "error"),
|
|
7896
|
+
advisories: findings.filter((f) => f.severity !== "error")
|
|
7897
|
+
};
|
|
7898
|
+
}
|
|
7899
|
+
|
|
7900
|
+
// src/runtime-gate.ts
|
|
7901
|
+
var TYPE_TO_STACK_KEY = {
|
|
7902
|
+
flow: "flows",
|
|
7903
|
+
object: "objects",
|
|
7904
|
+
view: "views",
|
|
7905
|
+
action: "actions",
|
|
7906
|
+
page: "pages",
|
|
7907
|
+
dashboard: "dashboards",
|
|
7908
|
+
agent: "agents",
|
|
7909
|
+
hook: "hooks",
|
|
7910
|
+
seed: "seeds"
|
|
7911
|
+
};
|
|
7912
|
+
function runtimeAuthoringRulesFor(type) {
|
|
7913
|
+
return AUTHORING_RULES.filter(
|
|
7914
|
+
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
7915
|
+
);
|
|
7916
|
+
}
|
|
7917
|
+
function runtimeGatedTypes() {
|
|
7918
|
+
const types = /* @__PURE__ */ new Set();
|
|
7919
|
+
for (const rule of AUTHORING_RULES) {
|
|
7920
|
+
if (!rule.surfaces.includes("runtime-publish")) continue;
|
|
7921
|
+
for (const t of rule.runtimeTypes ?? []) types.add(t);
|
|
7922
|
+
}
|
|
7923
|
+
return [...types].sort();
|
|
7924
|
+
}
|
|
7925
|
+
function stackKeyForType(type) {
|
|
7926
|
+
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
7927
|
+
}
|
|
7928
|
+
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
7929
|
+
function runRules(rules, stack, ctx) {
|
|
7930
|
+
const findings = [];
|
|
7931
|
+
for (const rule of rules) {
|
|
7932
|
+
try {
|
|
7933
|
+
findings.push(...rule.run(stack, ctx));
|
|
7934
|
+
} catch (err) {
|
|
7935
|
+
findings.push({
|
|
7936
|
+
severity: "warning",
|
|
7937
|
+
rule: "authoring-rule-threw",
|
|
7938
|
+
where: rule.name,
|
|
7939
|
+
path: rule.source,
|
|
7940
|
+
message: `rule ${rule.name} threw while judging this write: ${err instanceof Error ? err.message : String(err)}`,
|
|
7941
|
+
hint: "This is a bug in the rule, not in the metadata \u2014 the write was not blocked by it. Please report it with the body that triggered it."
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
return findings;
|
|
7946
|
+
}
|
|
7947
|
+
function runRuntimeAuthoringRules(args) {
|
|
7948
|
+
const rules = runtimeAuthoringRulesFor(args.type);
|
|
7949
|
+
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
7950
|
+
if (rules.length === 0) return empty;
|
|
7951
|
+
const stackKey = stackKeyForType(args.type);
|
|
7952
|
+
if (!stackKey) return empty;
|
|
7953
|
+
if (!args.item || typeof args.item !== "object") return empty;
|
|
7954
|
+
const item = args.item;
|
|
7955
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
7956
|
+
const contextObjects = args.context?.objects ?? [];
|
|
7957
|
+
const ctx = { sduiManifest: args.sduiManifest };
|
|
7958
|
+
const writesIntoContext = stackKey === "objects";
|
|
7959
|
+
const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
|
|
7960
|
+
const baseline = { objects: baselineObjects };
|
|
7961
|
+
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
7962
|
+
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
7963
|
+
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
7964
|
+
return {
|
|
7965
|
+
errors: added.filter((f) => f.severity === "error"),
|
|
7966
|
+
advisories: added.filter((f) => f.severity !== "error"),
|
|
7967
|
+
rulesRun: rules.map((r) => r.name)
|
|
7968
|
+
};
|
|
7969
|
+
}
|
|
5806
7970
|
export {
|
|
5807
7971
|
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
5808
7972
|
ACTION_BODY_WRITE_PATTERNS,
|
|
5809
7973
|
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
5810
7974
|
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
5811
7975
|
ACTION_NAME_UNDEFINED,
|
|
7976
|
+
ACTION_NO_PLACEMENT,
|
|
5812
7977
|
ACTION_RECORD_WRITE_DISCARDED,
|
|
5813
7978
|
ACTION_RECORD_WRITE_PATTERNS,
|
|
5814
7979
|
ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
@@ -5824,6 +7989,13 @@ export {
|
|
|
5824
7989
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
5825
7990
|
APPROVAL_EXPRESSION_INVALID,
|
|
5826
7991
|
APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
|
|
7992
|
+
AUTHORING_COMMANDS,
|
|
7993
|
+
AUTHORING_RULES,
|
|
7994
|
+
AUTHORING_SURFACES,
|
|
7995
|
+
AUTONUMBER_LITERAL_TOKEN,
|
|
7996
|
+
AUTONUMBER_OPTIONAL_FIELD,
|
|
7997
|
+
AUTONUMBER_SELF_REFERENCE,
|
|
7998
|
+
AUTONUMBER_UNKNOWN_FIELD,
|
|
5827
7999
|
CAPABILITY_REFERENCE_UNKNOWN,
|
|
5828
8000
|
CHART_AXIS_NOT_SELECTED,
|
|
5829
8001
|
CHART_CONFIG_MISSING,
|
|
@@ -5834,13 +8006,29 @@ export {
|
|
|
5834
8006
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
5835
8007
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
5836
8008
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
8009
|
+
EXPRESSION_INVALID,
|
|
5837
8010
|
FIELD_GROUP_EMPTY,
|
|
5838
8011
|
FIELD_GROUP_UNDECLARED,
|
|
5839
8012
|
FILTER_TOKEN_UNKNOWN,
|
|
8013
|
+
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
8014
|
+
FLOW_APPROVAL_REVISE_DISABLED,
|
|
8015
|
+
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
8016
|
+
FLOW_BARE_DOLLAR_REF,
|
|
8017
|
+
FLOW_BRANCH_LABEL_UNMATCHED,
|
|
8018
|
+
FLOW_DATE_EQUALITY_FILTER,
|
|
8019
|
+
FLOW_DECISION_UNCONDITIONAL_BRANCH,
|
|
8020
|
+
FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
8021
|
+
FLOW_DOUBLE_BRACE_INTERP,
|
|
5840
8022
|
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
8023
|
+
FLOW_ERROR_LABEL_NOT_FAULT,
|
|
8024
|
+
FLOW_INERT_NODE_CONDITION,
|
|
8025
|
+
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
5841
8026
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
8027
|
+
FLOW_PHANTOM_AGGREGATION,
|
|
8028
|
+
FLOW_RUNAS_UNSCOPED,
|
|
5842
8029
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
5843
8030
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
8031
|
+
FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
5844
8032
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
5845
8033
|
FLOW_UPDATE_READONLY_FIELD,
|
|
5846
8034
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -5853,19 +8041,25 @@ export {
|
|
|
5853
8041
|
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
5854
8042
|
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5855
8043
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
8044
|
+
LIVENESS_DEAD_PROPERTY,
|
|
8045
|
+
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
5856
8046
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
5857
8047
|
NAV_OBJECT_UNGRANTED,
|
|
8048
|
+
NAV_TARGET_UNRESOLVED,
|
|
8049
|
+
NULL_GUARD_HINT,
|
|
5858
8050
|
OBJECT_REFERENCE_UNKNOWN,
|
|
5859
8051
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
5860
8052
|
ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
5861
8053
|
ORG_AXIS_PERMISSION_INHERITANCE,
|
|
5862
8054
|
PAGE_FIELD_UNKNOWN,
|
|
5863
8055
|
PAGE_SOURCE_CLASSNAME,
|
|
8056
|
+
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
5864
8057
|
REACT_CHART_AGGREGATE_INVALID,
|
|
5865
8058
|
REACT_CHART_AXIS_UNKNOWN,
|
|
5866
8059
|
REACT_CHART_FIELD_UNKNOWN,
|
|
5867
8060
|
REFERENCE_INTEGRITY_RULES,
|
|
5868
8061
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
8062
|
+
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
5869
8063
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
5870
8064
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
5871
8065
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -5890,17 +8084,37 @@ export {
|
|
|
5890
8084
|
TITLE_UNRESOLVABLE,
|
|
5891
8085
|
TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
5892
8086
|
TRANSLATION_TARGET_UNKNOWN,
|
|
8087
|
+
UNIQUE_DOUBLE_DECLARATION,
|
|
5893
8088
|
VIEW_CONTAINER_SHAPE,
|
|
8089
|
+
VIEW_KEY_COLLISION,
|
|
8090
|
+
VIEW_REF_FORM_TARGET_KIND,
|
|
8091
|
+
VIEW_REF_FORM_TARGET_MISSING,
|
|
5894
8092
|
VISIBILITY_ALIAS_DEPRECATED,
|
|
5895
8093
|
VISIBILITY_ROOT_MISLAYERED,
|
|
5896
8094
|
WIDGET_DATASET_UNKNOWN,
|
|
5897
8095
|
WIDGET_DIMENSION_UNKNOWN,
|
|
5898
8096
|
WIDGET_MEASURE_UNKNOWN,
|
|
8097
|
+
authoringRulesFor,
|
|
5899
8098
|
buildAccessMatrix,
|
|
5900
8099
|
diffAccessMatrix,
|
|
5901
8100
|
extractHookBodyWriteSet,
|
|
5902
8101
|
extractHookBodyWrites,
|
|
8102
|
+
findUnguardedNullableOperands,
|
|
8103
|
+
lintAutonumberFormats,
|
|
8104
|
+
lintDataModel,
|
|
8105
|
+
lintFlowPatterns,
|
|
8106
|
+
lintLivenessProperties,
|
|
8107
|
+
lintUniqueDeclarations,
|
|
8108
|
+
lintViewRefs,
|
|
8109
|
+
nullGuardMessage,
|
|
8110
|
+
runAuthoringRules,
|
|
8111
|
+
runRuntimeAuthoringRules,
|
|
8112
|
+
runtimeAuthoringRulesFor,
|
|
8113
|
+
runtimeGatedTypes,
|
|
8114
|
+
splitBySeverity,
|
|
8115
|
+
stackKeyForType,
|
|
5903
8116
|
validateActionBodyWrites,
|
|
8117
|
+
validateActionLocations,
|
|
5904
8118
|
validateActionNameRefs,
|
|
5905
8119
|
validateAiAgentAuthoring,
|
|
5906
8120
|
validateAiSurfaceAffinity,
|
|
@@ -5914,10 +8128,12 @@ export {
|
|
|
5914
8128
|
validateFlowTemplatePaths,
|
|
5915
8129
|
validateFlowTriggerReadiness,
|
|
5916
8130
|
validateFormLayout,
|
|
8131
|
+
validateFunctionalCompleteness,
|
|
5917
8132
|
validateHookBodyWrites,
|
|
5918
8133
|
validateJsxPages,
|
|
5919
8134
|
validateListViewMode,
|
|
5920
8135
|
validateNavAccess,
|
|
8136
|
+
validateNavTargetRefs,
|
|
5921
8137
|
validateObjectReferences,
|
|
5922
8138
|
validateOrgAxisRedLines,
|
|
5923
8139
|
validatePageFieldBindings,
|