@objectstack/lint 17.0.0-rc.1 → 17.0.0-rc.3
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 +697 -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.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(index_exports, {
|
|
|
25
25
|
ACTION_BODY_WRITE_PATTERN_IDS: () => ACTION_BODY_WRITE_PATTERN_IDS,
|
|
26
26
|
ACTION_BODY_WRITE_UNKNOWN_FIELD: () => ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
27
27
|
ACTION_NAME_UNDEFINED: () => ACTION_NAME_UNDEFINED,
|
|
28
|
+
ACTION_NO_PLACEMENT: () => ACTION_NO_PLACEMENT,
|
|
28
29
|
ACTION_RECORD_WRITE_DISCARDED: () => ACTION_RECORD_WRITE_DISCARDED,
|
|
29
30
|
ACTION_RECORD_WRITE_PATTERNS: () => ACTION_RECORD_WRITE_PATTERNS,
|
|
30
31
|
ACTION_RECORD_WRITE_PATTERN_IDS: () => ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
@@ -40,6 +41,13 @@ __export(index_exports, {
|
|
|
40
41
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET: () => APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
41
42
|
APPROVAL_EXPRESSION_INVALID: () => APPROVAL_EXPRESSION_INVALID,
|
|
42
43
|
APPROVAL_EXPRESSION_NO_EMPTY_POLICY: () => APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
|
|
44
|
+
AUTHORING_COMMANDS: () => AUTHORING_COMMANDS,
|
|
45
|
+
AUTHORING_RULES: () => AUTHORING_RULES,
|
|
46
|
+
AUTHORING_SURFACES: () => AUTHORING_SURFACES,
|
|
47
|
+
AUTONUMBER_LITERAL_TOKEN: () => AUTONUMBER_LITERAL_TOKEN,
|
|
48
|
+
AUTONUMBER_OPTIONAL_FIELD: () => AUTONUMBER_OPTIONAL_FIELD,
|
|
49
|
+
AUTONUMBER_SELF_REFERENCE: () => AUTONUMBER_SELF_REFERENCE,
|
|
50
|
+
AUTONUMBER_UNKNOWN_FIELD: () => AUTONUMBER_UNKNOWN_FIELD,
|
|
43
51
|
CAPABILITY_REFERENCE_UNKNOWN: () => CAPABILITY_REFERENCE_UNKNOWN,
|
|
44
52
|
CHART_AXIS_NOT_SELECTED: () => CHART_AXIS_NOT_SELECTED,
|
|
45
53
|
CHART_CONFIG_MISSING: () => CHART_CONFIG_MISSING,
|
|
@@ -50,13 +58,29 @@ __export(index_exports, {
|
|
|
50
58
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
51
59
|
DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
52
60
|
DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
61
|
+
EXPRESSION_INVALID: () => EXPRESSION_INVALID,
|
|
53
62
|
FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
|
|
54
63
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
55
64
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
65
|
+
FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
|
|
66
|
+
FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
|
|
67
|
+
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE: () => FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
68
|
+
FLOW_BARE_DOLLAR_REF: () => FLOW_BARE_DOLLAR_REF,
|
|
69
|
+
FLOW_BRANCH_LABEL_UNMATCHED: () => FLOW_BRANCH_LABEL_UNMATCHED,
|
|
70
|
+
FLOW_DATE_EQUALITY_FILTER: () => FLOW_DATE_EQUALITY_FILTER,
|
|
71
|
+
FLOW_DECISION_UNCONDITIONAL_BRANCH: () => FLOW_DECISION_UNCONDITIONAL_BRANCH,
|
|
72
|
+
FLOW_DEFAULT_EDGE_WITH_CONDITION: () => FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
73
|
+
FLOW_DOUBLE_BRACE_INTERP: () => FLOW_DOUBLE_BRACE_INTERP,
|
|
56
74
|
FLOW_DRAFT_STATUS_AMBIGUOUS: () => FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
75
|
+
FLOW_ERROR_LABEL_NOT_FAULT: () => FLOW_ERROR_LABEL_NOT_FAULT,
|
|
76
|
+
FLOW_INERT_NODE_CONDITION: () => FLOW_INERT_NODE_CONDITION,
|
|
77
|
+
FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
57
78
|
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
79
|
+
FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
|
|
80
|
+
FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
|
|
58
81
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
59
82
|
FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
83
|
+
FLOW_TIME_RELATIVE_ANTIPATTERN: () => FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
60
84
|
FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
61
85
|
FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
|
|
62
86
|
FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -69,19 +93,25 @@ __export(index_exports, {
|
|
|
69
93
|
HOOK_BODY_WRITE_PATTERN_IDS: () => HOOK_BODY_WRITE_PATTERN_IDS,
|
|
70
94
|
HOOK_BODY_WRITE_UNKNOWN_FIELD: () => HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
71
95
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
96
|
+
LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
|
|
97
|
+
LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
72
98
|
MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
|
|
73
99
|
NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
|
|
100
|
+
NAV_TARGET_UNRESOLVED: () => NAV_TARGET_UNRESOLVED,
|
|
101
|
+
NULL_GUARD_HINT: () => NULL_GUARD_HINT,
|
|
74
102
|
OBJECT_REFERENCE_UNKNOWN: () => OBJECT_REFERENCE_UNKNOWN,
|
|
75
103
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM: () => OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
76
104
|
ORG_AXIS_CROSS_ORG_BU_GRANT: () => ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
77
105
|
ORG_AXIS_PERMISSION_INHERITANCE: () => ORG_AXIS_PERMISSION_INHERITANCE,
|
|
78
106
|
PAGE_FIELD_UNKNOWN: () => PAGE_FIELD_UNKNOWN,
|
|
79
107
|
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
108
|
+
REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
80
109
|
REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
|
|
81
110
|
REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
|
|
82
111
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
83
112
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
84
113
|
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
114
|
+
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
85
115
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
86
116
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
87
117
|
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -106,17 +136,37 @@ __export(index_exports, {
|
|
|
106
136
|
TITLE_UNRESOLVABLE: () => TITLE_UNRESOLVABLE,
|
|
107
137
|
TRANSLATION_OPTION_KEY_UNKNOWN: () => TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
108
138
|
TRANSLATION_TARGET_UNKNOWN: () => TRANSLATION_TARGET_UNKNOWN,
|
|
139
|
+
UNIQUE_DOUBLE_DECLARATION: () => UNIQUE_DOUBLE_DECLARATION,
|
|
109
140
|
VIEW_CONTAINER_SHAPE: () => VIEW_CONTAINER_SHAPE,
|
|
141
|
+
VIEW_KEY_COLLISION: () => VIEW_KEY_COLLISION,
|
|
142
|
+
VIEW_REF_FORM_TARGET_KIND: () => VIEW_REF_FORM_TARGET_KIND,
|
|
143
|
+
VIEW_REF_FORM_TARGET_MISSING: () => VIEW_REF_FORM_TARGET_MISSING,
|
|
110
144
|
VISIBILITY_ALIAS_DEPRECATED: () => VISIBILITY_ALIAS_DEPRECATED,
|
|
111
145
|
VISIBILITY_ROOT_MISLAYERED: () => VISIBILITY_ROOT_MISLAYERED,
|
|
112
146
|
WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
|
|
113
147
|
WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
|
|
114
148
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
149
|
+
authoringRulesFor: () => authoringRulesFor,
|
|
115
150
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
116
151
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
117
152
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
118
153
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
154
|
+
findUnguardedNullableOperands: () => findUnguardedNullableOperands,
|
|
155
|
+
lintAutonumberFormats: () => lintAutonumberFormats,
|
|
156
|
+
lintDataModel: () => lintDataModel,
|
|
157
|
+
lintFlowPatterns: () => lintFlowPatterns,
|
|
158
|
+
lintLivenessProperties: () => lintLivenessProperties,
|
|
159
|
+
lintUniqueDeclarations: () => lintUniqueDeclarations,
|
|
160
|
+
lintViewRefs: () => lintViewRefs,
|
|
161
|
+
nullGuardMessage: () => nullGuardMessage,
|
|
162
|
+
runAuthoringRules: () => runAuthoringRules,
|
|
163
|
+
runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
|
|
164
|
+
runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
|
|
165
|
+
runtimeGatedTypes: () => runtimeGatedTypes,
|
|
166
|
+
splitBySeverity: () => splitBySeverity,
|
|
167
|
+
stackKeyForType: () => stackKeyForType,
|
|
119
168
|
validateActionBodyWrites: () => validateActionBodyWrites,
|
|
169
|
+
validateActionLocations: () => validateActionLocations,
|
|
120
170
|
validateActionNameRefs: () => validateActionNameRefs,
|
|
121
171
|
validateAiAgentAuthoring: () => validateAiAgentAuthoring,
|
|
122
172
|
validateAiSurfaceAffinity: () => validateAiSurfaceAffinity,
|
|
@@ -130,10 +180,12 @@ __export(index_exports, {
|
|
|
130
180
|
validateFlowTemplatePaths: () => validateFlowTemplatePaths,
|
|
131
181
|
validateFlowTriggerReadiness: () => validateFlowTriggerReadiness,
|
|
132
182
|
validateFormLayout: () => validateFormLayout,
|
|
183
|
+
validateFunctionalCompleteness: () => validateFunctionalCompleteness,
|
|
133
184
|
validateHookBodyWrites: () => validateHookBodyWrites,
|
|
134
185
|
validateJsxPages: () => validateJsxPages,
|
|
135
186
|
validateListViewMode: () => validateListViewMode,
|
|
136
187
|
validateNavAccess: () => validateNavAccess,
|
|
188
|
+
validateNavTargetRefs: () => validateNavTargetRefs,
|
|
137
189
|
validateObjectReferences: () => validateObjectReferences,
|
|
138
190
|
validateOrgAxisRedLines: () => validateOrgAxisRedLines,
|
|
139
191
|
validatePageFieldBindings: () => validatePageFieldBindings,
|
|
@@ -335,7 +387,7 @@ function validateWidgetBindings(stack) {
|
|
|
335
387
|
const where = `dashboard "${dashName}" \u203A widget "${widgetId}"`;
|
|
336
388
|
const path = `dashboards[${i}].widgets[${j}]`;
|
|
337
389
|
const suppressed = (rule) => Array.isArray(w.suppressWarnings) && w.suppressWarnings.includes(rule);
|
|
338
|
-
const
|
|
390
|
+
const push2 = (f) => {
|
|
339
391
|
if (f.severity === "warning" && suppressed(f.rule)) return;
|
|
340
392
|
findings.push({ ...f, where, path });
|
|
341
393
|
};
|
|
@@ -347,14 +399,14 @@ function validateWidgetBindings(stack) {
|
|
|
347
399
|
const plural = legacyUsed.length > 1;
|
|
348
400
|
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\`).`;
|
|
349
401
|
if (!hasDataSource) {
|
|
350
|
-
|
|
402
|
+
push2({
|
|
351
403
|
severity: "error",
|
|
352
404
|
rule: WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
353
405
|
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.`,
|
|
354
406
|
hint: `${datasetHint} The renderer ignores the legacy keys, so without a data source this widget has no data at all.`
|
|
355
407
|
});
|
|
356
408
|
} else {
|
|
357
|
-
|
|
409
|
+
push2({
|
|
358
410
|
severity: "warning",
|
|
359
411
|
rule: WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
360
412
|
message: `sets legacy analytics key${plural ? "s" : ""} ${keyList} that the ADR-0021 single-form cutover removed \u2014 the dashboard renderer ignores ${plural ? "them" : "it"}.`,
|
|
@@ -365,7 +417,7 @@ function validateWidgetBindings(stack) {
|
|
|
365
417
|
const dsName = typeof w.dataset === "string" ? w.dataset : void 0;
|
|
366
418
|
const dataset = dsName ? datasets.get(dsName) : void 0;
|
|
367
419
|
if (dsName && !dataset) {
|
|
368
|
-
|
|
420
|
+
push2({
|
|
369
421
|
severity: "error",
|
|
370
422
|
rule: WIDGET_DATASET_UNKNOWN,
|
|
371
423
|
message: `dataset "${dsName}" does not resolve to a declared dataset.`,
|
|
@@ -373,7 +425,7 @@ function validateWidgetBindings(stack) {
|
|
|
373
425
|
});
|
|
374
426
|
}
|
|
375
427
|
if (!dsName) {
|
|
376
|
-
|
|
428
|
+
push2({
|
|
377
429
|
severity: "error",
|
|
378
430
|
rule: WIDGET_DATASET_UNKNOWN,
|
|
379
431
|
message: `binds no \`dataset\` \u2014 the ADR-0021 widget shape requires one, so this widget resolves no data and renders empty.`,
|
|
@@ -392,7 +444,7 @@ function validateWidgetBindings(stack) {
|
|
|
392
444
|
const field = eff.field;
|
|
393
445
|
if (field.includes(".")) continue;
|
|
394
446
|
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) continue;
|
|
395
|
-
|
|
447
|
+
push2({
|
|
396
448
|
severity: "error",
|
|
397
449
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
398
450
|
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}\`.`,
|
|
@@ -412,7 +464,7 @@ function validateWidgetBindings(stack) {
|
|
|
412
464
|
const dims = asStrings(w.dimensions);
|
|
413
465
|
for (let k = 0; k < dims.length; k++) {
|
|
414
466
|
if (dimensionNames.has(dims[k])) continue;
|
|
415
|
-
|
|
467
|
+
push2({
|
|
416
468
|
severity: "error",
|
|
417
469
|
rule: WIDGET_DIMENSION_UNKNOWN,
|
|
418
470
|
message: `dimensions[${k}] "${dims[k]}" is not a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
|
|
@@ -422,7 +474,7 @@ function validateWidgetBindings(stack) {
|
|
|
422
474
|
const values = asStrings(w.values);
|
|
423
475
|
for (let k = 0; k < values.length; k++) {
|
|
424
476
|
if (measures.has(values[k])) continue;
|
|
425
|
-
|
|
477
|
+
push2({
|
|
426
478
|
severity: "error",
|
|
427
479
|
rule: WIDGET_MEASURE_UNKNOWN,
|
|
428
480
|
message: `values[${k}] "${values[k]}" is not a measure of dataset "${dsName}" (declared measures: ${list(measures.keys())}).`,
|
|
@@ -435,7 +487,7 @@ function validateWidgetBindings(stack) {
|
|
|
435
487
|
const selectedValues = new Set(values.filter((v) => measures.has(v)));
|
|
436
488
|
const xAxis = chartConfig.xAxis && typeof chartConfig.xAxis === "object" ? chartConfig.xAxis : void 0;
|
|
437
489
|
if (xAxis && typeof xAxis.field === "string" && !dimensionNames.has(xAxis.field) && !dims.includes(xAxis.field)) {
|
|
438
|
-
|
|
490
|
+
push2({
|
|
439
491
|
severity: "error",
|
|
440
492
|
rule: CHART_FIELD_UNKNOWN,
|
|
441
493
|
message: `chartConfig.xAxis.field "${xAxis.field}" does not resolve to a dimension of dataset "${dsName}" (declared dimensions: ${list(dimensionNames)}).`,
|
|
@@ -445,7 +497,7 @@ function validateWidgetBindings(stack) {
|
|
|
445
497
|
const measureField = (label2, field) => {
|
|
446
498
|
if (values.includes(field)) return;
|
|
447
499
|
const declaredButUnselected = measures.has(field);
|
|
448
|
-
|
|
500
|
+
push2({
|
|
449
501
|
severity: "error",
|
|
450
502
|
rule: CHART_FIELD_UNKNOWN,
|
|
451
503
|
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())}).`,
|
|
@@ -463,7 +515,7 @@ function validateWidgetBindings(stack) {
|
|
|
463
515
|
if (typeof name === "string") measureField(`series[${k}].name`, name);
|
|
464
516
|
}
|
|
465
517
|
} else if (isChartType) {
|
|
466
|
-
|
|
518
|
+
push2({
|
|
467
519
|
severity: "warning",
|
|
468
520
|
rule: CHART_CONFIG_MISSING,
|
|
469
521
|
message: `chart-type widget ('${w.type}') has no chartConfig \u2014 the renderer cannot determine which measure to plot, so the series renders empty.`,
|
|
@@ -477,7 +529,7 @@ function validateWidgetBindings(stack) {
|
|
|
477
529
|
if (resolved.some((m) => !m)) continue;
|
|
478
530
|
const countOnly = resolved.every((m) => m.aggregate === "count" && !m.derived);
|
|
479
531
|
if (!countOnly) continue;
|
|
480
|
-
|
|
532
|
+
push2({
|
|
481
533
|
severity: "warning",
|
|
482
534
|
rule: TABLE_COUNT_ONLY,
|
|
483
535
|
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.`,
|
|
@@ -491,6 +543,226 @@ function validateWidgetBindings(stack) {
|
|
|
491
543
|
// src/validate-expressions.ts
|
|
492
544
|
var import_formula = require("@objectstack/formula");
|
|
493
545
|
var import_automation = require("@objectstack/spec/automation");
|
|
546
|
+
|
|
547
|
+
// src/validate-null-guards.ts
|
|
548
|
+
var import_cel_js = require("@marcbachmann/cel-js");
|
|
549
|
+
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.`;
|
|
550
|
+
var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
|
|
551
|
+
var DEFAULT_RECORD_ROOTS = ["record", "previous"];
|
|
552
|
+
var parseEnv;
|
|
553
|
+
function getParseEnv() {
|
|
554
|
+
if (!parseEnv) {
|
|
555
|
+
parseEnv = new import_cel_js.Environment({ unlistedVariablesAreDyn: true, enableOptionalTypes: true });
|
|
556
|
+
}
|
|
557
|
+
return parseEnv;
|
|
558
|
+
}
|
|
559
|
+
function isNode(v) {
|
|
560
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
561
|
+
}
|
|
562
|
+
function isNullLiteral(node) {
|
|
563
|
+
return isNode(node) && node.op === "value" && node.args === null;
|
|
564
|
+
}
|
|
565
|
+
function fieldOf(node, roots) {
|
|
566
|
+
if (!isNode(node)) return null;
|
|
567
|
+
if (node.op !== "." && node.op !== ".?") return null;
|
|
568
|
+
const args = node.args;
|
|
569
|
+
if (!Array.isArray(args) || args.length < 2) return null;
|
|
570
|
+
const [recv, seg] = args;
|
|
571
|
+
if (typeof seg !== "string") return null;
|
|
572
|
+
if (!isNode(recv) || recv.op !== "id") return null;
|
|
573
|
+
const root = recv.args;
|
|
574
|
+
if (typeof root !== "string" || !roots.includes(root)) return null;
|
|
575
|
+
return { operand: `${root}.${seg}`, field: seg };
|
|
576
|
+
}
|
|
577
|
+
function childNodes(node) {
|
|
578
|
+
const args = node.args;
|
|
579
|
+
if (isNode(args)) return [args];
|
|
580
|
+
if (!Array.isArray(args)) return [];
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const a of args) {
|
|
583
|
+
if (isNode(a)) out.push(a);
|
|
584
|
+
else if (Array.isArray(a)) {
|
|
585
|
+
for (const b of a) if (isNode(b)) out.push(b);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
}
|
|
590
|
+
function callName(node) {
|
|
591
|
+
if (node.op !== "call") return null;
|
|
592
|
+
const args = node.args;
|
|
593
|
+
if (!Array.isArray(args) || typeof args[0] !== "string") return null;
|
|
594
|
+
return args[0];
|
|
595
|
+
}
|
|
596
|
+
function callArgs(node) {
|
|
597
|
+
const args = node.args;
|
|
598
|
+
if (!Array.isArray(args) || !Array.isArray(args[1])) return [];
|
|
599
|
+
return args[1];
|
|
600
|
+
}
|
|
601
|
+
function union(a, b) {
|
|
602
|
+
return /* @__PURE__ */ new Set([...a, ...b]);
|
|
603
|
+
}
|
|
604
|
+
function intersect(a, b) {
|
|
605
|
+
const out = /* @__PURE__ */ new Set();
|
|
606
|
+
for (const v of a) if (b.has(v)) out.add(v);
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
function truthGuards(node, roots) {
|
|
610
|
+
if (!isNode(node)) return /* @__PURE__ */ new Set();
|
|
611
|
+
switch (node.op) {
|
|
612
|
+
case "!=": {
|
|
613
|
+
const [l, r] = node.args ?? [];
|
|
614
|
+
if (isNullLiteral(r)) {
|
|
615
|
+
const f = fieldOf(l, roots);
|
|
616
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
617
|
+
}
|
|
618
|
+
if (isNullLiteral(l)) {
|
|
619
|
+
const f = fieldOf(r, roots);
|
|
620
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
621
|
+
}
|
|
622
|
+
return /* @__PURE__ */ new Set();
|
|
623
|
+
}
|
|
624
|
+
case "&&": {
|
|
625
|
+
const [l, r] = node.args;
|
|
626
|
+
return union(truthGuards(l, roots), truthGuards(r, roots));
|
|
627
|
+
}
|
|
628
|
+
case "||": {
|
|
629
|
+
const [l, r] = node.args;
|
|
630
|
+
return intersect(truthGuards(l, roots), truthGuards(r, roots));
|
|
631
|
+
}
|
|
632
|
+
case "!_":
|
|
633
|
+
return falseGuards(node.args, roots);
|
|
634
|
+
case "?:": {
|
|
635
|
+
const [, t, f] = node.args;
|
|
636
|
+
return intersect(truthGuards(t, roots), truthGuards(f, roots));
|
|
637
|
+
}
|
|
638
|
+
default:
|
|
639
|
+
return /* @__PURE__ */ new Set();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function falseGuards(node, roots) {
|
|
643
|
+
if (!isNode(node)) return /* @__PURE__ */ new Set();
|
|
644
|
+
switch (node.op) {
|
|
645
|
+
case "==": {
|
|
646
|
+
const [l, r] = node.args ?? [];
|
|
647
|
+
if (isNullLiteral(r)) {
|
|
648
|
+
const f = fieldOf(l, roots);
|
|
649
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
650
|
+
}
|
|
651
|
+
if (isNullLiteral(l)) {
|
|
652
|
+
const f = fieldOf(r, roots);
|
|
653
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
654
|
+
}
|
|
655
|
+
return /* @__PURE__ */ new Set();
|
|
656
|
+
}
|
|
657
|
+
case "||": {
|
|
658
|
+
const [l, r] = node.args;
|
|
659
|
+
return union(falseGuards(l, roots), falseGuards(r, roots));
|
|
660
|
+
}
|
|
661
|
+
case "&&": {
|
|
662
|
+
const [l, r] = node.args;
|
|
663
|
+
return intersect(falseGuards(l, roots), falseGuards(r, roots));
|
|
664
|
+
}
|
|
665
|
+
case "!_":
|
|
666
|
+
return truthGuards(node.args, roots);
|
|
667
|
+
case "call": {
|
|
668
|
+
if (callName(node) !== "isBlank") return /* @__PURE__ */ new Set();
|
|
669
|
+
const [only] = callArgs(node);
|
|
670
|
+
const f = fieldOf(only, roots);
|
|
671
|
+
return f ? /* @__PURE__ */ new Set([f.operand]) : /* @__PURE__ */ new Set();
|
|
672
|
+
}
|
|
673
|
+
default:
|
|
674
|
+
return /* @__PURE__ */ new Set();
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function collectHasOperands(node, roots, out) {
|
|
678
|
+
if (!isNode(node)) return;
|
|
679
|
+
if (callName(node) === "has") {
|
|
680
|
+
for (const a of callArgs(node)) {
|
|
681
|
+
const f = fieldOf(a, roots);
|
|
682
|
+
if (f) out.add(f.operand);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const child of childNodes(node)) collectHasOperands(child, roots, out);
|
|
686
|
+
}
|
|
687
|
+
function findUnguardedNullableOperands(source, opts) {
|
|
688
|
+
if (typeof source !== "string" || !source.trim()) return [];
|
|
689
|
+
if (opts.nullableFields.size === 0) return [];
|
|
690
|
+
const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
|
|
691
|
+
let ast;
|
|
692
|
+
try {
|
|
693
|
+
ast = getParseEnv().parse(source).ast;
|
|
694
|
+
} catch {
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
const hasOperands = /* @__PURE__ */ new Set();
|
|
698
|
+
collectHasOperands(ast, roots, hasOperands);
|
|
699
|
+
const findings = [];
|
|
700
|
+
const seen = /* @__PURE__ */ new Set();
|
|
701
|
+
const report = (operandNode, operator, guards) => {
|
|
702
|
+
const f = fieldOf(operandNode, roots);
|
|
703
|
+
if (!f) return;
|
|
704
|
+
if (!opts.nullableFields.has(f.field)) return;
|
|
705
|
+
if (guards.has(f.operand)) return;
|
|
706
|
+
const key = `${f.operand}\0${operator}`;
|
|
707
|
+
if (seen.has(key)) return;
|
|
708
|
+
seen.add(key);
|
|
709
|
+
findings.push({
|
|
710
|
+
operand: f.operand,
|
|
711
|
+
field: f.field,
|
|
712
|
+
operator,
|
|
713
|
+
hasOnlyGuard: hasOperands.has(f.operand)
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
const visit = (node, guards) => {
|
|
717
|
+
if (!isNode(node)) return;
|
|
718
|
+
const op = node.op;
|
|
719
|
+
if (op === "&&") {
|
|
720
|
+
const [l, r] = node.args;
|
|
721
|
+
visit(l, guards);
|
|
722
|
+
visit(r, union(guards, truthGuards(l, roots)));
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (op === "||") {
|
|
726
|
+
const [l, r] = node.args;
|
|
727
|
+
visit(l, guards);
|
|
728
|
+
visit(r, union(guards, falseGuards(l, roots)));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (op === "?:") {
|
|
732
|
+
const [c, t, f] = node.args;
|
|
733
|
+
visit(c, guards);
|
|
734
|
+
visit(t, union(guards, truthGuards(c, roots)));
|
|
735
|
+
visit(f, union(guards, falseGuards(c, roots)));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
if (op === "call" && callName(node) === "has") {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
if (FAULTING_BINARY_OPS.has(op)) {
|
|
742
|
+
const [l, r] = node.args;
|
|
743
|
+
report(l, op, guards);
|
|
744
|
+
report(r, op, guards);
|
|
745
|
+
visit(l, guards);
|
|
746
|
+
visit(r, guards);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
if (op === "-_") {
|
|
750
|
+
report(node.args, "-", guards);
|
|
751
|
+
visit(node.args, guards);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
for (const child of childNodes(node)) visit(child, guards);
|
|
755
|
+
};
|
|
756
|
+
visit(ast, /* @__PURE__ */ new Set());
|
|
757
|
+
return findings;
|
|
758
|
+
}
|
|
759
|
+
function nullGuardMessage(subject, objectName, finding) {
|
|
760
|
+
const owner = objectName ? `'${objectName}'` : "this object";
|
|
761
|
+
const hasNote = finding.hasOnlyGuard ? ` \`has(${finding.operand})\` does not guard it.` : "";
|
|
762
|
+
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}`;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/validate-expressions.ts
|
|
494
766
|
function asArray2(v) {
|
|
495
767
|
if (Array.isArray(v)) return v;
|
|
496
768
|
if (v && typeof v === "object") {
|
|
@@ -534,11 +806,84 @@ function buildFieldTypeIndex(objects) {
|
|
|
534
806
|
}
|
|
535
807
|
return idx;
|
|
536
808
|
}
|
|
809
|
+
function fieldEntries(obj) {
|
|
810
|
+
const fields = obj.fields;
|
|
811
|
+
if (Array.isArray(fields)) {
|
|
812
|
+
return fields.filter((f) => f && typeof f === "object" && typeof f.name === "string").map((f) => [f.name, f]);
|
|
813
|
+
}
|
|
814
|
+
if (fields && typeof fields === "object") {
|
|
815
|
+
return Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]);
|
|
816
|
+
}
|
|
817
|
+
return [];
|
|
818
|
+
}
|
|
819
|
+
function isNullableField(def) {
|
|
820
|
+
if (def.required === true) return false;
|
|
821
|
+
if (def.defaultValue !== void 0 && def.defaultValue !== null) return false;
|
|
822
|
+
if (def.type === "autonumber") return false;
|
|
823
|
+
const options = def.options;
|
|
824
|
+
if (Array.isArray(options) && options.some((o) => !!o && typeof o === "object" && o.default === true)) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
function buildNullableFieldIndex(objects) {
|
|
830
|
+
const idx = /* @__PURE__ */ new Map();
|
|
831
|
+
for (const obj of objects) {
|
|
832
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
833
|
+
if (!name) continue;
|
|
834
|
+
const nullable = /* @__PURE__ */ new Set();
|
|
835
|
+
for (const [fname, def] of fieldEntries(obj)) {
|
|
836
|
+
if (isNullableField(def)) nullable.add(fname);
|
|
837
|
+
}
|
|
838
|
+
idx.set(name, nullable);
|
|
839
|
+
}
|
|
840
|
+
return idx;
|
|
841
|
+
}
|
|
842
|
+
function celSourceOf(raw) {
|
|
843
|
+
if (typeof raw === "string") return raw;
|
|
844
|
+
if (raw && typeof raw === "object") {
|
|
845
|
+
const rec = raw;
|
|
846
|
+
if (typeof rec.dialect === "string" && rec.dialect !== "cel") return void 0;
|
|
847
|
+
if (typeof rec.source === "string") return rec.source;
|
|
848
|
+
}
|
|
849
|
+
return void 0;
|
|
850
|
+
}
|
|
851
|
+
function rulePredicates(rule, path) {
|
|
852
|
+
const out = [];
|
|
853
|
+
const name = typeof rule.name === "string" ? rule.name : "?";
|
|
854
|
+
const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
|
|
855
|
+
const main = rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula;
|
|
856
|
+
if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
|
|
857
|
+
if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
|
|
858
|
+
for (const branch of ["then", "otherwise"]) {
|
|
859
|
+
const nested = rule[branch];
|
|
860
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
861
|
+
out.push(...rulePredicates(nested, `${here} ${branch}`));
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
537
866
|
function validateStackExpressions(stack) {
|
|
538
867
|
const issues = [];
|
|
539
868
|
const objects = asArray2(stack.objects);
|
|
540
869
|
const fieldIndex = buildFieldIndex(objects);
|
|
541
870
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
871
|
+
const nullableIndex = buildNullableFieldIndex(objects);
|
|
872
|
+
const checkNullGuards = (where, subject, raw, objectName) => {
|
|
873
|
+
if (!objectName) return;
|
|
874
|
+
const nullableFields = nullableIndex.get(objectName);
|
|
875
|
+
if (!nullableFields || nullableFields.size === 0) return;
|
|
876
|
+
const source = celSourceOf(raw);
|
|
877
|
+
if (!source) return;
|
|
878
|
+
for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
879
|
+
issues.push({
|
|
880
|
+
where,
|
|
881
|
+
message: nullGuardMessage(subject, objectName, finding),
|
|
882
|
+
source,
|
|
883
|
+
severity: "error"
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
};
|
|
542
887
|
const check = (where, raw, objectName, scope = "flattened") => {
|
|
543
888
|
if (raw == null) return;
|
|
544
889
|
const fields = objectName ? fieldIndex.get(objectName) : void 0;
|
|
@@ -579,17 +924,17 @@ function validateStackExpressions(stack) {
|
|
|
579
924
|
if (node.type === "script") {
|
|
580
925
|
const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
|
|
581
926
|
const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
|
|
582
|
-
const
|
|
583
|
-
if (
|
|
927
|
+
const retired = ["actionType", "template", "recipients", "variables", "script"].filter((k) => cfg[k] != null);
|
|
928
|
+
if (retired.length > 0) {
|
|
584
929
|
issues.push({
|
|
585
930
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
586
|
-
message: `script node
|
|
931
|
+
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.`,
|
|
587
932
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
588
933
|
});
|
|
589
|
-
} else if (
|
|
934
|
+
} else if (!fn) {
|
|
590
935
|
issues.push({
|
|
591
936
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
592
|
-
message: `script node
|
|
937
|
+
message: `script node declares no \`function\` \u2014 it would do nothing at runtime. Name a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
|
|
593
938
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
594
939
|
});
|
|
595
940
|
}
|
|
@@ -607,6 +952,9 @@ function validateStackExpressions(stack) {
|
|
|
607
952
|
const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
|
|
608
953
|
check(where, rule.expression ?? rule.predicate ?? rule.condition ?? rule.formula, objectName, "record");
|
|
609
954
|
check(`${where} when`, rule.when, objectName, "record");
|
|
955
|
+
for (const p of rulePredicates(rule, "")) {
|
|
956
|
+
checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
|
|
957
|
+
}
|
|
610
958
|
}
|
|
611
959
|
const fields = obj.fields;
|
|
612
960
|
const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.values(fields) : [];
|
|
@@ -659,6 +1007,12 @@ function validateStackExpressions(stack) {
|
|
|
659
1007
|
const hookName = hook.name ?? "?";
|
|
660
1008
|
if (typeof hook.object === "string") {
|
|
661
1009
|
check(`hook '${hookName}' (${hook.object}) condition`, hook.condition, hook.object, "record");
|
|
1010
|
+
checkNullGuards(
|
|
1011
|
+
`hook '${hookName}' (${hook.object}) condition`,
|
|
1012
|
+
`hook '${hookName}' condition`,
|
|
1013
|
+
hook.condition,
|
|
1014
|
+
hook.object
|
|
1015
|
+
);
|
|
662
1016
|
continue;
|
|
663
1017
|
}
|
|
664
1018
|
const targets = Array.isArray(hook.object) ? hook.object.filter((o) => typeof o === "string" && o !== "*") : [];
|
|
@@ -672,6 +1026,12 @@ function validateStackExpressions(stack) {
|
|
|
672
1026
|
for (const target of targets) {
|
|
673
1027
|
const mark = issues.length;
|
|
674
1028
|
check(`hook '${hookName}' (${target}) condition`, hook.condition, target, "record");
|
|
1029
|
+
checkNullGuards(
|
|
1030
|
+
`hook '${hookName}' (${target}) condition`,
|
|
1031
|
+
`hook '${hookName}' condition`,
|
|
1032
|
+
hook.condition,
|
|
1033
|
+
target
|
|
1034
|
+
);
|
|
675
1035
|
for (let i = mark; i < issues.length; i++) {
|
|
676
1036
|
const issue = issues[i];
|
|
677
1037
|
const key = `${issue.message}\0${issue.source ?? ""}`;
|
|
@@ -753,6 +1113,72 @@ function validateListViewMode(stack) {
|
|
|
753
1113
|
return out;
|
|
754
1114
|
}
|
|
755
1115
|
|
|
1116
|
+
// src/validate-functional-completeness.ts
|
|
1117
|
+
var import_kernel = require("@objectstack/spec/kernel");
|
|
1118
|
+
var isRec = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1119
|
+
function entriesOf(v) {
|
|
1120
|
+
if (Array.isArray(v)) {
|
|
1121
|
+
return v.flatMap(
|
|
1122
|
+
(def, i) => isRec(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (isRec(v)) {
|
|
1126
|
+
return Object.entries(v).flatMap(
|
|
1127
|
+
([name, def]) => isRec(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
return [];
|
|
1131
|
+
}
|
|
1132
|
+
function push(out, found, where, basePath) {
|
|
1133
|
+
for (const f of found) {
|
|
1134
|
+
out.push({
|
|
1135
|
+
severity: f.severity,
|
|
1136
|
+
rule: f.rule,
|
|
1137
|
+
where,
|
|
1138
|
+
path: `${basePath}.${f.path}`,
|
|
1139
|
+
message: f.message,
|
|
1140
|
+
hint: f.fix
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function validateFunctionalCompleteness(stack) {
|
|
1145
|
+
const out = [];
|
|
1146
|
+
if (!isRec(stack)) return out;
|
|
1147
|
+
for (const [oi, obj] of entriesOf(stack.objects).entries()) {
|
|
1148
|
+
for (const field of entriesOf(obj.def.fields)) {
|
|
1149
|
+
push(
|
|
1150
|
+
out,
|
|
1151
|
+
(0, import_kernel.checkFieldCompleteness)(field.def),
|
|
1152
|
+
`object "${obj.name}" \u203A fields.${field.name}`,
|
|
1153
|
+
`objects[${oi}].fields${field.key}`
|
|
1154
|
+
);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
for (const [vi, container] of entriesOf(stack.views).entries()) {
|
|
1158
|
+
const where = container.def.object ? `view container "${container.name}"` : `view container [${vi}]`;
|
|
1159
|
+
if (isRec(container.def.list)) {
|
|
1160
|
+
push(out, (0, import_kernel.checkViewCompleteness)(container.def.list), `${where} \u203A list`, `views[${vi}].list`);
|
|
1161
|
+
}
|
|
1162
|
+
for (const lv of entriesOf(container.def.listViews)) {
|
|
1163
|
+
push(
|
|
1164
|
+
out,
|
|
1165
|
+
(0, import_kernel.checkViewCompleteness)(lv.def),
|
|
1166
|
+
`${where} \u203A listViews.${lv.name}`,
|
|
1167
|
+
`views[${vi}].listViews${lv.key}`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const hook of entriesOf(stack.webhooks)) {
|
|
1172
|
+
push(
|
|
1173
|
+
out,
|
|
1174
|
+
(0, import_kernel.checkWebhookCompleteness)(hook.def),
|
|
1175
|
+
`webhook "${hook.name}"`,
|
|
1176
|
+
`webhooks${hook.key}`
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
return out;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
756
1182
|
// src/validate-flow-trigger-readiness.ts
|
|
757
1183
|
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
758
1184
|
var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
@@ -852,7 +1278,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
852
1278
|
|
|
853
1279
|
// src/flow-walk.ts
|
|
854
1280
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
855
|
-
function
|
|
1281
|
+
function isRec2(v) {
|
|
856
1282
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
857
1283
|
}
|
|
858
1284
|
function strName(v) {
|
|
@@ -867,7 +1293,7 @@ function flowNodeLabel(node, index) {
|
|
|
867
1293
|
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
868
1294
|
}
|
|
869
1295
|
function stripRegions(config) {
|
|
870
|
-
if (!
|
|
1296
|
+
if (!isRec2(config)) return void 0;
|
|
871
1297
|
let out;
|
|
872
1298
|
for (const key of Object.keys(config)) {
|
|
873
1299
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -878,11 +1304,11 @@ function stripRegions(config) {
|
|
|
878
1304
|
}
|
|
879
1305
|
function walkFlowNodes(flow, flowPath) {
|
|
880
1306
|
const out = [];
|
|
881
|
-
if (!
|
|
1307
|
+
if (!isRec2(flow)) return out;
|
|
882
1308
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
883
1309
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
884
1310
|
nodes.forEach((raw, index) => {
|
|
885
|
-
if (!
|
|
1311
|
+
if (!isRec2(raw)) return;
|
|
886
1312
|
const path = `${basePath}[${index}]`;
|
|
887
1313
|
out.push({
|
|
888
1314
|
node: raw,
|
|
@@ -893,7 +1319,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
893
1319
|
});
|
|
894
1320
|
const type = strName(raw.type);
|
|
895
1321
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
896
|
-
if (!slots || !
|
|
1322
|
+
if (!slots || !isRec2(raw.config)) return;
|
|
897
1323
|
const config = raw.config;
|
|
898
1324
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
899
1325
|
for (const slot of slots) {
|
|
@@ -901,7 +1327,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
901
1327
|
if (slot === "branches") {
|
|
902
1328
|
if (!Array.isArray(value)) continue;
|
|
903
1329
|
value.forEach((branch, b) => {
|
|
904
|
-
if (!
|
|
1330
|
+
if (!isRec2(branch)) return;
|
|
905
1331
|
const branchName = strName(branch.name) ?? `#${b}`;
|
|
906
1332
|
visitList(
|
|
907
1333
|
branch.nodes,
|
|
@@ -912,7 +1338,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
912
1338
|
});
|
|
913
1339
|
continue;
|
|
914
1340
|
}
|
|
915
|
-
if (!
|
|
1341
|
+
if (!isRec2(value)) continue;
|
|
916
1342
|
visitList(
|
|
917
1343
|
value.nodes,
|
|
918
1344
|
`${path}.config.${slot}.nodes`,
|
|
@@ -1661,10 +2087,12 @@ function validateReactPages(stack) {
|
|
|
1661
2087
|
// src/validate-react-page-props.ts
|
|
1662
2088
|
var import_node_module2 = require("module");
|
|
1663
2089
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
1664
|
-
var
|
|
2090
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
1665
2091
|
|
|
1666
2092
|
// src/validate-searchable-fields.ts
|
|
2093
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
1667
2094
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
2095
|
+
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1668
2096
|
function asArray10(v) {
|
|
1669
2097
|
if (Array.isArray(v)) return v;
|
|
1670
2098
|
if (v && typeof v === "object") {
|
|
@@ -1672,21 +2100,50 @@ function asArray10(v) {
|
|
|
1672
2100
|
}
|
|
1673
2101
|
return [];
|
|
1674
2102
|
}
|
|
1675
|
-
function
|
|
2103
|
+
function isRec3(v) {
|
|
1676
2104
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1677
2105
|
}
|
|
1678
2106
|
function strName2(v) {
|
|
1679
2107
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1680
2108
|
}
|
|
1681
|
-
function
|
|
2109
|
+
function declaredFieldTarget(obj) {
|
|
1682
2110
|
const fields = obj.fields;
|
|
1683
2111
|
if (!fields || typeof fields !== "object") return null;
|
|
1684
2112
|
const names = /* @__PURE__ */ new Set();
|
|
2113
|
+
const metas = {};
|
|
1685
2114
|
for (const f of asArray10(fields)) {
|
|
1686
2115
|
const n = strName2(f.name);
|
|
1687
|
-
if (n)
|
|
2116
|
+
if (!n) continue;
|
|
2117
|
+
names.add(n);
|
|
2118
|
+
metas[n] = {
|
|
2119
|
+
type: typeof f.type === "string" ? f.type : void 0,
|
|
2120
|
+
hidden: f.hidden === true
|
|
2121
|
+
};
|
|
1688
2122
|
}
|
|
1689
|
-
|
|
2123
|
+
if (names.size === 0) return null;
|
|
2124
|
+
const searchableFields = Array.isArray(obj.searchableFields) ? obj.searchableFields.filter((e) => typeof e === "string") : void 0;
|
|
2125
|
+
return {
|
|
2126
|
+
names,
|
|
2127
|
+
fields: metas,
|
|
2128
|
+
searchableFields,
|
|
2129
|
+
displayField: strName2(obj.nameField) ?? strName2(obj.displayNameField)
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function resolveAllowedSet(target) {
|
|
2133
|
+
let fields = target.fields;
|
|
2134
|
+
const systemDeclared = (target.searchableFields ?? []).filter(
|
|
2135
|
+
(f) => !target.names.has(f) && SYSTEM_FIELDS.has(f)
|
|
2136
|
+
);
|
|
2137
|
+
if (systemDeclared.length > 0) {
|
|
2138
|
+
fields = { ...fields };
|
|
2139
|
+
for (const f of systemDeclared) fields[f] = {};
|
|
2140
|
+
}
|
|
2141
|
+
const { allowed, source } = (0, import_data3.resolveSearchFieldResolution)({
|
|
2142
|
+
fields,
|
|
2143
|
+
searchableFields: target.searchableFields,
|
|
2144
|
+
displayField: target.displayField
|
|
2145
|
+
});
|
|
2146
|
+
return { allowed: new Set(allowed), source, declaredList: allowed };
|
|
1690
2147
|
}
|
|
1691
2148
|
function suggest2(target, known) {
|
|
1692
2149
|
let best;
|
|
@@ -1719,50 +2176,87 @@ function distance(a, b) {
|
|
|
1719
2176
|
}
|
|
1720
2177
|
function indexObjectSearchTargets(stack) {
|
|
1721
2178
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1722
|
-
if (!
|
|
2179
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1723
2180
|
for (const obj of asArray10(stack.objects)) {
|
|
1724
2181
|
const name = strName2(obj.name);
|
|
1725
|
-
if (name) fieldsByObject.set(name,
|
|
2182
|
+
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
1726
2183
|
}
|
|
1727
2184
|
return fieldsByObject;
|
|
1728
2185
|
}
|
|
1729
|
-
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
|
|
2186
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role = "narrowing") {
|
|
1730
2187
|
const findings = [];
|
|
1731
2188
|
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1732
2189
|
if (!objectName) return findings;
|
|
1733
2190
|
if (!fieldsByObject.has(objectName)) return findings;
|
|
1734
|
-
const
|
|
1735
|
-
if (!
|
|
2191
|
+
const target = fieldsByObject.get(objectName);
|
|
2192
|
+
if (!target) return findings;
|
|
2193
|
+
const known = target.names;
|
|
2194
|
+
const resolution = role === "narrowing" ? resolveAllowedSet(target) : void 0;
|
|
1736
2195
|
for (let i = 0; i < declared.length; i++) {
|
|
1737
2196
|
const entry = declared[i];
|
|
1738
2197
|
const name = strName2(entry);
|
|
1739
2198
|
if (!name) continue;
|
|
1740
|
-
if (known.has(name)
|
|
1741
|
-
|
|
2199
|
+
if (!known.has(name) && !SYSTEM_FIELDS.has(name)) {
|
|
2200
|
+
const dotted = name.includes(".");
|
|
2201
|
+
findings.push({
|
|
2202
|
+
severity: "error",
|
|
2203
|
+
rule: SEARCHABLE_FIELD_UNKNOWN,
|
|
2204
|
+
where,
|
|
2205
|
+
path: `${path}[${i}]`,
|
|
2206
|
+
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)),
|
|
2207
|
+
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(", ")}.` : "")
|
|
2208
|
+
});
|
|
2209
|
+
continue;
|
|
2210
|
+
}
|
|
2211
|
+
if (!resolution || resolution.allowed.has(name)) continue;
|
|
2212
|
+
if (!known.has(name)) continue;
|
|
2213
|
+
const meta = target.fields[name];
|
|
2214
|
+
if (resolution.source === "declared") {
|
|
2215
|
+
findings.push({
|
|
2216
|
+
severity: "error",
|
|
2217
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
2218
|
+
where,
|
|
2219
|
+
path: `${path}[${i}]`,
|
|
2220
|
+
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).`,
|
|
2221
|
+
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).`
|
|
2222
|
+
});
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
2226
|
+
let why;
|
|
2227
|
+
if (import_data3.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
2228
|
+
why = "a system/audit column, which the auto-default set never includes";
|
|
2229
|
+
} else if (meta?.hidden) {
|
|
2230
|
+
why = "hidden";
|
|
2231
|
+
} else if (typeof meta?.type === "string") {
|
|
2232
|
+
why = `of type '${meta.type}', which 'search' cannot scan`;
|
|
2233
|
+
} else {
|
|
2234
|
+
continue;
|
|
2235
|
+
}
|
|
1742
2236
|
findings.push({
|
|
1743
2237
|
severity: "error",
|
|
1744
|
-
rule:
|
|
2238
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1745
2239
|
where,
|
|
1746
2240
|
path: `${path}[${i}]`,
|
|
1747
|
-
message: `${subject} entry "${name}"
|
|
1748
|
-
hint: (
|
|
2241
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data3.SEARCHABLE_TEXTUAL_TYPES, ...import_data3.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).`,
|
|
2242
|
+
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.`
|
|
1749
2243
|
});
|
|
1750
2244
|
}
|
|
1751
2245
|
return findings;
|
|
1752
2246
|
}
|
|
1753
2247
|
function validateSearchableFields(stack) {
|
|
1754
2248
|
const findings = [];
|
|
1755
|
-
if (!
|
|
2249
|
+
if (!isRec3(stack)) return findings;
|
|
1756
2250
|
const objects = asArray10(stack.objects);
|
|
1757
2251
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1758
|
-
const check = (declared, objectName, where, path, subject) => {
|
|
2252
|
+
const check = (declared, objectName, where, path, subject, role) => {
|
|
1759
2253
|
findings.push(
|
|
1760
|
-
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
|
|
2254
|
+
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject, role)
|
|
1761
2255
|
);
|
|
1762
2256
|
};
|
|
1763
2257
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1764
2258
|
const obj = objects[oi];
|
|
1765
|
-
if (!
|
|
2259
|
+
if (!isRec3(obj)) continue;
|
|
1766
2260
|
const objName = strName2(obj.name);
|
|
1767
2261
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1768
2262
|
check(
|
|
@@ -1770,11 +2264,12 @@ function validateSearchableFields(stack) {
|
|
|
1770
2264
|
objName,
|
|
1771
2265
|
label2,
|
|
1772
2266
|
`objects[${oi}].searchableFields`,
|
|
1773
|
-
"searchableFields"
|
|
2267
|
+
"searchableFields",
|
|
2268
|
+
"canonical"
|
|
1774
2269
|
);
|
|
1775
|
-
if (
|
|
2270
|
+
if (isRec3(obj.listViews)) {
|
|
1776
2271
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1777
|
-
if (!
|
|
2272
|
+
if (!isRec3(lv)) continue;
|
|
1778
2273
|
check(
|
|
1779
2274
|
lv.searchableFields,
|
|
1780
2275
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1782,7 +2277,8 @@ function validateSearchableFields(stack) {
|
|
|
1782
2277
|
listViewObject(lv) ?? objName,
|
|
1783
2278
|
`${label2} \u203A listViews.${key}`,
|
|
1784
2279
|
`objects[${oi}].listViews.${key}.searchableFields`,
|
|
1785
|
-
"list-view searchableFields"
|
|
2280
|
+
"list-view searchableFields",
|
|
2281
|
+
"narrowing"
|
|
1786
2282
|
);
|
|
1787
2283
|
}
|
|
1788
2284
|
}
|
|
@@ -1790,27 +2286,29 @@ function validateSearchableFields(stack) {
|
|
|
1790
2286
|
const views = asArray10(stack.views);
|
|
1791
2287
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1792
2288
|
const view = views[vi];
|
|
1793
|
-
if (!
|
|
2289
|
+
if (!isRec3(view)) continue;
|
|
1794
2290
|
const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
1795
2291
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
1796
|
-
if (
|
|
2292
|
+
if (isRec3(view.list)) {
|
|
1797
2293
|
check(
|
|
1798
2294
|
view.list.searchableFields,
|
|
1799
2295
|
listViewObject(view.list) ?? viewObject,
|
|
1800
2296
|
`view "${viewLabel}" \u203A list`,
|
|
1801
2297
|
`views[${vi}].list.searchableFields`,
|
|
1802
|
-
"list-view searchableFields"
|
|
2298
|
+
"list-view searchableFields",
|
|
2299
|
+
"narrowing"
|
|
1803
2300
|
);
|
|
1804
2301
|
}
|
|
1805
|
-
if (
|
|
2302
|
+
if (isRec3(view.listViews)) {
|
|
1806
2303
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1807
|
-
if (!
|
|
2304
|
+
if (!isRec3(lv)) continue;
|
|
1808
2305
|
check(
|
|
1809
2306
|
lv.searchableFields,
|
|
1810
2307
|
listViewObject(lv) ?? viewObject,
|
|
1811
2308
|
`view "${viewLabel}" \u203A listViews.${key}`,
|
|
1812
2309
|
`views[${vi}].listViews.${key}.searchableFields`,
|
|
1813
|
-
"list-view searchableFields"
|
|
2310
|
+
"list-view searchableFields",
|
|
2311
|
+
"narrowing"
|
|
1814
2312
|
);
|
|
1815
2313
|
}
|
|
1816
2314
|
}
|
|
@@ -1819,11 +2317,11 @@ function validateSearchableFields(stack) {
|
|
|
1819
2317
|
}
|
|
1820
2318
|
function listViewObject(listView) {
|
|
1821
2319
|
const data = listView.data;
|
|
1822
|
-
return
|
|
2320
|
+
return isRec3(data) ? strName2(data.object) : void 0;
|
|
1823
2321
|
}
|
|
1824
2322
|
|
|
1825
2323
|
// src/page-walk.ts
|
|
1826
|
-
function
|
|
2324
|
+
function isRec4(v) {
|
|
1827
2325
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1828
2326
|
}
|
|
1829
2327
|
function strName3(v) {
|
|
@@ -1836,19 +2334,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1836
2334
|
}
|
|
1837
2335
|
function walkPageComponents(page, pagePath) {
|
|
1838
2336
|
const out = [];
|
|
1839
|
-
if (!
|
|
2337
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1840
2338
|
const pageObject = strName3(page.object);
|
|
1841
2339
|
const visit = (node, path, inheritedObject) => {
|
|
1842
|
-
if (!
|
|
1843
|
-
const props =
|
|
1844
|
-
const dataSource =
|
|
2340
|
+
if (!isRec4(node)) return;
|
|
2341
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2342
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1845
2343
|
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
1846
2344
|
out.push({ component: node, path, objectName });
|
|
1847
2345
|
if (!props) return;
|
|
1848
2346
|
if (Array.isArray(props.items)) {
|
|
1849
2347
|
for (let i = 0; i < props.items.length; i++) {
|
|
1850
2348
|
const item = props.items[i];
|
|
1851
|
-
if (!
|
|
2349
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1852
2350
|
for (let c = 0; c < item.children.length; c++) {
|
|
1853
2351
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1854
2352
|
}
|
|
@@ -1870,12 +2368,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1870
2368
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1871
2369
|
for (let r = 0; r < regions.length; r++) {
|
|
1872
2370
|
const region = regions[r];
|
|
1873
|
-
if (!
|
|
2371
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1874
2372
|
for (let c = 0; c < region.components.length; c++) {
|
|
1875
2373
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1876
2374
|
}
|
|
1877
2375
|
}
|
|
1878
|
-
const slots =
|
|
2376
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1879
2377
|
if (slots) {
|
|
1880
2378
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1881
2379
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -1900,7 +2398,7 @@ function asArray11(v) {
|
|
|
1900
2398
|
function strName4(v) {
|
|
1901
2399
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1902
2400
|
}
|
|
1903
|
-
function
|
|
2401
|
+
function isRec5(v) {
|
|
1904
2402
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1905
2403
|
}
|
|
1906
2404
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -1911,7 +2409,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
1911
2409
|
out.push({ name: bare, path });
|
|
1912
2410
|
return;
|
|
1913
2411
|
}
|
|
1914
|
-
if (!
|
|
2412
|
+
if (!isRec5(v)) return;
|
|
1915
2413
|
const named = strName4(v.field) ?? strName4(v.name);
|
|
1916
2414
|
if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
|
|
1917
2415
|
};
|
|
@@ -1953,15 +2451,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
1953
2451
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
1954
2452
|
for (let si = 0; si < sections.length; si++) {
|
|
1955
2453
|
const section = sections[si];
|
|
1956
|
-
if (!
|
|
2454
|
+
if (!isRec5(section)) continue;
|
|
1957
2455
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
1958
2456
|
}
|
|
1959
2457
|
}
|
|
1960
2458
|
return refs;
|
|
1961
2459
|
}
|
|
1962
2460
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
1963
|
-
const add =
|
|
1964
|
-
const picker = add &&
|
|
2461
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2462
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
1965
2463
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
1966
2464
|
return {
|
|
1967
2465
|
relatedObject: strName4(props.objectName),
|
|
@@ -1982,7 +2480,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
1982
2480
|
}
|
|
1983
2481
|
function indexObjectFields(stack) {
|
|
1984
2482
|
const objectFields = /* @__PURE__ */ new Map();
|
|
1985
|
-
if (!
|
|
2483
|
+
if (!isRec5(stack)) return objectFields;
|
|
1986
2484
|
for (const obj of asArray11(stack.objects)) {
|
|
1987
2485
|
const name = strName4(obj.name);
|
|
1988
2486
|
if (!name) continue;
|
|
@@ -2028,7 +2526,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2028
2526
|
};
|
|
2029
2527
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2030
2528
|
const type = strName4(component.type);
|
|
2031
|
-
const props =
|
|
2529
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
2032
2530
|
if (!type || !props) continue;
|
|
2033
2531
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2034
2532
|
const base = `${path}.properties`;
|
|
@@ -2043,7 +2541,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2043
2541
|
if (!refs) continue;
|
|
2044
2542
|
checkRefs(refs, objectName, where);
|
|
2045
2543
|
}
|
|
2046
|
-
const cfg =
|
|
2544
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2047
2545
|
if (cfg) {
|
|
2048
2546
|
const cfgObject = strName4(cfg.source) ?? strName4(page.object);
|
|
2049
2547
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -2052,7 +2550,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2052
2550
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2053
2551
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2054
2552
|
];
|
|
2055
|
-
const userFilters =
|
|
2553
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2056
2554
|
if (userFilters) {
|
|
2057
2555
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2058
2556
|
}
|
|
@@ -2168,28 +2666,28 @@ var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
|
2168
2666
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2169
2667
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2170
2668
|
var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
|
|
2171
|
-
var
|
|
2669
|
+
var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2172
2670
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2173
2671
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
2174
2672
|
const { values, where, path } = attrs;
|
|
2175
|
-
const
|
|
2673
|
+
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2176
2674
|
if (values.has("data")) return;
|
|
2177
2675
|
const aggregate = values.get("aggregate");
|
|
2178
2676
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2179
|
-
if (!
|
|
2677
|
+
if (!isRec6(aggregate)) return;
|
|
2180
2678
|
const fn = strOf(aggregate.function);
|
|
2181
2679
|
const field = strOf(aggregate.field);
|
|
2182
2680
|
const groupBy = aggregate.groupBy;
|
|
2183
|
-
const groupByField = strOf(groupBy) ?? (
|
|
2681
|
+
const groupByField = strOf(groupBy) ?? (isRec6(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2184
2682
|
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2185
|
-
|
|
2683
|
+
push2(
|
|
2186
2684
|
"error",
|
|
2187
2685
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2188
2686
|
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2189
2687
|
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2190
2688
|
);
|
|
2191
2689
|
} else if (fn && fn !== "count" && !field) {
|
|
2192
|
-
|
|
2690
|
+
push2(
|
|
2193
2691
|
"error",
|
|
2194
2692
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2195
2693
|
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
@@ -2203,7 +2701,7 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2203
2701
|
if (!name) return;
|
|
2204
2702
|
if (name.includes(".")) return;
|
|
2205
2703
|
if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
|
|
2206
|
-
|
|
2704
|
+
push2(
|
|
2207
2705
|
"error",
|
|
2208
2706
|
REACT_CHART_FIELD_UNKNOWN,
|
|
2209
2707
|
`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.`,
|
|
@@ -2220,7 +2718,7 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2220
2718
|
if (!name) return;
|
|
2221
2719
|
if (columns.includes(name)) return;
|
|
2222
2720
|
if (keys.comparison && name === keys.comparison) return;
|
|
2223
|
-
|
|
2721
|
+
push2(
|
|
2224
2722
|
"error",
|
|
2225
2723
|
REACT_CHART_AXIS_UNKNOWN,
|
|
2226
2724
|
`"${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).`,
|
|
@@ -2228,24 +2726,24 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2228
2726
|
);
|
|
2229
2727
|
};
|
|
2230
2728
|
const xAxisRaw = values.get("xAxis");
|
|
2231
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
2729
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec6(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
2232
2730
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
2233
2731
|
axisRef(categoryAxis, categoryProp);
|
|
2234
2732
|
const yAxisRaw = values.get("yAxis");
|
|
2235
2733
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
2236
2734
|
for (const a of yAxisList) {
|
|
2237
|
-
axisRef(strOf(a) ?? (
|
|
2735
|
+
axisRef(strOf(a) ?? (isRec6(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
2238
2736
|
}
|
|
2239
2737
|
const series = values.get("series");
|
|
2240
2738
|
if (Array.isArray(series)) {
|
|
2241
2739
|
for (const s of series) {
|
|
2242
|
-
if (!
|
|
2740
|
+
if (!isRec6(s)) continue;
|
|
2243
2741
|
const dataKey = strOf(s.dataKey);
|
|
2244
2742
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
2245
2743
|
}
|
|
2246
2744
|
}
|
|
2247
2745
|
if (categoryAxis && keys.category && categoryAxis !== keys.category && categoryAxis === keys.value) {
|
|
2248
|
-
|
|
2746
|
+
push2(
|
|
2249
2747
|
"error",
|
|
2250
2748
|
REACT_CHART_AXIS_UNKNOWN,
|
|
2251
2749
|
`${categoryProp} "${categoryAxis}" is the aggregate's VALUE column, not its category column.`,
|
|
@@ -2295,7 +2793,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
2295
2793
|
if (!Array.isArray(value)) return { child, parent };
|
|
2296
2794
|
for (let i = 0; i < value.length; i++) {
|
|
2297
2795
|
const sub = value[i];
|
|
2298
|
-
if (!
|
|
2796
|
+
if (!isRec6(sub)) continue;
|
|
2299
2797
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2300
2798
|
child.push({
|
|
2301
2799
|
objectName: strOf(sub.childObject),
|
|
@@ -2320,7 +2818,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
2320
2818
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
2321
2819
|
return;
|
|
2322
2820
|
}
|
|
2323
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
2821
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data4.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
2324
2822
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
2325
2823
|
}
|
|
2326
2824
|
}
|
|
@@ -2340,20 +2838,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
2340
2838
|
}
|
|
2341
2839
|
for (const key of spec.nestedFields ?? []) {
|
|
2342
2840
|
const v = readable(key);
|
|
2343
|
-
if (
|
|
2841
|
+
if (isRec6(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2344
2842
|
}
|
|
2345
2843
|
for (const key of spec.sections ?? []) {
|
|
2346
2844
|
const v = readable(key);
|
|
2347
2845
|
if (!Array.isArray(v)) continue;
|
|
2348
2846
|
for (let i = 0; i < v.length; i++) {
|
|
2349
2847
|
const section = v[i];
|
|
2350
|
-
if (!
|
|
2848
|
+
if (!isRec6(section)) continue;
|
|
2351
2849
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2352
2850
|
}
|
|
2353
2851
|
}
|
|
2354
2852
|
for (const key of spec.keyedByField ?? []) {
|
|
2355
2853
|
const v = readable(key);
|
|
2356
|
-
if (!
|
|
2854
|
+
if (!isRec6(v)) continue;
|
|
2357
2855
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2358
2856
|
}
|
|
2359
2857
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -2379,25 +2877,41 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
2379
2877
|
out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
|
|
2380
2878
|
}
|
|
2381
2879
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
2382
|
-
if (schemaType) {
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
|
|
2392
|
-
objectName,
|
|
2393
|
-
objectFields,
|
|
2394
|
-
where
|
|
2395
|
-
)
|
|
2396
|
-
);
|
|
2397
|
-
}
|
|
2880
|
+
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
2881
|
+
out.push(
|
|
2882
|
+
...checkFieldRefs(
|
|
2883
|
+
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
2884
|
+
objectName,
|
|
2885
|
+
objectFields,
|
|
2886
|
+
where
|
|
2887
|
+
)
|
|
2888
|
+
);
|
|
2398
2889
|
}
|
|
2399
2890
|
return out;
|
|
2400
2891
|
}
|
|
2892
|
+
var REACT_BLOCK_NEEDS_RECORD_CONTEXT = "react-block-needs-record-context";
|
|
2893
|
+
var RECORD_BLOCK_GENERIC_FIX = "author the page as `type:'record'` \u2014 a record page mounts the record context these blocks render from.";
|
|
2894
|
+
function recordContextFinding(tag, schemaType, where, path) {
|
|
2895
|
+
return {
|
|
2896
|
+
severity: "error",
|
|
2897
|
+
rule: REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
2898
|
+
where,
|
|
2899
|
+
path,
|
|
2900
|
+
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).`,
|
|
2901
|
+
hint: `On a react page bind the record yourself: ${import_ui2.REACT_RECORD_BLOCK_ALTERNATIVES[schemaType] ?? RECORD_BLOCK_GENERIC_FIX}`
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
function localComponentNames(tsc, sf) {
|
|
2905
|
+
const names = /* @__PURE__ */ new Set();
|
|
2906
|
+
const walk = (node) => {
|
|
2907
|
+
if (tsc.isFunctionDeclaration(node) && node.name) names.add(node.name.text);
|
|
2908
|
+
else if (tsc.isVariableDeclaration(node) && tsc.isIdentifier(node.name)) names.add(node.name.text);
|
|
2909
|
+
else if (tsc.isImportSpecifier(node)) names.add(node.name.text);
|
|
2910
|
+
tsc.forEachChild(node, walk);
|
|
2911
|
+
};
|
|
2912
|
+
walk(sf);
|
|
2913
|
+
return names;
|
|
2914
|
+
}
|
|
2401
2915
|
function validateReactPageProps(stack) {
|
|
2402
2916
|
const findings = [];
|
|
2403
2917
|
const objectFields = indexObjectFields(stack);
|
|
@@ -2416,9 +2930,18 @@ function validateReactPageProps(stack) {
|
|
|
2416
2930
|
} catch {
|
|
2417
2931
|
continue;
|
|
2418
2932
|
}
|
|
2933
|
+
const locals = localComponentNames(tsc, sf);
|
|
2419
2934
|
const visit = (node) => {
|
|
2420
2935
|
if (tsc.isJsxOpeningElement(node) || tsc.isJsxSelfClosingElement(node)) {
|
|
2421
2936
|
const tag = node.tagName.getText(sf);
|
|
2937
|
+
const where = `page "${name}" \u203A <${tag}>`;
|
|
2938
|
+
const path = `pages[${p}].source`;
|
|
2939
|
+
const recordType = import_ui2.RECORD_CONTEXT_BLOCK_TAGS.get(tag);
|
|
2940
|
+
if (recordType && !locals.has(tag)) {
|
|
2941
|
+
findings.push(recordContextFinding(tag, recordType, where, path));
|
|
2942
|
+
tsc.forEachChild(node, visit);
|
|
2943
|
+
return;
|
|
2944
|
+
}
|
|
2422
2945
|
const block = BLOCKS.get(tag);
|
|
2423
2946
|
if (block) {
|
|
2424
2947
|
let hasSpread = false;
|
|
@@ -2438,8 +2961,14 @@ function validateReactPageProps(stack) {
|
|
|
2438
2961
|
);
|
|
2439
2962
|
}
|
|
2440
2963
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2964
|
+
if (tag === "Block") {
|
|
2965
|
+
const blockType = strOf(values.get("type"));
|
|
2966
|
+
if (blockType && (0, import_ui2.isRecordContextBlockType)(blockType)) {
|
|
2967
|
+
findings.push(recordContextFinding(tag, blockType, where, path));
|
|
2968
|
+
tsc.forEachChild(node, visit);
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2443
2972
|
if (!hasSpread) {
|
|
2444
2973
|
for (const req of block.requiredBindings) {
|
|
2445
2974
|
if (!used.has(req)) {
|
|
@@ -2528,7 +3057,7 @@ function validatePageSourceStyling(stack) {
|
|
|
2528
3057
|
}
|
|
2529
3058
|
|
|
2530
3059
|
// src/validate-record-title.ts
|
|
2531
|
-
var
|
|
3060
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
2532
3061
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
2533
3062
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
2534
3063
|
function asArray14(v) {
|
|
@@ -2556,7 +3085,7 @@ function validateRecordTitle(stack) {
|
|
|
2556
3085
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
2557
3086
|
});
|
|
2558
3087
|
}
|
|
2559
|
-
const completeness = (0,
|
|
3088
|
+
const completeness = (0, import_data5.objectTitleCompleteness)(obj);
|
|
2560
3089
|
if (completeness.status === "none") {
|
|
2561
3090
|
findings.push({
|
|
2562
3091
|
severity: "warning",
|
|
@@ -3859,7 +4388,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
3859
4388
|
}
|
|
3860
4389
|
|
|
3861
4390
|
// src/validate-filter-tokens.ts
|
|
3862
|
-
var
|
|
4391
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
3863
4392
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
3864
4393
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
3865
4394
|
function asArray23(v) {
|
|
@@ -3872,11 +4401,11 @@ function asArray23(v) {
|
|
|
3872
4401
|
function label(v, fallback) {
|
|
3873
4402
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
3874
4403
|
}
|
|
3875
|
-
var KNOWN_LIST =
|
|
4404
|
+
var KNOWN_LIST = import_data6.CONTEXT_TOKENS.join("}, {");
|
|
3876
4405
|
function walkFilterValues(node, path, where, out, seen) {
|
|
3877
4406
|
if (node === null || node === void 0) return;
|
|
3878
4407
|
if (typeof node === "string") {
|
|
3879
|
-
const cls = (0,
|
|
4408
|
+
const cls = (0, import_data6.classifyFilterToken)(node);
|
|
3880
4409
|
if (cls?.kind === "unknown") {
|
|
3881
4410
|
const suggestion = cls.suggestion;
|
|
3882
4411
|
out.push({
|
|
@@ -4145,16 +4674,86 @@ function validateObjectReferences(stack) {
|
|
|
4145
4674
|
return findings;
|
|
4146
4675
|
}
|
|
4147
4676
|
|
|
4677
|
+
// src/validate-nav-target-refs.ts
|
|
4678
|
+
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4679
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4680
|
+
function asArray25(v) {
|
|
4681
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
4682
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
4683
|
+
return [];
|
|
4684
|
+
}
|
|
4685
|
+
function strName7(v) {
|
|
4686
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4687
|
+
}
|
|
4688
|
+
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
4689
|
+
var NAV_TARGETS = [
|
|
4690
|
+
["page", "pageName", "pages", "page"],
|
|
4691
|
+
["report", "reportName", "reports", "report"],
|
|
4692
|
+
["dashboard", "dashboardName", "dashboards", "dashboard"]
|
|
4693
|
+
];
|
|
4694
|
+
function namesOf(collection) {
|
|
4695
|
+
const out = /* @__PURE__ */ new Set();
|
|
4696
|
+
for (const entry of asArray25(collection)) {
|
|
4697
|
+
const n = strName7(entry.name);
|
|
4698
|
+
if (n) out.add(n);
|
|
4699
|
+
}
|
|
4700
|
+
return out;
|
|
4701
|
+
}
|
|
4702
|
+
function validateNavTargetRefs(stack) {
|
|
4703
|
+
const findings = [];
|
|
4704
|
+
if (!isRec7(stack)) return findings;
|
|
4705
|
+
const apps = asArray25(stack.apps);
|
|
4706
|
+
if (apps.length === 0) return findings;
|
|
4707
|
+
const declared = /* @__PURE__ */ new Map();
|
|
4708
|
+
for (const [, , collection] of NAV_TARGETS) {
|
|
4709
|
+
declared.set(collection, namesOf(stack[collection]));
|
|
4710
|
+
}
|
|
4711
|
+
for (const [ai, app] of apps.entries()) {
|
|
4712
|
+
const appName = strName7(app.name) ?? `#${ai}`;
|
|
4713
|
+
const walk = (items, basePath) => {
|
|
4714
|
+
if (!Array.isArray(items)) return;
|
|
4715
|
+
for (const [ni, raw] of items.entries()) {
|
|
4716
|
+
if (!isRec7(raw)) continue;
|
|
4717
|
+
const nav = raw;
|
|
4718
|
+
const navPath = `${basePath}[${ni}]`;
|
|
4719
|
+
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
4720
|
+
if (nav.type !== type) continue;
|
|
4721
|
+
const target = strName7(nav[prop]);
|
|
4722
|
+
if (!target || isInterpolated2(target)) continue;
|
|
4723
|
+
const known = declared.get(collection);
|
|
4724
|
+
if (known.has(target)) continue;
|
|
4725
|
+
const emptyCollection = known.size === 0;
|
|
4726
|
+
findings.push({
|
|
4727
|
+
severity: "warning",
|
|
4728
|
+
rule: NAV_TARGET_UNRESOLVED,
|
|
4729
|
+
where: `app "${appName}" \xB7 nav "${strName7(nav.id) ?? strName7(nav.label) ?? `#${ni}`}"`,
|
|
4730
|
+
path: `${navPath}.${prop}`,
|
|
4731
|
+
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.`,
|
|
4732
|
+
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
4733
|
+
});
|
|
4734
|
+
}
|
|
4735
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
4736
|
+
}
|
|
4737
|
+
};
|
|
4738
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4739
|
+
for (const [ari, area] of asArray25(app.areas).entries()) {
|
|
4740
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
4741
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
return findings;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4148
4747
|
// src/validate-action-name-refs.ts
|
|
4149
4748
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
4150
|
-
function
|
|
4749
|
+
function asArray26(v) {
|
|
4151
4750
|
if (Array.isArray(v)) return v;
|
|
4152
4751
|
if (v && typeof v === "object") {
|
|
4153
4752
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4154
4753
|
}
|
|
4155
4754
|
return [];
|
|
4156
4755
|
}
|
|
4157
|
-
function
|
|
4756
|
+
function strName8(v) {
|
|
4158
4757
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4159
4758
|
}
|
|
4160
4759
|
function strList(v) {
|
|
@@ -4191,14 +4790,14 @@ function suggest4(target, known) {
|
|
|
4191
4790
|
}
|
|
4192
4791
|
function collectActionNames(stack) {
|
|
4193
4792
|
const names = /* @__PURE__ */ new Set();
|
|
4194
|
-
for (const action of
|
|
4195
|
-
const n =
|
|
4793
|
+
for (const action of asArray26(stack.actions)) {
|
|
4794
|
+
const n = strName8(action?.name);
|
|
4196
4795
|
if (n) names.add(n);
|
|
4197
4796
|
}
|
|
4198
|
-
for (const obj of
|
|
4797
|
+
for (const obj of asArray26(stack.objects)) {
|
|
4199
4798
|
if (!obj || typeof obj !== "object") continue;
|
|
4200
|
-
for (const action of
|
|
4201
|
-
const n =
|
|
4799
|
+
for (const action of asArray26(obj.actions)) {
|
|
4800
|
+
const n = strName8(action?.name);
|
|
4202
4801
|
if (n) names.add(n);
|
|
4203
4802
|
}
|
|
4204
4803
|
}
|
|
@@ -4208,7 +4807,7 @@ function validateActionNameRefs(stack) {
|
|
|
4208
4807
|
const findings = [];
|
|
4209
4808
|
if (!stack || typeof stack !== "object") return findings;
|
|
4210
4809
|
const known = collectActionNames(stack);
|
|
4211
|
-
const check = (name, where, path, surface) => {
|
|
4810
|
+
const check = (name, where, path, surface, placement = "with the location this surface needs") => {
|
|
4212
4811
|
if (known.has(name)) return;
|
|
4213
4812
|
findings.push({
|
|
4214
4813
|
severity: "error",
|
|
@@ -4216,42 +4815,72 @@ function validateActionNameRefs(stack) {
|
|
|
4216
4815
|
where,
|
|
4217
4816
|
path,
|
|
4218
4817
|
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),
|
|
4219
|
-
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`)
|
|
4818
|
+
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(", ")}.` : "")
|
|
4220
4819
|
});
|
|
4221
4820
|
};
|
|
4222
|
-
const
|
|
4821
|
+
const SELECTION_BAR_PLACEMENT = "(no `locations` entry needed \u2014 the selection bar places it by name)";
|
|
4822
|
+
const checkListContainer = (container, owner, label2, path) => {
|
|
4823
|
+
if (!container || typeof container !== "object") return;
|
|
4824
|
+
const list3 = container;
|
|
4825
|
+
for (const key of ["rowActions", "bulkActions"]) {
|
|
4826
|
+
const names = strList(list3[key]);
|
|
4827
|
+
for (let ai = 0; ai < names.length; ai++) {
|
|
4828
|
+
check(
|
|
4829
|
+
names[ai],
|
|
4830
|
+
`${owner} \xB7 ${label2} \xB7 ${key}`,
|
|
4831
|
+
`${path}.${key}[${ai}]`,
|
|
4832
|
+
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu",
|
|
4833
|
+
key === "bulkActions" ? SELECTION_BAR_PLACEMENT : void 0
|
|
4834
|
+
);
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
const defs = Array.isArray(list3.bulkActionDefs) ? list3.bulkActionDefs : [];
|
|
4838
|
+
for (let di = 0; di < defs.length; di++) {
|
|
4839
|
+
const def = defs[di];
|
|
4840
|
+
if (!def || typeof def !== "object") continue;
|
|
4841
|
+
if (def.execution !== "aggregate") continue;
|
|
4842
|
+
if (def.actionDef !== void 0) continue;
|
|
4843
|
+
const name = strName8(def.name);
|
|
4844
|
+
if (!name) continue;
|
|
4845
|
+
check(
|
|
4846
|
+
name,
|
|
4847
|
+
`${owner} \xB7 ${label2} \xB7 bulkActionDefs[${di}]`,
|
|
4848
|
+
`${path}.bulkActionDefs[${di}].name`,
|
|
4849
|
+
"Aggregate bulk action",
|
|
4850
|
+
SELECTION_BAR_PLACEMENT
|
|
4851
|
+
);
|
|
4852
|
+
}
|
|
4853
|
+
};
|
|
4854
|
+
const views = asArray26(stack.views);
|
|
4223
4855
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4224
4856
|
const view = views[vi];
|
|
4225
4857
|
if (!view || typeof view !== "object") continue;
|
|
4226
|
-
const viewName =
|
|
4227
|
-
const
|
|
4228
|
-
|
|
4229
|
-
const list3 = container;
|
|
4230
|
-
for (const key of ["rowActions", "bulkActions"]) {
|
|
4231
|
-
const names = strList(list3[key]);
|
|
4232
|
-
for (let ai = 0; ai < names.length; ai++) {
|
|
4233
|
-
check(
|
|
4234
|
-
names[ai],
|
|
4235
|
-
`view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
|
|
4236
|
-
`${path}.${key}[${ai}]`,
|
|
4237
|
-
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
4238
|
-
);
|
|
4239
|
-
}
|
|
4240
|
-
}
|
|
4241
|
-
};
|
|
4242
|
-
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
4858
|
+
const viewName = strName8(view.name) ?? strName8(view.object) ?? `#${vi}`;
|
|
4859
|
+
const owner = `view "${viewName}"`;
|
|
4860
|
+
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
4243
4861
|
const listViews = view.listViews;
|
|
4244
4862
|
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
4245
4863
|
for (const [key, lv] of Object.entries(listViews)) {
|
|
4246
|
-
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
4864
|
+
checkListContainer(lv, owner, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
4247
4865
|
}
|
|
4248
4866
|
}
|
|
4249
4867
|
}
|
|
4250
|
-
const
|
|
4868
|
+
const objects = asArray26(stack.objects);
|
|
4869
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
4870
|
+
const obj = objects[oi];
|
|
4871
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4872
|
+
const objListViews = obj.listViews;
|
|
4873
|
+
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
4874
|
+
const owner = `object "${strName8(obj.name) ?? `#${oi}`}"`;
|
|
4875
|
+
for (const [key, lv] of Object.entries(objListViews)) {
|
|
4876
|
+
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
4877
|
+
}
|
|
4878
|
+
}
|
|
4879
|
+
const pages = asArray26(stack.pages);
|
|
4251
4880
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4252
4881
|
const page = pages[pi];
|
|
4253
4882
|
if (!page || typeof page !== "object") continue;
|
|
4254
|
-
const pageName =
|
|
4883
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
4255
4884
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4256
4885
|
const props = component.properties;
|
|
4257
4886
|
if (!props || typeof props !== "object") continue;
|
|
@@ -4259,30 +4888,30 @@ function validateActionNameRefs(stack) {
|
|
|
4259
4888
|
for (let ai = 0; ai < names.length; ai++) {
|
|
4260
4889
|
check(
|
|
4261
4890
|
names[ai],
|
|
4262
|
-
`page "${pageName}" \xB7 component "${
|
|
4891
|
+
`page "${pageName}" \xB7 component "${strName8(component.type) ?? "?"}"`,
|
|
4263
4892
|
`${path}.properties.actionNames[${ai}]`,
|
|
4264
4893
|
"Quick-actions bar"
|
|
4265
4894
|
);
|
|
4266
4895
|
}
|
|
4267
4896
|
}
|
|
4268
4897
|
}
|
|
4269
|
-
const apps =
|
|
4898
|
+
const apps = asArray26(stack.apps);
|
|
4270
4899
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4271
4900
|
const app = apps[ai];
|
|
4272
4901
|
if (!app || typeof app !== "object") continue;
|
|
4273
|
-
const appName =
|
|
4902
|
+
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4274
4903
|
const walkNav = (items, basePath) => {
|
|
4275
|
-
const navItems =
|
|
4904
|
+
const navItems = asArray26(items);
|
|
4276
4905
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4277
4906
|
const nav = navItems[ni];
|
|
4278
4907
|
if (!nav || typeof nav !== "object") continue;
|
|
4279
4908
|
const navPath = `${basePath}[${ni}]`;
|
|
4280
4909
|
const actionDef = nav.actionDef;
|
|
4281
|
-
const actionName =
|
|
4910
|
+
const actionName = strName8(actionDef?.actionName);
|
|
4282
4911
|
if (nav.type === "action" && actionName) {
|
|
4283
4912
|
check(
|
|
4284
4913
|
actionName,
|
|
4285
|
-
`app "${appName}" \xB7 nav "${
|
|
4914
|
+
`app "${appName}" \xB7 nav "${strName8(nav.id) ?? `#${ni}`}"`,
|
|
4286
4915
|
`${navPath}.actionDef.actionName`,
|
|
4287
4916
|
"Navigation action item"
|
|
4288
4917
|
);
|
|
@@ -4291,7 +4920,7 @@ function validateActionNameRefs(stack) {
|
|
|
4291
4920
|
}
|
|
4292
4921
|
};
|
|
4293
4922
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4294
|
-
const areas =
|
|
4923
|
+
const areas = asArray26(app.areas);
|
|
4295
4924
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4296
4925
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4297
4926
|
}
|
|
@@ -4299,42 +4928,116 @@ function validateActionNameRefs(stack) {
|
|
|
4299
4928
|
return findings;
|
|
4300
4929
|
}
|
|
4301
4930
|
|
|
4302
|
-
// src/validate-
|
|
4303
|
-
var
|
|
4304
|
-
|
|
4305
|
-
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
4306
|
-
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
4307
|
-
function asArray26(v) {
|
|
4931
|
+
// src/validate-action-locations.ts
|
|
4932
|
+
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
4933
|
+
function asArray27(v) {
|
|
4308
4934
|
if (Array.isArray(v)) return v;
|
|
4309
4935
|
if (v && typeof v === "object") {
|
|
4310
4936
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4311
4937
|
}
|
|
4312
4938
|
return [];
|
|
4313
4939
|
}
|
|
4314
|
-
function
|
|
4940
|
+
function strName9(v) {
|
|
4315
4941
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4316
4942
|
}
|
|
4317
4943
|
function strList2(v) {
|
|
4318
4944
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
4319
4945
|
}
|
|
4320
|
-
function
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
if (n === 0) return m;
|
|
4328
|
-
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
4329
|
-
for (let i = 1; i <= m; i++) {
|
|
4330
|
-
const curr = [i, ...new Array(n).fill(0)];
|
|
4331
|
-
for (let j = 1; j <= n; j++) {
|
|
4332
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
4333
|
-
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
4946
|
+
function collectNamePlacedActions(stack) {
|
|
4947
|
+
const placed = /* @__PURE__ */ new Set();
|
|
4948
|
+
const harvest = (container) => {
|
|
4949
|
+
if (!container || typeof container !== "object") return;
|
|
4950
|
+
const list3 = container;
|
|
4951
|
+
for (const key of ["rowActions", "bulkActions"]) {
|
|
4952
|
+
for (const n of strList2(list3[key])) placed.add(n);
|
|
4334
4953
|
}
|
|
4335
|
-
|
|
4954
|
+
for (const def of asArray27(list3.bulkActionDefs)) {
|
|
4955
|
+
const n = strName9(def?.name);
|
|
4956
|
+
if (n) placed.add(n);
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
const harvestListViews = (listViews) => {
|
|
4960
|
+
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
4961
|
+
for (const lv of Object.values(listViews)) harvest(lv);
|
|
4962
|
+
};
|
|
4963
|
+
for (const view of asArray27(stack.views)) {
|
|
4964
|
+
if (!view || typeof view !== "object") continue;
|
|
4965
|
+
harvest(view.list);
|
|
4966
|
+
harvestListViews(view.listViews);
|
|
4336
4967
|
}
|
|
4337
|
-
|
|
4968
|
+
for (const obj of asArray27(stack.objects)) {
|
|
4969
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4970
|
+
harvestListViews(obj.listViews);
|
|
4971
|
+
}
|
|
4972
|
+
return placed;
|
|
4973
|
+
}
|
|
4974
|
+
function validateActionLocations(stack) {
|
|
4975
|
+
const findings = [];
|
|
4976
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
4977
|
+
const namePlaced = collectNamePlacedActions(stack);
|
|
4978
|
+
const check = (action, path) => {
|
|
4979
|
+
if (!action || typeof action !== "object") return;
|
|
4980
|
+
if ("locations" in action) return;
|
|
4981
|
+
const name = strName9(action.name);
|
|
4982
|
+
if (!name) return;
|
|
4983
|
+
if (namePlaced.has(name)) return;
|
|
4984
|
+
findings.push({
|
|
4985
|
+
severity: "warning",
|
|
4986
|
+
rule: ACTION_NO_PLACEMENT,
|
|
4987
|
+
where: `action "${name}"`,
|
|
4988
|
+
path,
|
|
4989
|
+
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.`,
|
|
4990
|
+
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."
|
|
4991
|
+
});
|
|
4992
|
+
};
|
|
4993
|
+
const actions = asArray27(stack.actions);
|
|
4994
|
+
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
4995
|
+
const objects = asArray27(stack.objects);
|
|
4996
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
4997
|
+
const obj = objects[oi];
|
|
4998
|
+
if (!obj || typeof obj !== "object") continue;
|
|
4999
|
+
const own = asArray27(obj.actions);
|
|
5000
|
+
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
5001
|
+
}
|
|
5002
|
+
return findings;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
// src/validate-chart-bindings.ts
|
|
5006
|
+
var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
5007
|
+
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
5008
|
+
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
5009
|
+
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
5010
|
+
function asArray28(v) {
|
|
5011
|
+
if (Array.isArray(v)) return v;
|
|
5012
|
+
if (v && typeof v === "object") {
|
|
5013
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5014
|
+
}
|
|
5015
|
+
return [];
|
|
5016
|
+
}
|
|
5017
|
+
function strName10(v) {
|
|
5018
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5019
|
+
}
|
|
5020
|
+
function strList3(v) {
|
|
5021
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
5022
|
+
}
|
|
5023
|
+
function isRec8(v) {
|
|
5024
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5025
|
+
}
|
|
5026
|
+
function distance4(a, b) {
|
|
5027
|
+
const m = a.length;
|
|
5028
|
+
const n = b.length;
|
|
5029
|
+
if (m === 0) return n;
|
|
5030
|
+
if (n === 0) return m;
|
|
5031
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
5032
|
+
for (let i = 1; i <= m; i++) {
|
|
5033
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
5034
|
+
for (let j = 1; j <= n; j++) {
|
|
5035
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
5036
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
5037
|
+
}
|
|
5038
|
+
prev = curr;
|
|
5039
|
+
}
|
|
5040
|
+
return prev[n];
|
|
4338
5041
|
}
|
|
4339
5042
|
function suggest5(target, known) {
|
|
4340
5043
|
let best;
|
|
@@ -4355,17 +5058,17 @@ function list2(names) {
|
|
|
4355
5058
|
}
|
|
4356
5059
|
function indexDatasets(stack) {
|
|
4357
5060
|
const out = /* @__PURE__ */ new Map();
|
|
4358
|
-
for (const ds of
|
|
4359
|
-
const name =
|
|
5061
|
+
for (const ds of asArray28(stack.datasets)) {
|
|
5062
|
+
const name = strName10(ds.name);
|
|
4360
5063
|
if (!name) continue;
|
|
4361
5064
|
const dimensions = /* @__PURE__ */ new Set();
|
|
4362
|
-
for (const d of
|
|
4363
|
-
const n =
|
|
5065
|
+
for (const d of asArray28(ds.dimensions)) {
|
|
5066
|
+
const n = strName10(d.name);
|
|
4364
5067
|
if (n) dimensions.add(n);
|
|
4365
5068
|
}
|
|
4366
5069
|
const measures = /* @__PURE__ */ new Set();
|
|
4367
|
-
for (const m of
|
|
4368
|
-
const n =
|
|
5070
|
+
for (const m of asArray28(ds.measures)) {
|
|
5071
|
+
const n = strName10(m.name);
|
|
4369
5072
|
if (n) measures.add(n);
|
|
4370
5073
|
}
|
|
4371
5074
|
out.set(name, { dimensions, measures });
|
|
@@ -4443,75 +5146,75 @@ function validateChartBindings(stack) {
|
|
|
4443
5146
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
4444
5147
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
4445
5148
|
};
|
|
4446
|
-
const reports =
|
|
5149
|
+
const reports = asArray28(stack.reports);
|
|
4447
5150
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
4448
5151
|
const report = reports[ri];
|
|
4449
|
-
if (!
|
|
4450
|
-
const reportName =
|
|
5152
|
+
if (!isRec8(report)) continue;
|
|
5153
|
+
const reportName = strName10(report.name) ?? `#${ri}`;
|
|
4451
5154
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
4452
|
-
if (!
|
|
5155
|
+
if (!isRec8(chart)) return;
|
|
4453
5156
|
check({
|
|
4454
5157
|
dataset,
|
|
4455
5158
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
4456
5159
|
// it in lets the yAxis "declared but not selected" check work without
|
|
4457
5160
|
// reporting the selection itself twice.
|
|
4458
5161
|
values: { names: values, path: `${path}.values` },
|
|
4459
|
-
xAxis:
|
|
4460
|
-
yAxis:
|
|
4461
|
-
series:
|
|
5162
|
+
xAxis: strName10(chart.xAxis) ? { name: strName10(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
5163
|
+
yAxis: strName10(chart.yAxis) ? { name: strName10(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
5164
|
+
series: asArray28(chart.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
4462
5165
|
where,
|
|
4463
5166
|
path: `${path}.chart`
|
|
4464
5167
|
});
|
|
4465
5168
|
};
|
|
4466
5169
|
checkReportChart(
|
|
4467
5170
|
report.chart,
|
|
4468
|
-
|
|
4469
|
-
|
|
5171
|
+
strName10(report.dataset),
|
|
5172
|
+
strList3(report.values),
|
|
4470
5173
|
`report "${reportName}" \xB7 chart`,
|
|
4471
5174
|
`reports[${ri}]`
|
|
4472
5175
|
);
|
|
4473
5176
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
4474
5177
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
4475
5178
|
const block = blocks[bi];
|
|
4476
|
-
if (!
|
|
5179
|
+
if (!isRec8(block)) continue;
|
|
4477
5180
|
checkReportChart(
|
|
4478
5181
|
block.chart,
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
`report "${reportName}" \xB7 block "${
|
|
5182
|
+
strName10(block.dataset),
|
|
5183
|
+
strList3(block.values),
|
|
5184
|
+
`report "${reportName}" \xB7 block "${strName10(block.name) ?? `#${bi}`}" chart`,
|
|
4482
5185
|
`reports[${ri}].blocks[${bi}]`
|
|
4483
5186
|
);
|
|
4484
5187
|
}
|
|
4485
5188
|
}
|
|
4486
5189
|
const checkListChart = (container, where, path) => {
|
|
4487
|
-
if (!
|
|
5190
|
+
if (!isRec8(container)) return;
|
|
4488
5191
|
const chart = container.chart;
|
|
4489
|
-
if (!
|
|
5192
|
+
if (!isRec8(chart)) return;
|
|
4490
5193
|
check({
|
|
4491
|
-
dataset:
|
|
4492
|
-
dimensions: { names:
|
|
4493
|
-
values: { names:
|
|
5194
|
+
dataset: strName10(chart.dataset),
|
|
5195
|
+
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5196
|
+
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
4494
5197
|
where,
|
|
4495
5198
|
path: `${path}.chart`
|
|
4496
5199
|
});
|
|
4497
5200
|
};
|
|
4498
|
-
const views =
|
|
5201
|
+
const views = asArray28(stack.views);
|
|
4499
5202
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4500
5203
|
const view = views[vi];
|
|
4501
|
-
if (!
|
|
4502
|
-
const viewName =
|
|
5204
|
+
if (!isRec8(view)) continue;
|
|
5205
|
+
const viewName = strName10(view.name) ?? strName10(view.objectName) ?? `#${vi}`;
|
|
4503
5206
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
4504
|
-
if (
|
|
5207
|
+
if (isRec8(view.listViews)) {
|
|
4505
5208
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
4506
5209
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
4507
5210
|
}
|
|
4508
5211
|
}
|
|
4509
5212
|
}
|
|
4510
|
-
const objects =
|
|
5213
|
+
const objects = asArray28(stack.objects);
|
|
4511
5214
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4512
5215
|
const obj = objects[oi];
|
|
4513
|
-
if (!
|
|
4514
|
-
const objName =
|
|
5216
|
+
if (!isRec8(obj) || !isRec8(obj.listViews)) continue;
|
|
5217
|
+
const objName = strName10(obj.name) ?? `#${oi}`;
|
|
4515
5218
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
4516
5219
|
checkListChart(
|
|
4517
5220
|
lv,
|
|
@@ -4520,22 +5223,22 @@ function validateChartBindings(stack) {
|
|
|
4520
5223
|
);
|
|
4521
5224
|
}
|
|
4522
5225
|
}
|
|
4523
|
-
const pages =
|
|
5226
|
+
const pages = asArray28(stack.pages);
|
|
4524
5227
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4525
5228
|
const page = pages[pi];
|
|
4526
|
-
if (!
|
|
4527
|
-
const pageName =
|
|
5229
|
+
if (!isRec8(page)) continue;
|
|
5230
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
4528
5231
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4529
|
-
const props =
|
|
4530
|
-
if (!props || !
|
|
4531
|
-
const axisRefs =
|
|
4532
|
-
const seriesRefs =
|
|
5232
|
+
const props = isRec8(component.properties) ? component.properties : void 0;
|
|
5233
|
+
if (!props || !strName10(props.dataset)) continue;
|
|
5234
|
+
const axisRefs = asArray28(props.yAxis).map((a, ai) => ({ name: strName10(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
5235
|
+
const seriesRefs = asArray28(props.series).map((s, si) => ({ name: strName10(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
4533
5236
|
check({
|
|
4534
|
-
dataset:
|
|
4535
|
-
dimensions: { names:
|
|
4536
|
-
values: { names:
|
|
5237
|
+
dataset: strName10(props.dataset),
|
|
5238
|
+
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5239
|
+
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
4537
5240
|
series: [...axisRefs, ...seriesRefs],
|
|
4538
|
-
where: `page "${pageName}" \xB7 ${
|
|
5241
|
+
where: `page "${pageName}" \xB7 ${strName10(component.type) ?? "chart"}`,
|
|
4539
5242
|
path: `${path}.properties`
|
|
4540
5243
|
});
|
|
4541
5244
|
}
|
|
@@ -4547,7 +5250,7 @@ function validateChartBindings(stack) {
|
|
|
4547
5250
|
var import_system3 = require("@objectstack/spec/system");
|
|
4548
5251
|
|
|
4549
5252
|
// src/build-access-matrix.ts
|
|
4550
|
-
function
|
|
5253
|
+
function asArray29(v) {
|
|
4551
5254
|
if (Array.isArray(v)) return v;
|
|
4552
5255
|
if (v && typeof v === "object") {
|
|
4553
5256
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4558,13 +5261,13 @@ function buildAccessMatrix(stack) {
|
|
|
4558
5261
|
const entries = [];
|
|
4559
5262
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
4560
5263
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
4561
|
-
for (const obj of
|
|
5264
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4562
5265
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
4563
5266
|
if (!name) continue;
|
|
4564
5267
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
4565
5268
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
4566
5269
|
}
|
|
4567
|
-
for (const ps of
|
|
5270
|
+
for (const ps of asArray29(stack.permissions)) {
|
|
4568
5271
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
4569
5272
|
if (!psName) continue;
|
|
4570
5273
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -4637,34 +5340,34 @@ function diffAccessMatrix(before, after) {
|
|
|
4637
5340
|
|
|
4638
5341
|
// src/validate-nav-access.ts
|
|
4639
5342
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
4640
|
-
function
|
|
5343
|
+
function asArray30(v) {
|
|
4641
5344
|
if (Array.isArray(v)) return v;
|
|
4642
5345
|
if (v && typeof v === "object") {
|
|
4643
5346
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4644
5347
|
}
|
|
4645
5348
|
return [];
|
|
4646
5349
|
}
|
|
4647
|
-
function
|
|
5350
|
+
function strName11(v) {
|
|
4648
5351
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4649
5352
|
}
|
|
4650
5353
|
function collectNavExposures(stack) {
|
|
4651
5354
|
const out = [];
|
|
4652
|
-
const apps =
|
|
5355
|
+
const apps = asArray30(stack.apps);
|
|
4653
5356
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4654
5357
|
const app = apps[ai];
|
|
4655
5358
|
if (!app || typeof app !== "object") continue;
|
|
4656
|
-
const appName =
|
|
5359
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
4657
5360
|
const walk = (items, basePath) => {
|
|
4658
|
-
const navItems =
|
|
5361
|
+
const navItems = asArray30(items);
|
|
4659
5362
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4660
5363
|
const nav = navItems[ni];
|
|
4661
5364
|
if (!nav || typeof nav !== "object") continue;
|
|
4662
5365
|
const navPath = `${basePath}[${ni}]`;
|
|
4663
|
-
const objectName =
|
|
5366
|
+
const objectName = strName11(nav.objectName);
|
|
4664
5367
|
if (nav.type === "object" && objectName) {
|
|
4665
5368
|
out.push({
|
|
4666
5369
|
objectName,
|
|
4667
|
-
where: `app "${appName}" \xB7 nav "${
|
|
5370
|
+
where: `app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
|
|
4668
5371
|
path: `${navPath}.objectName`
|
|
4669
5372
|
});
|
|
4670
5373
|
}
|
|
@@ -4672,7 +5375,7 @@ function collectNavExposures(stack) {
|
|
|
4672
5375
|
}
|
|
4673
5376
|
};
|
|
4674
5377
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4675
|
-
const areas =
|
|
5378
|
+
const areas = asArray30(app.areas);
|
|
4676
5379
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4677
5380
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4678
5381
|
}
|
|
@@ -4682,13 +5385,13 @@ function collectNavExposures(stack) {
|
|
|
4682
5385
|
function validateNavAccess(stack) {
|
|
4683
5386
|
const findings = [];
|
|
4684
5387
|
if (!stack || typeof stack !== "object") return findings;
|
|
4685
|
-
const permissionSets =
|
|
5388
|
+
const permissionSets = asArray30(stack.permissions);
|
|
4686
5389
|
if (permissionSets.length === 0) return findings;
|
|
4687
5390
|
const exposures = collectNavExposures(stack);
|
|
4688
5391
|
if (exposures.length === 0) return findings;
|
|
4689
5392
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4690
|
-
for (const obj of
|
|
4691
|
-
const n =
|
|
5393
|
+
for (const obj of asArray30(stack.objects)) {
|
|
5394
|
+
const n = strName11(obj.name);
|
|
4692
5395
|
if (n) ownObjects.add(n);
|
|
4693
5396
|
}
|
|
4694
5397
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -4720,15 +5423,15 @@ function validateNavAccess(stack) {
|
|
|
4720
5423
|
var import_system4 = require("@objectstack/spec/system");
|
|
4721
5424
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
4722
5425
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
4723
|
-
function
|
|
5426
|
+
function isRec9(v) {
|
|
4724
5427
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4725
5428
|
}
|
|
4726
|
-
function
|
|
5429
|
+
function asArray31(v) {
|
|
4727
5430
|
if (Array.isArray(v)) return v;
|
|
4728
|
-
if (
|
|
5431
|
+
if (isRec9(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec9(def) ? def : {} }));
|
|
4729
5432
|
return [];
|
|
4730
5433
|
}
|
|
4731
|
-
function
|
|
5434
|
+
function strName12(v) {
|
|
4732
5435
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4733
5436
|
}
|
|
4734
5437
|
function distance5(a, b) {
|
|
@@ -4786,20 +5489,20 @@ function collectViewRecord(view, factsFor) {
|
|
|
4786
5489
|
const addView = (objectName, name) => {
|
|
4787
5490
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
4788
5491
|
};
|
|
4789
|
-
const listBinding =
|
|
4790
|
-
if (
|
|
4791
|
-
addView(recordObject ?? listBinding,
|
|
5492
|
+
const listBinding = isRec9(view.list) ? bindingOf(view.list) : void 0;
|
|
5493
|
+
if (isRec9(view.list)) addView(listBinding, strName12(view.list.name));
|
|
5494
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
4792
5495
|
for (const key of ["listViews", "formViews"]) {
|
|
4793
5496
|
const container = view[key];
|
|
4794
|
-
if (!
|
|
5497
|
+
if (!isRec9(container)) continue;
|
|
4795
5498
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
4796
|
-
if (!
|
|
5499
|
+
if (!isRec9(sub)) continue;
|
|
4797
5500
|
const binding = bindingOf(sub) ?? listBinding;
|
|
4798
5501
|
addView(binding, subKey);
|
|
4799
|
-
addView(binding,
|
|
5502
|
+
addView(binding, strName12(sub.name));
|
|
4800
5503
|
if (binding) {
|
|
4801
|
-
for (const section of
|
|
4802
|
-
const sectionName =
|
|
5504
|
+
for (const section of asArray31(sub.sections)) {
|
|
5505
|
+
const sectionName = strName12(section.name);
|
|
4803
5506
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
4804
5507
|
}
|
|
4805
5508
|
}
|
|
@@ -4807,14 +5510,14 @@ function collectViewRecord(view, factsFor) {
|
|
|
4807
5510
|
}
|
|
4808
5511
|
const sectionBinding = recordObject ?? listBinding;
|
|
4809
5512
|
if (sectionBinding) {
|
|
4810
|
-
for (const section of
|
|
4811
|
-
const sectionName =
|
|
5513
|
+
for (const section of asArray31(view.sections)) {
|
|
5514
|
+
const sectionName = strName12(section.name);
|
|
4812
5515
|
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
4813
5516
|
}
|
|
4814
5517
|
}
|
|
4815
5518
|
}
|
|
4816
5519
|
function viewObjectName(view) {
|
|
4817
|
-
return
|
|
5520
|
+
return strName12(view.objectName) ?? strName12(view.object) ?? (isRec9(view.data) ? strName12(view.data.object) : void 0);
|
|
4818
5521
|
}
|
|
4819
5522
|
function readOptions(field) {
|
|
4820
5523
|
const raw = field.options;
|
|
@@ -4826,14 +5529,14 @@ function readOptions(field) {
|
|
|
4826
5529
|
values.add(opt);
|
|
4827
5530
|
continue;
|
|
4828
5531
|
}
|
|
4829
|
-
if (!
|
|
4830
|
-
const value =
|
|
5532
|
+
if (!isRec9(opt)) continue;
|
|
5533
|
+
const value = strName12(opt.value);
|
|
4831
5534
|
if (!value) continue;
|
|
4832
5535
|
values.add(value);
|
|
4833
|
-
const label2 =
|
|
5536
|
+
const label2 = strName12(opt.label);
|
|
4834
5537
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
4835
5538
|
}
|
|
4836
|
-
} else if (
|
|
5539
|
+
} else if (isRec9(raw)) {
|
|
4837
5540
|
for (const [value, label2] of Object.entries(raw)) {
|
|
4838
5541
|
values.add(value);
|
|
4839
5542
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -4853,48 +5556,48 @@ function buildUniverse(stack) {
|
|
|
4853
5556
|
}
|
|
4854
5557
|
return facts;
|
|
4855
5558
|
};
|
|
4856
|
-
for (const obj of
|
|
4857
|
-
const objectName =
|
|
5559
|
+
for (const obj of asArray31(stack.objects)) {
|
|
5560
|
+
const objectName = strName12(obj.name);
|
|
4858
5561
|
if (!objectName) continue;
|
|
4859
5562
|
const facts = factsFor(objectName);
|
|
4860
|
-
for (const field of
|
|
4861
|
-
const fieldName =
|
|
5563
|
+
for (const field of asArray31(obj.fields)) {
|
|
5564
|
+
const fieldName = strName12(field.name);
|
|
4862
5565
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
4863
5566
|
}
|
|
4864
|
-
for (const action of
|
|
4865
|
-
const actionName =
|
|
5567
|
+
for (const action of asArray31(obj.actions)) {
|
|
5568
|
+
const actionName = strName12(action.name);
|
|
4866
5569
|
if (actionName) facts.actions.set(actionName, action);
|
|
4867
5570
|
}
|
|
4868
|
-
for (const view of
|
|
4869
|
-
collectViewRecord({ ...view, object:
|
|
5571
|
+
for (const view of asArray31(obj.views)) {
|
|
5572
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
4870
5573
|
}
|
|
4871
5574
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
4872
|
-
for (const group of
|
|
4873
|
-
const key =
|
|
5575
|
+
for (const group of asArray31(obj.fieldGroups)) {
|
|
5576
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
4874
5577
|
if (key) facts.sections.add(key);
|
|
4875
5578
|
}
|
|
4876
5579
|
}
|
|
4877
|
-
for (const view of
|
|
5580
|
+
for (const view of asArray31(stack.views)) {
|
|
4878
5581
|
collectViewRecord(view, factsFor);
|
|
4879
5582
|
}
|
|
4880
|
-
const pages =
|
|
5583
|
+
const pages = asArray31(stack.pages);
|
|
4881
5584
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4882
5585
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
4883
5586
|
if (!walked.objectName) continue;
|
|
4884
|
-
const props =
|
|
5587
|
+
const props = isRec9(walked.component.properties) ? walked.component.properties : void 0;
|
|
4885
5588
|
if (!props) continue;
|
|
4886
|
-
for (const section of
|
|
4887
|
-
const sectionName =
|
|
5589
|
+
for (const section of asArray31(props.sections)) {
|
|
5590
|
+
const sectionName = strName12(section.name);
|
|
4888
5591
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
4889
5592
|
}
|
|
4890
5593
|
}
|
|
4891
5594
|
}
|
|
4892
5595
|
const globalActions = /* @__PURE__ */ new Map();
|
|
4893
5596
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
4894
|
-
for (const action of
|
|
4895
|
-
const actionName =
|
|
5597
|
+
for (const action of asArray31(stack.actions)) {
|
|
5598
|
+
const actionName = strName12(action.name);
|
|
4896
5599
|
if (!actionName) continue;
|
|
4897
|
-
const owner =
|
|
5600
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
4898
5601
|
if (owner) {
|
|
4899
5602
|
factsFor(owner).actions.set(actionName, action);
|
|
4900
5603
|
actionOwners.set(actionName, owner);
|
|
@@ -4908,41 +5611,41 @@ function buildUniverse(stack) {
|
|
|
4908
5611
|
}
|
|
4909
5612
|
}
|
|
4910
5613
|
const apps = /* @__PURE__ */ new Map();
|
|
4911
|
-
for (const app of
|
|
4912
|
-
const appName =
|
|
5614
|
+
for (const app of asArray31(stack.apps)) {
|
|
5615
|
+
const appName = strName12(app.name);
|
|
4913
5616
|
if (!appName) continue;
|
|
4914
5617
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
4915
5618
|
const walkNav = (items) => {
|
|
4916
|
-
for (const item of
|
|
4917
|
-
const id =
|
|
5619
|
+
for (const item of asArray31(items)) {
|
|
5620
|
+
const id = strName12(item.id);
|
|
4918
5621
|
if (id) navIds.add(id);
|
|
4919
5622
|
if (item.children) walkNav(item.children);
|
|
4920
5623
|
}
|
|
4921
5624
|
};
|
|
4922
5625
|
walkNav(app.navigation);
|
|
4923
|
-
for (const area of
|
|
4924
|
-
const areaId =
|
|
5626
|
+
for (const area of asArray31(app.areas)) {
|
|
5627
|
+
const areaId = strName12(area.id);
|
|
4925
5628
|
if (areaId) navIds.add(areaId);
|
|
4926
5629
|
walkNav(area.navigation);
|
|
4927
5630
|
}
|
|
4928
5631
|
apps.set(appName, navIds);
|
|
4929
5632
|
}
|
|
4930
5633
|
const dashboards = /* @__PURE__ */ new Map();
|
|
4931
|
-
for (const dash of
|
|
4932
|
-
const dashName =
|
|
5634
|
+
for (const dash of asArray31(stack.dashboards)) {
|
|
5635
|
+
const dashName = strName12(dash.name);
|
|
4933
5636
|
if (!dashName) continue;
|
|
4934
5637
|
const widgets = /* @__PURE__ */ new Set();
|
|
4935
|
-
for (const widget of
|
|
4936
|
-
const id =
|
|
5638
|
+
for (const widget of asArray31(dash.widgets)) {
|
|
5639
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
4937
5640
|
if (id) widgets.add(id);
|
|
4938
5641
|
}
|
|
4939
5642
|
const actions = /* @__PURE__ */ new Set();
|
|
4940
5643
|
const headerActions = [
|
|
4941
|
-
...
|
|
4942
|
-
...
|
|
5644
|
+
...asArray31(isRec9(dash.header) ? dash.header.actions : void 0),
|
|
5645
|
+
...asArray31(dash.actions)
|
|
4943
5646
|
];
|
|
4944
5647
|
for (const action of headerActions) {
|
|
4945
|
-
const key =
|
|
5648
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
4946
5649
|
if (key) actions.add(key);
|
|
4947
5650
|
}
|
|
4948
5651
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -4954,7 +5657,7 @@ function localePath(bundleIndex, locale) {
|
|
|
4954
5657
|
}
|
|
4955
5658
|
function validateTranslationReferences(stack) {
|
|
4956
5659
|
const findings = [];
|
|
4957
|
-
if (!
|
|
5660
|
+
if (!isRec9(stack)) return findings;
|
|
4958
5661
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
4959
5662
|
if (bundles.length === 0) return findings;
|
|
4960
5663
|
const universe = buildUniverse(stack);
|
|
@@ -4963,13 +5666,13 @@ function validateTranslationReferences(stack) {
|
|
|
4963
5666
|
};
|
|
4964
5667
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
4965
5668
|
const bundle = bundles[bi];
|
|
4966
|
-
if (!
|
|
5669
|
+
if (!isRec9(bundle)) continue;
|
|
4967
5670
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
4968
|
-
if (!
|
|
5671
|
+
if (!isRec9(rawData)) continue;
|
|
4969
5672
|
const base = localePath(bi, locale);
|
|
4970
5673
|
const inLocale = `locale "${locale}"`;
|
|
4971
5674
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
4972
|
-
if (!
|
|
5675
|
+
if (!isRec9(rawNode)) continue;
|
|
4973
5676
|
const objPath = `${base}.objects.${objectName}`;
|
|
4974
5677
|
const facts = universe.objects.get(objectName);
|
|
4975
5678
|
if (!facts) {
|
|
@@ -4995,7 +5698,7 @@ function validateTranslationReferences(stack) {
|
|
|
4995
5698
|
);
|
|
4996
5699
|
continue;
|
|
4997
5700
|
}
|
|
4998
|
-
if (!
|
|
5701
|
+
if (!isRec9(rawField)) continue;
|
|
4999
5702
|
checkOptionKeys(findings, {
|
|
5000
5703
|
optionMap: rawField.options,
|
|
5001
5704
|
field,
|
|
@@ -5077,7 +5780,7 @@ function validateTranslationReferences(stack) {
|
|
|
5077
5780
|
);
|
|
5078
5781
|
continue;
|
|
5079
5782
|
}
|
|
5080
|
-
if (!
|
|
5783
|
+
if (!isRec9(rawApp)) continue;
|
|
5081
5784
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
5082
5785
|
if (navIds.has(navId)) continue;
|
|
5083
5786
|
orphan(
|
|
@@ -5100,7 +5803,7 @@ function validateTranslationReferences(stack) {
|
|
|
5100
5803
|
);
|
|
5101
5804
|
continue;
|
|
5102
5805
|
}
|
|
5103
|
-
if (!
|
|
5806
|
+
if (!isRec9(rawDash)) continue;
|
|
5104
5807
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
5105
5808
|
if (dash.widgets.has(widgetId)) continue;
|
|
5106
5809
|
orphan(
|
|
@@ -5125,7 +5828,7 @@ function validateTranslationReferences(stack) {
|
|
|
5125
5828
|
return findings;
|
|
5126
5829
|
}
|
|
5127
5830
|
function asRecord(v) {
|
|
5128
|
-
return
|
|
5831
|
+
return isRec9(v) ? v : {};
|
|
5129
5832
|
}
|
|
5130
5833
|
function checkOptionKeys(findings, ctx) {
|
|
5131
5834
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -5137,7 +5840,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5137
5840
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
5138
5841
|
where: ctx.where,
|
|
5139
5842
|
path: ctx.path,
|
|
5140
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
5843
|
+
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.`,
|
|
5141
5844
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
5142
5845
|
});
|
|
5143
5846
|
return;
|
|
@@ -5156,11 +5859,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5156
5859
|
}
|
|
5157
5860
|
}
|
|
5158
5861
|
function checkActionParams(findings, ctx) {
|
|
5159
|
-
const rawParams = Object.keys(asRecord(
|
|
5862
|
+
const rawParams = Object.keys(asRecord(isRec9(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
5160
5863
|
if (rawParams.length === 0) return;
|
|
5161
5864
|
const declared = /* @__PURE__ */ new Set();
|
|
5162
|
-
for (const param of
|
|
5163
|
-
const name =
|
|
5865
|
+
for (const param of asArray31(ctx.action.params)) {
|
|
5866
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
5164
5867
|
if (name) declared.add(name);
|
|
5165
5868
|
}
|
|
5166
5869
|
for (const paramName of rawParams) {
|
|
@@ -5178,14 +5881,14 @@ function checkActionParams(findings, ctx) {
|
|
|
5178
5881
|
|
|
5179
5882
|
// src/validate-ai-surface-affinity.ts
|
|
5180
5883
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
5181
|
-
function
|
|
5884
|
+
function asArray32(v) {
|
|
5182
5885
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5183
5886
|
if (v && typeof v === "object") {
|
|
5184
5887
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5185
5888
|
}
|
|
5186
5889
|
return [];
|
|
5187
5890
|
}
|
|
5188
|
-
function
|
|
5891
|
+
function strName13(v) {
|
|
5189
5892
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5190
5893
|
}
|
|
5191
5894
|
function surfaceOf(v) {
|
|
@@ -5195,18 +5898,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5195
5898
|
const findings = [];
|
|
5196
5899
|
if (!stack || typeof stack !== "object") return findings;
|
|
5197
5900
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
5198
|
-
for (const skill of
|
|
5199
|
-
const n =
|
|
5901
|
+
for (const skill of asArray32(stack.skills)) {
|
|
5902
|
+
const n = strName13(skill.name);
|
|
5200
5903
|
if (n) skillsByName.set(n, skill);
|
|
5201
5904
|
}
|
|
5202
|
-
const agents =
|
|
5905
|
+
const agents = asArray32(stack.agents);
|
|
5203
5906
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5204
5907
|
const agent = agents[ai];
|
|
5205
|
-
const agentName =
|
|
5908
|
+
const agentName = strName13(agent.name) ?? `#${ai}`;
|
|
5206
5909
|
const agentSurface = surfaceOf(agent.surface);
|
|
5207
5910
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
5208
5911
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
5209
|
-
const ref =
|
|
5912
|
+
const ref = strName13(skillRefs[si]);
|
|
5210
5913
|
if (!ref) continue;
|
|
5211
5914
|
const skill = skillsByName.get(ref);
|
|
5212
5915
|
if (!skill) continue;
|
|
@@ -5228,14 +5931,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5228
5931
|
// src/validate-ai-tool-references.ts
|
|
5229
5932
|
var import_system5 = require("@objectstack/spec/system");
|
|
5230
5933
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
5231
|
-
function
|
|
5934
|
+
function asArray33(v) {
|
|
5232
5935
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5233
5936
|
if (v && typeof v === "object") {
|
|
5234
5937
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5235
5938
|
}
|
|
5236
5939
|
return [];
|
|
5237
5940
|
}
|
|
5238
|
-
function
|
|
5941
|
+
function strName14(v) {
|
|
5239
5942
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5240
5943
|
}
|
|
5241
5944
|
function distance6(a, b) {
|
|
@@ -5276,26 +5979,26 @@ function materialisesAsTool(action) {
|
|
|
5276
5979
|
if (!ai || typeof ai !== "object") return false;
|
|
5277
5980
|
const aiRec = ai;
|
|
5278
5981
|
if (aiRec.exposed !== true) return false;
|
|
5279
|
-
if (!
|
|
5280
|
-
const type =
|
|
5982
|
+
if (!strName14(aiRec.description)) return false;
|
|
5983
|
+
const type = strName14(action.type);
|
|
5281
5984
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
5282
5985
|
if (type === "script") return Boolean(action.target || action.body);
|
|
5283
5986
|
return Boolean(action.target);
|
|
5284
5987
|
}
|
|
5285
5988
|
function collectToolUniverse(stack) {
|
|
5286
5989
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
5287
|
-
for (const tool of
|
|
5288
|
-
const n =
|
|
5990
|
+
for (const tool of asArray33(stack.tools)) {
|
|
5991
|
+
const n = strName14(tool.name);
|
|
5289
5992
|
if (n) universe.add(n);
|
|
5290
5993
|
}
|
|
5291
5994
|
const addActionFamily = (actions) => {
|
|
5292
|
-
for (const action of
|
|
5293
|
-
const n =
|
|
5995
|
+
for (const action of asArray33(actions)) {
|
|
5996
|
+
const n = strName14(action.name);
|
|
5294
5997
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
5295
5998
|
}
|
|
5296
5999
|
};
|
|
5297
6000
|
addActionFamily(stack.actions);
|
|
5298
|
-
for (const obj of
|
|
6001
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5299
6002
|
addActionFamily(obj.actions);
|
|
5300
6003
|
}
|
|
5301
6004
|
return universe;
|
|
@@ -5303,13 +6006,13 @@ function collectToolUniverse(stack) {
|
|
|
5303
6006
|
function collectUnexposedActionNames(stack) {
|
|
5304
6007
|
const names = /* @__PURE__ */ new Set();
|
|
5305
6008
|
const scan = (actions) => {
|
|
5306
|
-
for (const action of
|
|
5307
|
-
const n =
|
|
6009
|
+
for (const action of asArray33(actions)) {
|
|
6010
|
+
const n = strName14(action.name);
|
|
5308
6011
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
5309
6012
|
}
|
|
5310
6013
|
};
|
|
5311
6014
|
scan(stack.actions);
|
|
5312
|
-
for (const obj of
|
|
6015
|
+
for (const obj of asArray33(stack.objects)) scan(obj.actions);
|
|
5313
6016
|
return names;
|
|
5314
6017
|
}
|
|
5315
6018
|
function validateAiToolReferences(stack) {
|
|
@@ -5327,13 +6030,13 @@ function validateAiToolReferences(stack) {
|
|
|
5327
6030
|
}
|
|
5328
6031
|
return universe.has(ref);
|
|
5329
6032
|
};
|
|
5330
|
-
const skills =
|
|
6033
|
+
const skills = asArray33(stack.skills);
|
|
5331
6034
|
for (let si = 0; si < skills.length; si++) {
|
|
5332
6035
|
const skill = skills[si];
|
|
5333
|
-
const skillName =
|
|
6036
|
+
const skillName = strName14(skill.name) ?? `#${si}`;
|
|
5334
6037
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
5335
6038
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
5336
|
-
const ref =
|
|
6039
|
+
const ref = strName14(refs[ti]);
|
|
5337
6040
|
if (!ref || resolves(ref)) continue;
|
|
5338
6041
|
const isPattern = ref.endsWith("*");
|
|
5339
6042
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -5352,24 +6055,24 @@ function validateAiToolReferences(stack) {
|
|
|
5352
6055
|
|
|
5353
6056
|
// src/validate-ai-agent-authoring.ts
|
|
5354
6057
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
5355
|
-
function
|
|
6058
|
+
function asArray34(v) {
|
|
5356
6059
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5357
6060
|
if (v && typeof v === "object") {
|
|
5358
6061
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5359
6062
|
}
|
|
5360
6063
|
return [];
|
|
5361
6064
|
}
|
|
5362
|
-
function
|
|
6065
|
+
function strName15(v) {
|
|
5363
6066
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5364
6067
|
}
|
|
5365
6068
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
5366
6069
|
function validateAiAgentAuthoring(stack) {
|
|
5367
6070
|
const findings = [];
|
|
5368
6071
|
if (!stack || typeof stack !== "object") return findings;
|
|
5369
|
-
const agents =
|
|
6072
|
+
const agents = asArray34(stack.agents);
|
|
5370
6073
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5371
6074
|
const agent = agents[ai];
|
|
5372
|
-
const name =
|
|
6075
|
+
const name = strName15(agent.name) ?? `#${ai}`;
|
|
5373
6076
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
5374
6077
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
5375
6078
|
findings.push({
|
|
@@ -5474,24 +6177,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
5474
6177
|
"owner",
|
|
5475
6178
|
"record_type"
|
|
5476
6179
|
]);
|
|
5477
|
-
var
|
|
5478
|
-
function
|
|
5479
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5480
|
-
if (
|
|
6180
|
+
var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6181
|
+
function asArray35(v) {
|
|
6182
|
+
if (Array.isArray(v)) return v.filter((x) => isRec10(x));
|
|
6183
|
+
if (isRec10(v)) {
|
|
5481
6184
|
return Object.entries(v).map(([name, def]) => ({
|
|
5482
6185
|
name,
|
|
5483
|
-
...
|
|
6186
|
+
...isRec10(def) ? def : {}
|
|
5484
6187
|
}));
|
|
5485
6188
|
}
|
|
5486
6189
|
return [];
|
|
5487
6190
|
}
|
|
5488
6191
|
function indexObjectFields2(stack) {
|
|
5489
6192
|
const out = /* @__PURE__ */ new Map();
|
|
5490
|
-
for (const obj of
|
|
6193
|
+
for (const obj of asArray35(stack.objects)) {
|
|
5491
6194
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
5492
6195
|
if (!name) continue;
|
|
5493
6196
|
const names = /* @__PURE__ */ new Set();
|
|
5494
|
-
for (const f of
|
|
6197
|
+
for (const f of asArray35(obj.fields)) {
|
|
5495
6198
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
5496
6199
|
}
|
|
5497
6200
|
out.set(name, names);
|
|
@@ -5621,12 +6324,12 @@ ${source}
|
|
|
5621
6324
|
}
|
|
5622
6325
|
function validateHookBodyWrites(stack) {
|
|
5623
6326
|
const findings = [];
|
|
5624
|
-
const hooks =
|
|
6327
|
+
const hooks = asArray35(stack.hooks);
|
|
5625
6328
|
if (hooks.length === 0) return findings;
|
|
5626
6329
|
let objectFields = null;
|
|
5627
6330
|
hooks.forEach((hook, hookIndex) => {
|
|
5628
6331
|
const body = hook.body;
|
|
5629
|
-
if (!
|
|
6332
|
+
if (!isRec10(body) || body.language !== "js") return;
|
|
5630
6333
|
const source = body.source;
|
|
5631
6334
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
5632
6335
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -5707,13 +6410,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
5707
6410
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
5708
6411
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
5709
6412
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
5710
|
-
var
|
|
5711
|
-
function
|
|
5712
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5713
|
-
if (
|
|
6413
|
+
var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6414
|
+
function asArray36(v) {
|
|
6415
|
+
if (Array.isArray(v)) return v.filter((x) => isRec11(x));
|
|
6416
|
+
if (isRec11(v)) {
|
|
5714
6417
|
return Object.entries(v).map(([name, def]) => ({
|
|
5715
6418
|
name,
|
|
5716
|
-
...
|
|
6419
|
+
...isRec11(def) ? def : {}
|
|
5717
6420
|
}));
|
|
5718
6421
|
}
|
|
5719
6422
|
return [];
|
|
@@ -5727,9 +6430,11 @@ function collectActionBodies(stack) {
|
|
|
5727
6430
|
const sites = [];
|
|
5728
6431
|
const seen = /* @__PURE__ */ new Set();
|
|
5729
6432
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
5730
|
-
|
|
6433
|
+
asArray36(actions).forEach((action, index) => {
|
|
6434
|
+
const type = typeof action.type === "string" ? action.type : "script";
|
|
6435
|
+
if (type !== "script") return;
|
|
5731
6436
|
const body = action.body;
|
|
5732
|
-
if (!
|
|
6437
|
+
if (!isRec11(body) || body.language !== "js") return;
|
|
5733
6438
|
const source = body.source;
|
|
5734
6439
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
5735
6440
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -5740,7 +6445,7 @@ function collectActionBodies(stack) {
|
|
|
5740
6445
|
});
|
|
5741
6446
|
};
|
|
5742
6447
|
collect(stack.actions, "actions");
|
|
5743
|
-
|
|
6448
|
+
asArray36(stack.objects).forEach((obj, objIndex) => {
|
|
5744
6449
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
5745
6450
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
5746
6451
|
});
|
|
@@ -5748,7 +6453,7 @@ function collectActionBodies(stack) {
|
|
|
5748
6453
|
}
|
|
5749
6454
|
function validateActionBodyWrites(stack) {
|
|
5750
6455
|
const findings = [];
|
|
5751
|
-
if (!
|
|
6456
|
+
if (!isRec11(stack)) return findings;
|
|
5752
6457
|
const sites = collectActionBodies(stack);
|
|
5753
6458
|
if (sites.length === 0) return findings;
|
|
5754
6459
|
let objectFields = null;
|
|
@@ -5807,13 +6512,13 @@ var import_shared3 = require("@objectstack/spec/shared");
|
|
|
5807
6512
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5808
6513
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
5809
6514
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
5810
|
-
var
|
|
5811
|
-
function
|
|
5812
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
5813
|
-
if (
|
|
6515
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6516
|
+
function asArray37(v) {
|
|
6517
|
+
if (Array.isArray(v)) return v.filter((x) => isRec12(x));
|
|
6518
|
+
if (isRec12(v)) {
|
|
5814
6519
|
return Object.entries(v).map(([name, def]) => ({
|
|
5815
6520
|
name,
|
|
5816
|
-
...
|
|
6521
|
+
...isRec12(def) ? def : {}
|
|
5817
6522
|
}));
|
|
5818
6523
|
}
|
|
5819
6524
|
return [];
|
|
@@ -5826,8 +6531,8 @@ function readLiteralObjectName2(config) {
|
|
|
5826
6531
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
5827
6532
|
function validateFlowNodeWrites(stack) {
|
|
5828
6533
|
const findings = [];
|
|
5829
|
-
if (!
|
|
5830
|
-
const flows =
|
|
6534
|
+
if (!isRec12(stack)) return findings;
|
|
6535
|
+
const flows = asArray37(stack.flows);
|
|
5831
6536
|
if (flows.length === 0) return findings;
|
|
5832
6537
|
let objectFields = null;
|
|
5833
6538
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -5835,10 +6540,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
5835
6540
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
5836
6541
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
5837
6542
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
5838
|
-
const config =
|
|
6543
|
+
const config = isRec12(node.config) ? node.config : void 0;
|
|
5839
6544
|
if (!config) return;
|
|
5840
6545
|
const fields = config.fields;
|
|
5841
|
-
if (!
|
|
6546
|
+
if (!isRec12(fields)) return;
|
|
5842
6547
|
const written = Object.keys(fields);
|
|
5843
6548
|
if (written.length === 0) return;
|
|
5844
6549
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -5877,6 +6582,13 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5877
6582
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
5878
6583
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
5879
6584
|
{ name: "validateNavAccess", run: validateNavAccess },
|
|
6585
|
+
// Nav targets that are NOT object names — page/report/dashboard. Restores the
|
|
6586
|
+
// coverage `defineStack`'s own cross-reference block switches off whenever the
|
|
6587
|
+
// stack declares none of that collection (`pageNames.size > 0 && …`), which is
|
|
6588
|
+
// exactly the state a stack is in when the target was never written.
|
|
6589
|
+
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
6590
|
+
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
6591
|
+
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5880
6592
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
5881
6593
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
5882
6594
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
@@ -5924,12 +6636,20 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5924
6636
|
{ name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
|
|
5925
6637
|
// The `kind:'react'` page surface. Every prop a react block binds BY FIELD
|
|
5926
6638
|
// NAME is resolved against the object it names (#4340) — `<ListView columns>`,
|
|
5927
|
-
// `<ObjectForm fields>`,
|
|
6639
|
+
// `<ObjectForm fields>`, `<Block type="element:…">` through the SAME
|
|
5928
6640
|
// `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
|
|
5929
6641
|
// over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
|
|
5930
6642
|
// `searchableFields` (#4329). Squarely the charter's question, on the surface
|
|
5931
6643
|
// where it had no answer at all.
|
|
5932
6644
|
//
|
|
6645
|
+
// It also carries `react-block-needs-record-context` (#4413) — a BINDING
|
|
6646
|
+
// question rather than a resolution one: the `record:*` family reads its
|
|
6647
|
+
// record from a record page's context, so on THIS surface the binding does
|
|
6648
|
+
// not exist at all and the props the contract published for it were read by
|
|
6649
|
+
// no renderer. This rule used to resolve those props' field names against
|
|
6650
|
+
// the object they named — lint standing guard over a binding that never ran.
|
|
6651
|
+
// It rejects the blocks now, out of the same parse.
|
|
6652
|
+
//
|
|
5933
6653
|
// It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
|
|
5934
6654
|
// accepted a react page whose every field binding was stale — including the
|
|
5935
6655
|
// gating ones (a missing required binding, a filter position naming no field:
|
|
@@ -5953,6 +6673,1488 @@ function validateReferenceIntegrity(stack) {
|
|
|
5953
6673
|
}
|
|
5954
6674
|
return findings;
|
|
5955
6675
|
}
|
|
6676
|
+
|
|
6677
|
+
// src/lint-flow-patterns.ts
|
|
6678
|
+
function asArray38(v) {
|
|
6679
|
+
if (Array.isArray(v)) return v;
|
|
6680
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6681
|
+
return [];
|
|
6682
|
+
}
|
|
6683
|
+
function conditionSource(raw) {
|
|
6684
|
+
if (typeof raw === "string") return raw;
|
|
6685
|
+
if (raw && typeof raw === "object" && typeof raw.source === "string") return raw.source;
|
|
6686
|
+
return "";
|
|
6687
|
+
}
|
|
6688
|
+
var TIME_FNS = "daysFromNow|daysAgo|today|now|date|datetime";
|
|
6689
|
+
var TIME_FN_RE = new RegExp(`\\b(?:${TIME_FNS})\\s*\\(`);
|
|
6690
|
+
var DATE_EQ = new RegExp(
|
|
6691
|
+
`(?:(?:${TIME_FNS})\\s*\\([^)]*\\)\\s*(?:==|!=))|(?:(?:==|!=)\\s*(?:${TIME_FNS})\\s*\\()`
|
|
6692
|
+
);
|
|
6693
|
+
var FLOW_TIME_RELATIVE_ANTIPATTERN = "flow-time-relative-antipattern";
|
|
6694
|
+
var FLOW_DATE_EQUALITY_FILTER = "flow-date-equality-filter";
|
|
6695
|
+
var FLOW_PHANTOM_AGGREGATION = "flow-phantom-aggregation";
|
|
6696
|
+
var FLOW_DOUBLE_BRACE_INTERP = "flow-double-brace-interpolation";
|
|
6697
|
+
var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
|
|
6698
|
+
var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
|
|
6699
|
+
var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
|
|
6700
|
+
var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
|
|
6701
|
+
var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
|
|
6702
|
+
var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
|
|
6703
|
+
var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
|
|
6704
|
+
var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
|
|
6705
|
+
var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
|
|
6706
|
+
var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
|
|
6707
|
+
var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
|
|
6708
|
+
var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6709
|
+
"decision",
|
|
6710
|
+
"assignment",
|
|
6711
|
+
"loop",
|
|
6712
|
+
"parallel",
|
|
6713
|
+
"try_catch",
|
|
6714
|
+
"create_record",
|
|
6715
|
+
"update_record",
|
|
6716
|
+
"delete_record",
|
|
6717
|
+
"get_record",
|
|
6718
|
+
"http",
|
|
6719
|
+
"notify",
|
|
6720
|
+
"script",
|
|
6721
|
+
"screen",
|
|
6722
|
+
"wait",
|
|
6723
|
+
"subflow",
|
|
6724
|
+
"map",
|
|
6725
|
+
"connector_action",
|
|
6726
|
+
"approval",
|
|
6727
|
+
"end"
|
|
6728
|
+
]);
|
|
6729
|
+
var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
|
|
6730
|
+
var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
|
|
6731
|
+
var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
|
|
6732
|
+
function isScheduleTriggered(flow, startCfg) {
|
|
6733
|
+
if (flow.type === "schedule") return true;
|
|
6734
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "schedule") return true;
|
|
6735
|
+
return startCfg.schedule != null;
|
|
6736
|
+
}
|
|
6737
|
+
function userLessTriggerKind(flow, startCfg) {
|
|
6738
|
+
if (isScheduleTriggered(flow, startCfg)) return "schedule";
|
|
6739
|
+
if (startCfg.timeRelative != null) return "time-relative";
|
|
6740
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "time_relative") return "time-relative";
|
|
6741
|
+
if (flow.type === "api") return "api";
|
|
6742
|
+
if (typeof startCfg.triggerType === "string" && startCfg.triggerType === "api") return "api";
|
|
6743
|
+
return null;
|
|
6744
|
+
}
|
|
6745
|
+
var PHANTOM_AGG_KEYS = /* @__PURE__ */ new Set(["aggregations", "aggregate", "groupBy", "rollup", "having"]);
|
|
6746
|
+
function celTimeSource(v) {
|
|
6747
|
+
if (v && typeof v === "object" && v.dialect === "cel") {
|
|
6748
|
+
const src = v.source;
|
|
6749
|
+
if (typeof src === "string" && TIME_FN_RE.test(src)) return src;
|
|
6750
|
+
}
|
|
6751
|
+
return null;
|
|
6752
|
+
}
|
|
6753
|
+
var RANGE_OPS = /* @__PURE__ */ new Set(["$gte", "$gt", "$lte", "$lt", "$ne"]);
|
|
6754
|
+
function scanFilterForDateEquality(filter, where, findings) {
|
|
6755
|
+
if (!filter || typeof filter !== "object" || Array.isArray(filter)) return;
|
|
6756
|
+
for (const [key, val] of Object.entries(filter)) {
|
|
6757
|
+
if (key === "$or" || key === "$and") {
|
|
6758
|
+
if (Array.isArray(val)) for (const sub of val) scanFilterForDateEquality(sub, where, findings);
|
|
6759
|
+
continue;
|
|
6760
|
+
}
|
|
6761
|
+
const direct = celTimeSource(val);
|
|
6762
|
+
let hit = direct ? { op: "==", src: direct } : null;
|
|
6763
|
+
if (!hit && val && typeof val === "object" && val.dialect !== "cel") {
|
|
6764
|
+
for (const [op, operand] of Object.entries(val)) {
|
|
6765
|
+
if (RANGE_OPS.has(op)) continue;
|
|
6766
|
+
if (op === "$eq") {
|
|
6767
|
+
const s = celTimeSource(operand);
|
|
6768
|
+
if (s) {
|
|
6769
|
+
hit = { op: "$eq", src: s };
|
|
6770
|
+
break;
|
|
6771
|
+
}
|
|
6772
|
+
} else if (op === "$in" && Array.isArray(operand)) {
|
|
6773
|
+
for (const item of operand) {
|
|
6774
|
+
const s = celTimeSource(item);
|
|
6775
|
+
if (s) {
|
|
6776
|
+
hit = { op: "$in", src: s };
|
|
6777
|
+
break;
|
|
6778
|
+
}
|
|
6779
|
+
}
|
|
6780
|
+
if (hit) break;
|
|
6781
|
+
}
|
|
6782
|
+
}
|
|
6783
|
+
}
|
|
6784
|
+
if (hit) {
|
|
6785
|
+
findings.push({
|
|
6786
|
+
where,
|
|
6787
|
+
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.`,
|
|
6788
|
+
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)`,
|
|
6789
|
+
rule: FLOW_DATE_EQUALITY_FILTER
|
|
6790
|
+
});
|
|
6791
|
+
}
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
var DOUBLE_BRACE = /\{\{\s*[\w$][\w$.\s]*\}\}/;
|
|
6795
|
+
var BARE_DOLLAR_REF = /(?:^|[^{])\$[A-Za-z_]\w*\.[A-Za-z_]/;
|
|
6796
|
+
var CEL_KEYS = /* @__PURE__ */ new Set(["condition", "expression", "conditions"]);
|
|
6797
|
+
function collectTemplateStrings(value, key, out) {
|
|
6798
|
+
if (key && CEL_KEYS.has(key)) return;
|
|
6799
|
+
if (typeof value === "string") {
|
|
6800
|
+
out.push(value);
|
|
6801
|
+
return;
|
|
6802
|
+
}
|
|
6803
|
+
if (Array.isArray(value)) {
|
|
6804
|
+
for (const v of value) collectTemplateStrings(v, key, out);
|
|
6805
|
+
return;
|
|
6806
|
+
}
|
|
6807
|
+
if (value && typeof value === "object") {
|
|
6808
|
+
for (const [k, v] of Object.entries(value)) collectTemplateStrings(v, k, out);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
function edgeLabelOf(e) {
|
|
6812
|
+
return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6813
|
+
}
|
|
6814
|
+
function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
|
|
6815
|
+
const typeById = /* @__PURE__ */ new Map();
|
|
6816
|
+
for (const n of nodes) {
|
|
6817
|
+
if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
|
|
6818
|
+
}
|
|
6819
|
+
for (const e of edges) {
|
|
6820
|
+
const label2 = typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6821
|
+
if (!ERROR_LABELS.has(label2)) continue;
|
|
6822
|
+
if (e.type === "fault") continue;
|
|
6823
|
+
if (e.condition) continue;
|
|
6824
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6825
|
+
if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
|
|
6826
|
+
findings.push({
|
|
6827
|
+
where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6828
|
+
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.`,
|
|
6829
|
+
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)`,
|
|
6830
|
+
rule: FLOW_ERROR_LABEL_NOT_FAULT
|
|
6831
|
+
});
|
|
6832
|
+
}
|
|
6833
|
+
}
|
|
6834
|
+
function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
6835
|
+
const outEdgesBySource = /* @__PURE__ */ new Map();
|
|
6836
|
+
for (const e of edges) {
|
|
6837
|
+
if (e.type === "fault") continue;
|
|
6838
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6839
|
+
if (!src) continue;
|
|
6840
|
+
if (!outEdgesBySource.has(src)) outEdgesBySource.set(src, []);
|
|
6841
|
+
outEdgesBySource.get(src).push(e);
|
|
6842
|
+
}
|
|
6843
|
+
for (const [src, outs] of outEdgesBySource) {
|
|
6844
|
+
for (const e of outs) {
|
|
6845
|
+
if (e.isDefault === true && e.condition) {
|
|
6846
|
+
findings.push({
|
|
6847
|
+
where: `flow '${flowName}' \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6848
|
+
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.`,
|
|
6849
|
+
hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
|
|
6850
|
+
rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
6851
|
+
// Gating: the two keys contradict, the condition always wins, and the
|
|
6852
|
+
// marker never routes. No reading makes it do what it says.
|
|
6853
|
+
severity: "error"
|
|
6854
|
+
});
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
|
|
6858
|
+
if (defaults.length > 1) {
|
|
6859
|
+
findings.push({
|
|
6860
|
+
where: `flow '${flowName}' \xB7 node '${src}'`,
|
|
6861
|
+
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".`,
|
|
6862
|
+
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)`,
|
|
6863
|
+
rule: FLOW_MULTIPLE_DEFAULT_EDGES
|
|
6864
|
+
});
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
for (const node of nodes) {
|
|
6868
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
6869
|
+
if (!INERT_CONDITION_NODE_TYPES.has(nodeType)) continue;
|
|
6870
|
+
const cfg = node.config ?? {};
|
|
6871
|
+
if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
|
|
6872
|
+
findings.push({
|
|
6873
|
+
where: `flow '${flowName}' \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
6874
|
+
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.)`,
|
|
6875
|
+
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)`,
|
|
6876
|
+
rule: FLOW_INERT_NODE_CONDITION
|
|
6877
|
+
});
|
|
6878
|
+
}
|
|
6879
|
+
for (const node of nodes) {
|
|
6880
|
+
if (node.type !== "decision") continue;
|
|
6881
|
+
const nid = typeof node.id === "string" ? node.id : "";
|
|
6882
|
+
if (!nid) continue;
|
|
6883
|
+
const outs = outEdgesBySource.get(nid) ?? [];
|
|
6884
|
+
if (outs.length === 0) continue;
|
|
6885
|
+
const cfg = node.config ?? {};
|
|
6886
|
+
const declaredLabels = new Set(
|
|
6887
|
+
(Array.isArray(cfg.conditions) ? cfg.conditions : []).map((c) => typeof c?.label === "string" ? c.label.trim().toLowerCase() : "").filter(Boolean)
|
|
6888
|
+
);
|
|
6889
|
+
const edgeLabels = new Set(outs.map(edgeLabelOf).filter(Boolean));
|
|
6890
|
+
const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
|
|
6891
|
+
if (unclaimed.length > 0) {
|
|
6892
|
+
findings.push({
|
|
6893
|
+
where: `flow '${flowName}' \xB7 decision '${nid}'`,
|
|
6894
|
+
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.`,
|
|
6895
|
+
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)`,
|
|
6896
|
+
rule: FLOW_BRANCH_LABEL_UNMATCHED,
|
|
6897
|
+
// Gating: a label nothing claims cannot route under ANY reading, on
|
|
6898
|
+
// every run. See the severity policy at the top of this file.
|
|
6899
|
+
severity: "error"
|
|
6900
|
+
});
|
|
6901
|
+
}
|
|
6902
|
+
const gated = outs.filter((e) => e.condition || e.isDefault === true);
|
|
6903
|
+
if (gated.length === 0) continue;
|
|
6904
|
+
const ungated = outs.filter(
|
|
6905
|
+
(e) => !e.condition && e.isDefault !== true && !declaredLabels.has(edgeLabelOf(e))
|
|
6906
|
+
);
|
|
6907
|
+
if (ungated.length > 0) {
|
|
6908
|
+
findings.push({
|
|
6909
|
+
where: `flow '${flowName}' \xB7 decision '${nid}'`,
|
|
6910
|
+
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\`.`,
|
|
6911
|
+
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)`,
|
|
6912
|
+
rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
|
|
6913
|
+
});
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
}
|
|
6917
|
+
function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
6918
|
+
const approvals = nodes.filter((n) => n.type === "approval");
|
|
6919
|
+
if (approvals.length === 0) return;
|
|
6920
|
+
const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
|
|
6921
|
+
const outEdges = /* @__PURE__ */ new Map();
|
|
6922
|
+
for (const e of edges) {
|
|
6923
|
+
const src = typeof e.source === "string" ? e.source : "";
|
|
6924
|
+
if (!src) continue;
|
|
6925
|
+
if (!outEdges.has(src)) outEdges.set(src, []);
|
|
6926
|
+
outEdges.get(src).push(e);
|
|
6927
|
+
}
|
|
6928
|
+
for (const a of approvals) {
|
|
6929
|
+
const aid = typeof a.id === "string" ? a.id : "";
|
|
6930
|
+
if (!aid) continue;
|
|
6931
|
+
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));
|
|
6932
|
+
if (reviseTargets.length === 0) continue;
|
|
6933
|
+
const where = `flow '${flowName}' \xB7 approval '${aid}'`;
|
|
6934
|
+
const cfg = a.config ?? {};
|
|
6935
|
+
if (cfg.maxRevisions === 0) {
|
|
6936
|
+
findings.push({
|
|
6937
|
+
where,
|
|
6938
|
+
message: `declares a 'revise' out-edge but \`maxRevisions: 0\` disables send-back \u2014 every revise auto-rejects, so the revise branch never runs.`,
|
|
6939
|
+
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).`,
|
|
6940
|
+
rule: FLOW_APPROVAL_REVISE_DISABLED
|
|
6941
|
+
});
|
|
6942
|
+
}
|
|
6943
|
+
const seen = new Set(reviseTargets);
|
|
6944
|
+
const queue = [...reviseTargets];
|
|
6945
|
+
const returnEdges = [];
|
|
6946
|
+
while (queue.length) {
|
|
6947
|
+
const cur = queue.shift();
|
|
6948
|
+
for (const e of outEdges.get(cur) ?? []) {
|
|
6949
|
+
if (e.target === aid) returnEdges.push(e);
|
|
6950
|
+
const t = typeof e.target === "string" ? e.target : "";
|
|
6951
|
+
if (t && nodeIds.has(t) && !seen.has(t)) {
|
|
6952
|
+
seen.add(t);
|
|
6953
|
+
queue.push(t);
|
|
6954
|
+
}
|
|
6955
|
+
}
|
|
6956
|
+
}
|
|
6957
|
+
if (returnEdges.length === 0) {
|
|
6958
|
+
findings.push({
|
|
6959
|
+
where,
|
|
6960
|
+
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.)`,
|
|
6961
|
+
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.`,
|
|
6962
|
+
rule: FLOW_APPROVAL_REVISE_DEAD_END
|
|
6963
|
+
});
|
|
6964
|
+
} else if (!returnEdges.some((e) => e.type === "back")) {
|
|
6965
|
+
findings.push({
|
|
6966
|
+
where,
|
|
6967
|
+
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.`,
|
|
6968
|
+
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).`,
|
|
6969
|
+
rule: FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE
|
|
6970
|
+
});
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
}
|
|
6974
|
+
function lintFlowPatterns(stack) {
|
|
6975
|
+
const findings = [];
|
|
6976
|
+
for (const flow of asArray38(stack.flows)) {
|
|
6977
|
+
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
6978
|
+
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
6979
|
+
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
6980
|
+
const start = nodes.find((n) => n.type === "start");
|
|
6981
|
+
const startCfg = start?.config ?? {};
|
|
6982
|
+
const triggerType = typeof startCfg.triggerType === "string" ? startCfg.triggerType : "";
|
|
6983
|
+
if (triggerType.startsWith("record-")) {
|
|
6984
|
+
const src = conditionSource(startCfg.condition).trim();
|
|
6985
|
+
if (src && DATE_EQ.test(src)) {
|
|
6986
|
+
findings.push({
|
|
6987
|
+
where: `flow '${flowName}' \xB7 start condition`,
|
|
6988
|
+
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.`,
|
|
6989
|
+
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)`,
|
|
6990
|
+
rule: FLOW_TIME_RELATIVE_ANTIPATTERN
|
|
6991
|
+
});
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
|
|
6995
|
+
const userLessKind = userLessTriggerKind(flow, startCfg);
|
|
6996
|
+
if (userLessKind && runAs !== "system") {
|
|
6997
|
+
const dataNode = nodes.find((n) => DATA_NODE_TYPES.has(typeof n.type === "string" ? n.type : ""));
|
|
6998
|
+
if (dataNode) {
|
|
6999
|
+
const declared = typeof flow.runAs === "string" ? `\`runAs:'${runAs}'\`` : `the default \`runAs:'user'\``;
|
|
7000
|
+
findings.push({
|
|
7001
|
+
where: `flow '${flowName}' \xB7 runAs`,
|
|
7002
|
+
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.`,
|
|
7003
|
+
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)`,
|
|
7004
|
+
rule: FLOW_RUNAS_UNSCOPED,
|
|
7005
|
+
severity: "error"
|
|
7006
|
+
});
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
for (const node of nodes) {
|
|
7010
|
+
const nodeWhere = `flow '${flowName}' \xB7 node '${node.id}' (${node.type})`;
|
|
7011
|
+
const cfg = node.config ?? {};
|
|
7012
|
+
if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
|
|
7013
|
+
for (const key of Object.keys(cfg)) {
|
|
7014
|
+
if (PHANTOM_AGG_KEYS.has(key)) {
|
|
7015
|
+
findings.push({
|
|
7016
|
+
where: nodeWhere,
|
|
7017
|
+
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.`,
|
|
7018
|
+
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)`,
|
|
7019
|
+
rule: FLOW_PHANTOM_AGGREGATION
|
|
7020
|
+
});
|
|
7021
|
+
}
|
|
7022
|
+
}
|
|
7023
|
+
const strings = [];
|
|
7024
|
+
collectTemplateStrings(node.config, void 0, strings);
|
|
7025
|
+
for (const str2 of strings) {
|
|
7026
|
+
if (DOUBLE_BRACE.test(str2)) {
|
|
7027
|
+
findings.push({
|
|
7028
|
+
where: nodeWhere,
|
|
7029
|
+
message: `double-brace interpolation \`${str2.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
|
|
7030
|
+
hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
|
|
7031
|
+
rule: FLOW_DOUBLE_BRACE_INTERP
|
|
7032
|
+
});
|
|
7033
|
+
}
|
|
7034
|
+
if (BARE_DOLLAR_REF.test(str2)) {
|
|
7035
|
+
findings.push({
|
|
7036
|
+
where: nodeWhere,
|
|
7037
|
+
message: `\`${str2.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
|
|
7038
|
+
hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
|
|
7039
|
+
rule: FLOW_BARE_DOLLAR_REF
|
|
7040
|
+
});
|
|
7041
|
+
}
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
scanApprovalReviseLoops(flowName, nodes, edges, findings);
|
|
7045
|
+
scanErrorLabelledEdges(flowName, nodes, edges, findings);
|
|
7046
|
+
scanBranchRouting(flowName, nodes, edges, findings);
|
|
7047
|
+
}
|
|
7048
|
+
return findings;
|
|
7049
|
+
}
|
|
7050
|
+
|
|
7051
|
+
// src/lint-liveness-properties.ts
|
|
7052
|
+
var import_node_module4 = require("module");
|
|
7053
|
+
var import_node_path = require("path");
|
|
7054
|
+
var import_node_fs = require("fs");
|
|
7055
|
+
var import_meta4 = {};
|
|
7056
|
+
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7057
|
+
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7058
|
+
function asArray39(v) {
|
|
7059
|
+
if (Array.isArray(v)) return v;
|
|
7060
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7061
|
+
return [];
|
|
7062
|
+
}
|
|
7063
|
+
function resolveLivenessDir() {
|
|
7064
|
+
try {
|
|
7065
|
+
const require2 = (0, import_node_module4.createRequire)(import_meta4.url);
|
|
7066
|
+
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
7067
|
+
const dir = (0, import_node_path.join)((0, import_node_path.dirname)(pkgJson), "liveness");
|
|
7068
|
+
return (0, import_node_fs.existsSync)(dir) ? dir : null;
|
|
7069
|
+
} catch {
|
|
7070
|
+
return null;
|
|
7071
|
+
}
|
|
7072
|
+
}
|
|
7073
|
+
function loadWarnMap(dir, type) {
|
|
7074
|
+
const map = /* @__PURE__ */ new Map();
|
|
7075
|
+
const file = (0, import_node_path.join)(dir, `${type}.json`);
|
|
7076
|
+
if (!(0, import_node_fs.existsSync)(file)) return map;
|
|
7077
|
+
let ledger;
|
|
7078
|
+
try {
|
|
7079
|
+
ledger = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
7080
|
+
} catch {
|
|
7081
|
+
return map;
|
|
7082
|
+
}
|
|
7083
|
+
const props = ledger.props || {};
|
|
7084
|
+
for (const [key, entry] of Object.entries(props)) {
|
|
7085
|
+
if (entry?.children) {
|
|
7086
|
+
for (const [ck, centry] of Object.entries(entry.children)) {
|
|
7087
|
+
if (shouldWarn(centry)) map.set(`${key}.${ck}`, centry);
|
|
7088
|
+
}
|
|
7089
|
+
}
|
|
7090
|
+
if (shouldWarn(entry)) map.set(key, entry);
|
|
7091
|
+
}
|
|
7092
|
+
return map;
|
|
7093
|
+
}
|
|
7094
|
+
function shouldWarn(entry) {
|
|
7095
|
+
if (!entry) return false;
|
|
7096
|
+
return entry.authorWarn === true || entry.status === "experimental";
|
|
7097
|
+
}
|
|
7098
|
+
function isAuthored(value) {
|
|
7099
|
+
if (value === void 0 || value === null) return false;
|
|
7100
|
+
if (typeof value === "boolean") return value === true;
|
|
7101
|
+
return true;
|
|
7102
|
+
}
|
|
7103
|
+
function describe(entry) {
|
|
7104
|
+
if (entry.status === "experimental") {
|
|
7105
|
+
return { kind: "is experimental \u2014 declared but NOT enforced at runtime", rule: LIVENESS_EXPERIMENTAL_PROPERTY };
|
|
7106
|
+
}
|
|
7107
|
+
return { kind: "has no runtime effect (liveness: dead)", rule: LIVENESS_DEAD_PROPERTY };
|
|
7108
|
+
}
|
|
7109
|
+
function checkItem(type, item, whereBase, warnMap, findings) {
|
|
7110
|
+
for (const [path, entry] of warnMap) {
|
|
7111
|
+
const values = path.includes(".") ? getNested(item, path) : [item[path]];
|
|
7112
|
+
for (const value of values instanceof Array ? values : [values]) {
|
|
7113
|
+
if (!isAuthored(value)) continue;
|
|
7114
|
+
const { kind, rule } = describe(entry);
|
|
7115
|
+
const hint = entry.authorHint ?? entry.note ?? "Remove it \u2014 it is declared in the spec but not consumed at runtime.";
|
|
7116
|
+
findings.push({
|
|
7117
|
+
where: whereBase,
|
|
7118
|
+
message: `sets \`${path}\` but this ${type} property ${kind}.`,
|
|
7119
|
+
hint,
|
|
7120
|
+
rule
|
|
7121
|
+
});
|
|
7122
|
+
break;
|
|
7123
|
+
}
|
|
7124
|
+
}
|
|
7125
|
+
}
|
|
7126
|
+
function getNested(obj, path) {
|
|
7127
|
+
let cur = [obj];
|
|
7128
|
+
for (const seg of path.split(".")) {
|
|
7129
|
+
const next = [];
|
|
7130
|
+
for (const c of cur) {
|
|
7131
|
+
if (c === null || typeof c !== "object") continue;
|
|
7132
|
+
const v = Array.isArray(c) ? void 0 : c[seg];
|
|
7133
|
+
if (Array.isArray(c)) {
|
|
7134
|
+
for (const el of c) {
|
|
7135
|
+
if (el && typeof el === "object") next.push(el[seg]);
|
|
7136
|
+
}
|
|
7137
|
+
} else {
|
|
7138
|
+
next.push(v);
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
cur = next;
|
|
7142
|
+
}
|
|
7143
|
+
return cur.flatMap((v) => Array.isArray(v) ? v : [v]);
|
|
7144
|
+
}
|
|
7145
|
+
var TYPE_COLLECTIONS = [
|
|
7146
|
+
{ type: "flow", key: "flows" },
|
|
7147
|
+
{ type: "action", key: "actions" },
|
|
7148
|
+
{ type: "agent", key: "agents" },
|
|
7149
|
+
{ type: "tool", key: "tools" },
|
|
7150
|
+
{ type: "skill", key: "skills" },
|
|
7151
|
+
{ type: "dataset", key: "datasets" },
|
|
7152
|
+
{ type: "permission", key: "permissions" },
|
|
7153
|
+
{ type: "hook", key: "hooks" },
|
|
7154
|
+
{ type: "page", key: "pages" },
|
|
7155
|
+
{ type: "view", key: "views" },
|
|
7156
|
+
{ type: "webhook", key: "webhooks" },
|
|
7157
|
+
// #4487. Note what adding a TYPE costs versus adding a warned property: the
|
|
7158
|
+
// doc below is right that coverage grows by marking entries `authorWarn` —
|
|
7159
|
+
// but only WITHIN a type already listed here. A newly governed type needs its
|
|
7160
|
+
// collection registered or its ledger warns nobody, which would leave the
|
|
7161
|
+
// ledger correct and silent: the exact shape this lint exists to prevent.
|
|
7162
|
+
{ type: "datasource", key: "datasources" },
|
|
7163
|
+
// #4488 — the six newly governed types that carry `authorWarn` entries.
|
|
7164
|
+
// (doc / seed / validation are governed too but warn on nothing today, so
|
|
7165
|
+
// they are not listed; add them here the day one of their entries warns.)
|
|
7166
|
+
{ type: "app", key: "apps" },
|
|
7167
|
+
{ type: "book", key: "books" },
|
|
7168
|
+
{ type: "job", key: "jobs" },
|
|
7169
|
+
{ type: "email_template", key: "emailTemplates" },
|
|
7170
|
+
{ type: "mapping", key: "mappings" },
|
|
7171
|
+
{ type: "translation", key: "translations" }
|
|
7172
|
+
];
|
|
7173
|
+
function lintLivenessProperties(stack) {
|
|
7174
|
+
const dir = resolveLivenessDir();
|
|
7175
|
+
if (!dir) return [];
|
|
7176
|
+
const findings = [];
|
|
7177
|
+
const objectWarn = loadWarnMap(dir, "object");
|
|
7178
|
+
const fieldWarn = loadWarnMap(dir, "field");
|
|
7179
|
+
for (const obj of asArray39(stack.objects)) {
|
|
7180
|
+
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7181
|
+
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
7182
|
+
if (fieldWarn.size > 0) {
|
|
7183
|
+
for (const field of asArray39(obj.fields)) {
|
|
7184
|
+
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
7185
|
+
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
7186
|
+
}
|
|
7187
|
+
}
|
|
7188
|
+
}
|
|
7189
|
+
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7190
|
+
const warnMap = loadWarnMap(dir, type);
|
|
7191
|
+
if (warnMap.size === 0) continue;
|
|
7192
|
+
for (const item of asArray39(stack[key])) {
|
|
7193
|
+
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7194
|
+
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
return findings;
|
|
7198
|
+
}
|
|
7199
|
+
|
|
7200
|
+
// src/lint-autonumber-formats.ts
|
|
7201
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
7202
|
+
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
7203
|
+
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7204
|
+
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7205
|
+
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7206
|
+
function asArray40(v) {
|
|
7207
|
+
if (Array.isArray(v)) return v;
|
|
7208
|
+
if (v && typeof v === "object") {
|
|
7209
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7210
|
+
}
|
|
7211
|
+
return [];
|
|
7212
|
+
}
|
|
7213
|
+
function lintAutonumberFormats(stack) {
|
|
7214
|
+
const findings = [];
|
|
7215
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7216
|
+
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7217
|
+
const fields = asArray40(obj.fields);
|
|
7218
|
+
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7219
|
+
for (const f of fields) {
|
|
7220
|
+
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
7221
|
+
}
|
|
7222
|
+
for (const f of fields) {
|
|
7223
|
+
if (f.type !== "autonumber") continue;
|
|
7224
|
+
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
7225
|
+
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
7226
|
+
if (!fmt) continue;
|
|
7227
|
+
const tokens = (0, import_data7.parseAutonumberFormat)(fmt);
|
|
7228
|
+
const refs = (0, import_data7.referencedFields)(tokens);
|
|
7229
|
+
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
7230
|
+
for (const t of tokens) {
|
|
7231
|
+
if (t.kind !== "literal") continue;
|
|
7232
|
+
const braced = t.text.match(/\{[^{}]*\}/g);
|
|
7233
|
+
if (!braced) continue;
|
|
7234
|
+
for (const tok of braced) {
|
|
7235
|
+
const body = tok.slice(1, -1);
|
|
7236
|
+
const isExtraSeq = /^0+$/.test(body);
|
|
7237
|
+
findings.push({
|
|
7238
|
+
where,
|
|
7239
|
+
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.`,
|
|
7240
|
+
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}.`,
|
|
7241
|
+
rule: AUTONUMBER_LITERAL_TOKEN,
|
|
7242
|
+
severity: "warning"
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
for (const ref of refs) {
|
|
7247
|
+
if (ref === name) {
|
|
7248
|
+
findings.push({
|
|
7249
|
+
where,
|
|
7250
|
+
message: `format interpolates \`{${ref}}\` \u2014 its own value, which does not exist yet when the number is generated.`,
|
|
7251
|
+
hint: `Reference a DIFFERENT field that is set before create (e.g. \`{plan_no}{000}\`), or drop the token.`,
|
|
7252
|
+
rule: AUTONUMBER_SELF_REFERENCE,
|
|
7253
|
+
severity: "error"
|
|
7254
|
+
});
|
|
7255
|
+
continue;
|
|
7256
|
+
}
|
|
7257
|
+
const meta = fieldMeta.get(ref);
|
|
7258
|
+
if (!meta) {
|
|
7259
|
+
findings.push({
|
|
7260
|
+
where,
|
|
7261
|
+
message: `format interpolates \`{${ref}}\`, but object '${objectName}' has no field named '${ref}' \u2014 generation will always throw.`,
|
|
7262
|
+
hint: `Reference an existing field, or remove the \`{${ref}}\` token from the format.`,
|
|
7263
|
+
rule: AUTONUMBER_UNKNOWN_FIELD,
|
|
7264
|
+
severity: "error"
|
|
7265
|
+
});
|
|
7266
|
+
} else if (!meta.required) {
|
|
7267
|
+
findings.push({
|
|
7268
|
+
where,
|
|
7269
|
+
message: `format interpolates \`{${ref}}\`, but '${ref}' is optional \u2014 any record left blank fails autonumber generation at create time.`,
|
|
7270
|
+
hint: `Mark '${ref}' as \`required: true\` so it is always set before the record number is rendered.`,
|
|
7271
|
+
rule: AUTONUMBER_OPTIONAL_FIELD,
|
|
7272
|
+
severity: "warning"
|
|
7273
|
+
});
|
|
7274
|
+
}
|
|
7275
|
+
}
|
|
7276
|
+
}
|
|
7277
|
+
}
|
|
7278
|
+
return findings;
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
// src/lint-view-refs.ts
|
|
7282
|
+
var import_spec2 = require("@objectstack/spec");
|
|
7283
|
+
function asArray41(v) {
|
|
7284
|
+
if (Array.isArray(v)) return v;
|
|
7285
|
+
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7286
|
+
return [];
|
|
7287
|
+
}
|
|
7288
|
+
var VIEW_KEY_COLLISION = "view-key-collision";
|
|
7289
|
+
var VIEW_REF_FORM_TARGET_MISSING = "view-ref-form-target-missing";
|
|
7290
|
+
var VIEW_REF_FORM_TARGET_KIND = "view-ref-form-target-kind";
|
|
7291
|
+
function containerFromObject(obj) {
|
|
7292
|
+
return { list: obj.list, form: obj.form, listViews: obj.listViews, formViews: obj.formViews };
|
|
7293
|
+
}
|
|
7294
|
+
function viewContainerObjectName(item) {
|
|
7295
|
+
if (typeof item.name === "string" && item.name) return item.name;
|
|
7296
|
+
if (typeof item.id === "string" && item.id) return item.id;
|
|
7297
|
+
if (typeof item.object === "string" && item.object) return item.object;
|
|
7298
|
+
if (typeof item.list?.data?.object === "string") return item.list.data.object;
|
|
7299
|
+
if (typeof item.form?.data?.object === "string") return item.form.data.object;
|
|
7300
|
+
return void 0;
|
|
7301
|
+
}
|
|
7302
|
+
function lintViewRefs(stack) {
|
|
7303
|
+
const findings = [];
|
|
7304
|
+
const viewKinds = /* @__PURE__ */ new Map();
|
|
7305
|
+
const indexKind = (name, kind) => {
|
|
7306
|
+
let s = viewKinds.get(name);
|
|
7307
|
+
if (!s) viewKinds.set(name, s = /* @__PURE__ */ new Set());
|
|
7308
|
+
s.add(kind);
|
|
7309
|
+
};
|
|
7310
|
+
const containers = [];
|
|
7311
|
+
for (const v of asArray41(stack.views)) {
|
|
7312
|
+
if (v.viewKind) {
|
|
7313
|
+
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7314
|
+
continue;
|
|
7315
|
+
}
|
|
7316
|
+
if (!(0, import_spec2.isAggregatedViewContainer)(v)) continue;
|
|
7317
|
+
const object = viewContainerObjectName(v);
|
|
7318
|
+
if (object) containers.push({ object, container: v });
|
|
7319
|
+
}
|
|
7320
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7321
|
+
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7322
|
+
if (!object) continue;
|
|
7323
|
+
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
7324
|
+
containers.push({ object, container: containerFromObject(obj) });
|
|
7325
|
+
}
|
|
7326
|
+
}
|
|
7327
|
+
for (const { object, container } of containers) {
|
|
7328
|
+
const { items, collisions } = (0, import_spec2.expandViewContainerWithDiagnostics)(object, container);
|
|
7329
|
+
for (const it of items) indexKind(it.name, it.viewKind);
|
|
7330
|
+
for (const col of collisions) {
|
|
7331
|
+
findings.push({
|
|
7332
|
+
where: `object '${object}' \xB7 view key '${col.key}'`,
|
|
7333
|
+
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.`,
|
|
7334
|
+
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.`,
|
|
7335
|
+
rule: VIEW_KEY_COLLISION,
|
|
7336
|
+
severity: "warning"
|
|
7337
|
+
});
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7340
|
+
const seenFormTargets = /* @__PURE__ */ new Set();
|
|
7341
|
+
const checkAction = (action, ownerObject) => {
|
|
7342
|
+
if (!action || action.type !== "form") return;
|
|
7343
|
+
const target = action.target;
|
|
7344
|
+
if (typeof target !== "string" || !target) return;
|
|
7345
|
+
if (target.includes("${")) return;
|
|
7346
|
+
if (!target.includes(".")) return;
|
|
7347
|
+
const actionName = typeof action.name === "string" ? action.name : "(unnamed)";
|
|
7348
|
+
const dedupeKey = `${actionName}\0${target}`;
|
|
7349
|
+
if (seenFormTargets.has(dedupeKey)) return;
|
|
7350
|
+
seenFormTargets.add(dedupeKey);
|
|
7351
|
+
const where = ownerObject ? `action '${actionName}' on object '${ownerObject}'` : `action '${actionName}'`;
|
|
7352
|
+
const kinds = viewKinds.get(target);
|
|
7353
|
+
if (!kinds) {
|
|
7354
|
+
findings.push({
|
|
7355
|
+
where,
|
|
7356
|
+
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.`,
|
|
7357
|
+
hint: `Check for a typo, or a form view renamed by a key collision. Expected a form view named '<object>.<formViewKey>'.`,
|
|
7358
|
+
rule: VIEW_REF_FORM_TARGET_MISSING,
|
|
7359
|
+
severity: "warning"
|
|
7360
|
+
});
|
|
7361
|
+
return;
|
|
7362
|
+
}
|
|
7363
|
+
if (!kinds.has("form")) {
|
|
7364
|
+
const actual = [...kinds].join("/");
|
|
7365
|
+
findings.push({
|
|
7366
|
+
where,
|
|
7367
|
+
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.`,
|
|
7368
|
+
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.`,
|
|
7369
|
+
rule: VIEW_REF_FORM_TARGET_KIND,
|
|
7370
|
+
severity: "error"
|
|
7371
|
+
});
|
|
7372
|
+
}
|
|
7373
|
+
};
|
|
7374
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7375
|
+
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7376
|
+
for (const action of asArray41(obj.actions)) checkAction(action, object);
|
|
7377
|
+
}
|
|
7378
|
+
for (const action of asArray41(stack.actions)) checkAction(action);
|
|
7379
|
+
return findings;
|
|
7380
|
+
}
|
|
7381
|
+
|
|
7382
|
+
// src/data-model-rules.ts
|
|
7383
|
+
var RELATIONSHIP_TYPES = /* @__PURE__ */ new Set(["lookup", "master_detail"]);
|
|
7384
|
+
var NUMERIC_TYPES = /* @__PURE__ */ new Set([
|
|
7385
|
+
"number",
|
|
7386
|
+
"currency",
|
|
7387
|
+
"integer",
|
|
7388
|
+
"decimal",
|
|
7389
|
+
"percent",
|
|
7390
|
+
"float",
|
|
7391
|
+
"double"
|
|
7392
|
+
]);
|
|
7393
|
+
var OPTION_FIELD_TYPES = /* @__PURE__ */ new Set(["select", "multiselect", "radio", "enum"]);
|
|
7394
|
+
var NAME_LIKE_FIELDS = ["name", "title", "subject", "label", "full_name", "display_name", "code"];
|
|
7395
|
+
var LINE_ITEM_RE = /_(line|lines|line_item|line_items|item|items|detail|details|entry|entries)$/;
|
|
7396
|
+
var ASSOCIATION_TOKENS = [
|
|
7397
|
+
"comment",
|
|
7398
|
+
"attachment",
|
|
7399
|
+
"note",
|
|
7400
|
+
"log",
|
|
7401
|
+
"audit",
|
|
7402
|
+
"activity",
|
|
7403
|
+
"activities",
|
|
7404
|
+
"history",
|
|
7405
|
+
"event",
|
|
7406
|
+
"reaction",
|
|
7407
|
+
"like",
|
|
7408
|
+
"mention",
|
|
7409
|
+
"notification",
|
|
7410
|
+
"message"
|
|
7411
|
+
];
|
|
7412
|
+
function isLineItemName(name) {
|
|
7413
|
+
return LINE_ITEM_RE.test(name);
|
|
7414
|
+
}
|
|
7415
|
+
function isAssociationName(name) {
|
|
7416
|
+
const lc = name.toLowerCase();
|
|
7417
|
+
return ASSOCIATION_TOKENS.some((t) => lc === t || lc.endsWith(`_${t}`) || lc.endsWith(`_${t}s`));
|
|
7418
|
+
}
|
|
7419
|
+
function fieldEntries2(fields) {
|
|
7420
|
+
if (!fields) return [];
|
|
7421
|
+
if (Array.isArray(fields)) {
|
|
7422
|
+
return fields.filter((f) => f && f.name != null).map((f) => ({ name: String(f.name), def: f }));
|
|
7423
|
+
}
|
|
7424
|
+
return Object.entries(fields).map(([name, def]) => ({ name, def }));
|
|
7425
|
+
}
|
|
7426
|
+
function refOf2(def) {
|
|
7427
|
+
return def?.reference || def?.reference_to;
|
|
7428
|
+
}
|
|
7429
|
+
var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
|
|
7430
|
+
function uniqueDeclared(u) {
|
|
7431
|
+
return u === true || u === "global";
|
|
7432
|
+
}
|
|
7433
|
+
function lintUniqueDeclarations(objects) {
|
|
7434
|
+
const issues = [];
|
|
7435
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7436
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7437
|
+
const obj = objects[i];
|
|
7438
|
+
if (!obj?.name) continue;
|
|
7439
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
7440
|
+
if (declaredIndexes.length === 0) continue;
|
|
7441
|
+
const singleColumnUniqueIndexes = /* @__PURE__ */ new Map();
|
|
7442
|
+
for (const idx of declaredIndexes) {
|
|
7443
|
+
if (!uniqueDeclared(idx?.unique)) continue;
|
|
7444
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
7445
|
+
if (cols.length !== 1) continue;
|
|
7446
|
+
if (!singleColumnUniqueIndexes.has(cols[0])) singleColumnUniqueIndexes.set(cols[0], idx);
|
|
7447
|
+
}
|
|
7448
|
+
if (singleColumnUniqueIndexes.size === 0) continue;
|
|
7449
|
+
for (const { name, def } of fieldEntries2(obj.fields)) {
|
|
7450
|
+
if (!uniqueDeclared(def?.unique)) continue;
|
|
7451
|
+
if (def.unique === "global") continue;
|
|
7452
|
+
const idx = singleColumnUniqueIndexes.get(name);
|
|
7453
|
+
if (!idx) continue;
|
|
7454
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
7455
|
+
issues.push({
|
|
7456
|
+
severity: "warning",
|
|
7457
|
+
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
7458
|
+
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.`,
|
|
7459
|
+
path: `objects[${i}]`,
|
|
7460
|
+
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.`
|
|
7461
|
+
});
|
|
7462
|
+
}
|
|
7463
|
+
}
|
|
7464
|
+
return issues;
|
|
7465
|
+
}
|
|
7466
|
+
function lintDataModel(objects) {
|
|
7467
|
+
const issues = lintUniqueDeclarations(objects);
|
|
7468
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7469
|
+
const childrenByParent = {};
|
|
7470
|
+
for (const child of objects) {
|
|
7471
|
+
if (!child?.name) continue;
|
|
7472
|
+
for (const { name: fieldName, def } of fieldEntries2(child.fields)) {
|
|
7473
|
+
if (!RELATIONSHIP_TYPES.has(def?.type)) continue;
|
|
7474
|
+
const parent = refOf2(def);
|
|
7475
|
+
if (!parent) continue;
|
|
7476
|
+
(childrenByParent[parent] || (childrenByParent[parent] = [])).push({ child, fieldName, def });
|
|
7477
|
+
}
|
|
7478
|
+
}
|
|
7479
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7480
|
+
const obj = objects[i];
|
|
7481
|
+
if (!obj?.name) continue;
|
|
7482
|
+
const objPath = `objects[${i}]`;
|
|
7483
|
+
const fields = fieldEntries2(obj.fields);
|
|
7484
|
+
const hasNameField = !!obj.primaryField || !!obj.titleFormat || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
|
|
7485
|
+
if (fields.length > 0 && !hasNameField) {
|
|
7486
|
+
issues.push({
|
|
7487
|
+
severity: "suggestion",
|
|
7488
|
+
rule: "object/missing-name-field",
|
|
7489
|
+
message: `Object "${obj.name}" has no name/title field or primaryField \u2014 records will display as raw IDs`,
|
|
7490
|
+
path: `${objPath}.fields`
|
|
7491
|
+
});
|
|
7492
|
+
}
|
|
7493
|
+
for (const { name: fieldName, def } of fields) {
|
|
7494
|
+
if (!def || typeof def !== "object") continue;
|
|
7495
|
+
const fieldPath = `${objPath}.fields.${fieldName}`;
|
|
7496
|
+
const type = def.type;
|
|
7497
|
+
if (OPTION_FIELD_TYPES.has(type)) {
|
|
7498
|
+
const hasOptions = Array.isArray(def.options) && def.options.length > 0 || !!def.optionsFrom || !!def.dataSource || !!def.reference;
|
|
7499
|
+
if (!hasOptions) {
|
|
7500
|
+
issues.push({
|
|
7501
|
+
severity: "warning",
|
|
7502
|
+
rule: "field/select-missing-options",
|
|
7503
|
+
message: `${type} field "${obj.name}.${fieldName}" has no options`,
|
|
7504
|
+
path: `${fieldPath}.options`
|
|
7505
|
+
});
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
if (!RELATIONSHIP_TYPES.has(type)) continue;
|
|
7509
|
+
const parent = refOf2(def);
|
|
7510
|
+
if (!parent) {
|
|
7511
|
+
issues.push({
|
|
7512
|
+
severity: "error",
|
|
7513
|
+
rule: "relationship/missing-reference",
|
|
7514
|
+
message: `${type} field "${obj.name}.${fieldName}" is missing a reference target`,
|
|
7515
|
+
path: `${fieldPath}.reference`
|
|
7516
|
+
});
|
|
7517
|
+
continue;
|
|
7518
|
+
}
|
|
7519
|
+
if (type === "master_detail") {
|
|
7520
|
+
if (def.required !== true) {
|
|
7521
|
+
issues.push({
|
|
7522
|
+
severity: "warning",
|
|
7523
|
+
rule: "relationship/master-detail-required",
|
|
7524
|
+
message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should be required (a detail record cannot exist without its master)`,
|
|
7525
|
+
path: `${fieldPath}.required`,
|
|
7526
|
+
fix: "required: true"
|
|
7527
|
+
});
|
|
7528
|
+
}
|
|
7529
|
+
if (def.deleteBehavior === void 0) {
|
|
7530
|
+
issues.push({
|
|
7531
|
+
severity: "suggestion",
|
|
7532
|
+
rule: "relationship/delete-behavior",
|
|
7533
|
+
message: `master_detail "${obj.name}.${fieldName}" \u2192 ${parent} should declare deleteBehavior (cascade/restrict/set_null)`,
|
|
7534
|
+
path: `${fieldPath}.deleteBehavior`,
|
|
7535
|
+
fix: "deleteBehavior: 'cascade'"
|
|
7536
|
+
});
|
|
7537
|
+
}
|
|
7538
|
+
if (isLineItemName(obj.name) && def.inlineEdit !== true) {
|
|
7539
|
+
issues.push({
|
|
7540
|
+
severity: "suggestion",
|
|
7541
|
+
rule: "relationship/line-items-inline-edit",
|
|
7542
|
+
message: `"${obj.name}" looks like line items of ${parent}; consider inlineEdit: true on "${fieldName}" so it is entered inline within the ${parent} form`,
|
|
7543
|
+
path: `${fieldPath}.inlineEdit`,
|
|
7544
|
+
fix: "inlineEdit: true"
|
|
7545
|
+
});
|
|
7546
|
+
}
|
|
7547
|
+
}
|
|
7548
|
+
if (type === "lookup" && isLineItemName(obj.name)) {
|
|
7549
|
+
issues.push({
|
|
7550
|
+
severity: "suggestion",
|
|
7551
|
+
rule: "relationship/line-item-should-be-master-detail",
|
|
7552
|
+
message: `"${obj.name}" looks like line items of ${parent} but uses lookup; master_detail gives ownership + cascade + roll-ups`,
|
|
7553
|
+
path: `${fieldPath}.type`,
|
|
7554
|
+
fix: "type: 'master_detail'"
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
if (def.inlineEdit === true && isAssociationName(obj.name)) {
|
|
7558
|
+
issues.push({
|
|
7559
|
+
severity: "warning",
|
|
7560
|
+
rule: "relationship/association-inline-edit",
|
|
7561
|
+
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`,
|
|
7562
|
+
path: `${fieldPath}.inlineEdit`,
|
|
7563
|
+
fix: "remove inlineEdit (use relatedList on the detail page)"
|
|
7564
|
+
});
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
const children = childrenByParent[obj.name] || [];
|
|
7568
|
+
const summaryChildObjects = new Set(
|
|
7569
|
+
fields.filter((f) => f.def?.type === "summary").map((f) => f.def?.summaryOperations?.object || f.def?.reference).filter(Boolean)
|
|
7570
|
+
);
|
|
7571
|
+
const seenSuggestedChild = /* @__PURE__ */ new Set();
|
|
7572
|
+
for (const { child, def } of children) {
|
|
7573
|
+
if (def?.type !== "master_detail") continue;
|
|
7574
|
+
if (!child?.name || seenSuggestedChild.has(child.name)) continue;
|
|
7575
|
+
if (summaryChildObjects.has(child.name)) continue;
|
|
7576
|
+
const numericChildField = fieldEntries2(child.fields).find((f) => NUMERIC_TYPES.has(f.def?.type));
|
|
7577
|
+
if (!numericChildField) continue;
|
|
7578
|
+
seenSuggestedChild.add(child.name);
|
|
7579
|
+
issues.push({
|
|
7580
|
+
severity: "suggestion",
|
|
7581
|
+
rule: "rollup/missing-summary",
|
|
7582
|
+
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}`,
|
|
7583
|
+
path: `${objPath}.fields`,
|
|
7584
|
+
fix: `summary field aggregating ${child.name}.${numericChildField.name}`
|
|
7585
|
+
});
|
|
7586
|
+
}
|
|
7587
|
+
}
|
|
7588
|
+
return issues;
|
|
7589
|
+
}
|
|
7590
|
+
|
|
7591
|
+
// src/authoring-rules.ts
|
|
7592
|
+
var AUTHORING_COMMANDS = ["validate", "build", "lint"];
|
|
7593
|
+
var AUTHORING_SURFACES = ["cli", "runtime-publish"];
|
|
7594
|
+
var ALL = AUTHORING_COMMANDS;
|
|
7595
|
+
var CLI_ONLY = ["cli"];
|
|
7596
|
+
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
7597
|
+
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.";
|
|
7598
|
+
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.";
|
|
7599
|
+
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.";
|
|
7600
|
+
var EXPRESSION_INVALID = "expression-invalid";
|
|
7601
|
+
var AUTHORING_RULES = [
|
|
7602
|
+
// ADR-0032 §1a/1b — CEL predicates in actions/validations/flows/sharing/hooks
|
|
7603
|
+
// are parsed for syntax AND checked that each `record.<field>` resolves. This
|
|
7604
|
+
// is what catches a BARE field ref (`done` instead of `record.done`) that
|
|
7605
|
+
// would otherwise silently hide an action on every record (#2183/#2185).
|
|
7606
|
+
{
|
|
7607
|
+
name: "validateStackExpressions",
|
|
7608
|
+
tier: "gating",
|
|
7609
|
+
input: "parsed",
|
|
7610
|
+
commands: ALL,
|
|
7611
|
+
source: "packages/lint/src/validate-expressions.ts",
|
|
7612
|
+
// Runtime publish gate (#4463): the EXPRESSION family. On a flow write it
|
|
7613
|
+
// checks every script-node callable and every declared predicate the flow
|
|
7614
|
+
// carries, against the live object universe — the same parse `os build`
|
|
7615
|
+
// runs, now at the door Studio/REST/MCP authors actually use.
|
|
7616
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7617
|
+
runtimeTypes: ["flow"],
|
|
7618
|
+
run: (stack) => validateStackExpressions(stack).map((i) => ({
|
|
7619
|
+
severity: i.severity ?? "error",
|
|
7620
|
+
rule: EXPRESSION_INVALID,
|
|
7621
|
+
where: i.where,
|
|
7622
|
+
path: i.where,
|
|
7623
|
+
message: i.message,
|
|
7624
|
+
hint: `source: \`${i.source}\``
|
|
7625
|
+
}))
|
|
7626
|
+
},
|
|
7627
|
+
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
7628
|
+
// mode) are silently dropped: `ObjectListViewSchema` omits them, so this must
|
|
7629
|
+
// read the pre-parse tier or the evidence is already gone.
|
|
7630
|
+
{
|
|
7631
|
+
name: "validateListViewMode",
|
|
7632
|
+
tier: "gating",
|
|
7633
|
+
input: "normalized",
|
|
7634
|
+
commands: ALL,
|
|
7635
|
+
source: "packages/lint/src/validate-list-view-mode.ts",
|
|
7636
|
+
surfaces: CLI_ONLY,
|
|
7637
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7638
|
+
run: (stack) => validateListViewMode(stack)
|
|
7639
|
+
},
|
|
7640
|
+
// [ADR-0078] A Zod-VALID instance that silently does nothing: a `summary`
|
|
7641
|
+
// with no `summaryOperations`, a `lookup` with no `reference`, a `select`
|
|
7642
|
+
// with no `options`. Every key is one we know, so #4001's unknown-key
|
|
7643
|
+
// rejection cannot see it, and the liveness ledger cannot either (it is
|
|
7644
|
+
// per-property; the properties ARE live). This is the gate between them.
|
|
7645
|
+
//
|
|
7646
|
+
// `gating` because the error-severity shapes are fully inert — the field
|
|
7647
|
+
// reads 0 forever while authoring reports success, which is the failure the
|
|
7648
|
+
// ADR was written for (cloud#687). Pre-parse so the findings survive an
|
|
7649
|
+
// unrelated schema error elsewhere in the stack.
|
|
7650
|
+
{
|
|
7651
|
+
name: "validateFunctionalCompleteness",
|
|
7652
|
+
tier: "gating",
|
|
7653
|
+
input: "normalized",
|
|
7654
|
+
commands: ALL,
|
|
7655
|
+
source: "packages/lint/src/validate-functional-completeness.ts",
|
|
7656
|
+
surfaces: CLI_ONLY,
|
|
7657
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7658
|
+
run: (stack) => validateFunctionalCompleteness(stack)
|
|
7659
|
+
},
|
|
7660
|
+
// A flat list-view object in `views: []` parses to an EMPTY container
|
|
7661
|
+
// (ViewSchema strips unknown keys): the schema step passes, zero views
|
|
7662
|
+
// register, and the Console renders nothing. Pre-parse for the same reason.
|
|
7663
|
+
{
|
|
7664
|
+
name: "validateViewContainers",
|
|
7665
|
+
tier: "gating",
|
|
7666
|
+
input: "normalized",
|
|
7667
|
+
commands: ALL,
|
|
7668
|
+
source: "packages/lint/src/validate-view-containers.ts",
|
|
7669
|
+
surfaces: CLI_ONLY,
|
|
7670
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7671
|
+
run: (stack) => validateViewContainers(stack)
|
|
7672
|
+
},
|
|
7673
|
+
// ADR-0021 (#1719/#1721) — a widget's `dataset`/`dimensions`/`values` and its
|
|
7674
|
+
// chartConfig axis/series must resolve against the declared datasets.
|
|
7675
|
+
{
|
|
7676
|
+
name: "validateWidgetBindings",
|
|
7677
|
+
tier: "gating",
|
|
7678
|
+
input: "parsed",
|
|
7679
|
+
commands: ALL,
|
|
7680
|
+
source: "packages/lint/src/validate-widget-bindings.ts",
|
|
7681
|
+
surfaces: CLI_ONLY,
|
|
7682
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7683
|
+
run: (stack) => validateWidgetBindings(stack)
|
|
7684
|
+
},
|
|
7685
|
+
// ADR-0049 / #3367 — a header or widget action naming a `script`/`modal`
|
|
7686
|
+
// target that resolves to no defined action ships a button that renders and
|
|
7687
|
+
// silently does nothing on click. Unresolved `url` routes stay advisory.
|
|
7688
|
+
{
|
|
7689
|
+
name: "validateDashboardActionRefs",
|
|
7690
|
+
tier: "gating",
|
|
7691
|
+
input: "parsed",
|
|
7692
|
+
commands: ALL,
|
|
7693
|
+
source: "packages/lint/src/validate-dashboard-action-refs.ts",
|
|
7694
|
+
surfaces: CLI_ONLY,
|
|
7695
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7696
|
+
run: (stack) => validateDashboardActionRefs(stack)
|
|
7697
|
+
},
|
|
7698
|
+
// #3574 — a filter value like `{current_user}` resolves in no vocabulary,
|
|
7699
|
+
// reaches the data engine as a literal and matches nothing. The surface
|
|
7700
|
+
// renders empty with no error, and a silent zero is indistinguishable from a
|
|
7701
|
+
// genuine one at review time.
|
|
7702
|
+
{
|
|
7703
|
+
name: "validateFilterTokens",
|
|
7704
|
+
tier: "gating",
|
|
7705
|
+
input: "parsed",
|
|
7706
|
+
commands: ALL,
|
|
7707
|
+
source: "packages/lint/src/validate-filter-tokens.ts",
|
|
7708
|
+
surfaces: CLI_ONLY,
|
|
7709
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7710
|
+
run: (stack) => validateFilterTokens(stack)
|
|
7711
|
+
},
|
|
7712
|
+
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
7713
|
+
// rules that answer "does this name resolve to anything?". It reached all
|
|
7714
|
+
// three commands before this file existed; it is an entry here so the two
|
|
7715
|
+
// registries compose instead of competing, and so its members are covered by
|
|
7716
|
+
// the same guard as everything else.
|
|
7717
|
+
{
|
|
7718
|
+
name: "validateReferenceIntegrity",
|
|
7719
|
+
tier: "gating",
|
|
7720
|
+
input: "parsed",
|
|
7721
|
+
commands: ALL,
|
|
7722
|
+
source: "packages/lint/src/reference-integrity-suite.ts",
|
|
7723
|
+
// Runtime publish gate (#4463): the REFERENCE family. On a flow snapshot the
|
|
7724
|
+
// members that answer a flow question run (`validateFlowTemplatePaths`,
|
|
7725
|
+
// `validateFlowNodeWrites`, `validateReadonlyFlowWrites`,
|
|
7726
|
+
// `validateObjectReferences`); the page/nav/AI members see no such
|
|
7727
|
+
// collection in the snapshot and return nothing, and the two that would
|
|
7728
|
+
// load `typescript` need a hook/action/react body the snapshot never
|
|
7729
|
+
// carries — which is what `runtime-lazy-deps.test.ts` pins.
|
|
7730
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7731
|
+
runtimeTypes: ["flow"],
|
|
7732
|
+
run: (stack) => validateReferenceIntegrity(stack)
|
|
7733
|
+
},
|
|
7734
|
+
// ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
|
|
7735
|
+
// `id`), name real CSS properties + design tokens, and carry a `large` base.
|
|
7736
|
+
{
|
|
7737
|
+
name: "validateResponsiveStyles",
|
|
7738
|
+
tier: "gating",
|
|
7739
|
+
input: "parsed",
|
|
7740
|
+
commands: ALL,
|
|
7741
|
+
source: "packages/lint/src/validate-responsive-styles.ts",
|
|
7742
|
+
surfaces: CLI_ONLY,
|
|
7743
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7744
|
+
run: (stack) => validateResponsiveStyles(stack)
|
|
7745
|
+
},
|
|
7746
|
+
// ADR-0080 — a `kind:'jsx'` page's `source` is parsed (never executed) and
|
|
7747
|
+
// compiled to the SDUI tree at save time, so malformed source must fail loudly
|
|
7748
|
+
// here (ADR-0078) instead of being stored and breaking only at render.
|
|
7749
|
+
{
|
|
7750
|
+
name: "validateJsxPages",
|
|
7751
|
+
tier: "gating",
|
|
7752
|
+
input: "parsed",
|
|
7753
|
+
commands: ALL,
|
|
7754
|
+
source: "packages/lint/src/validate-jsx-pages.ts",
|
|
7755
|
+
surfaces: CLI_ONLY,
|
|
7756
|
+
surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
|
|
7757
|
+
run: (stack, ctx) => validateJsxPages(stack, ctx.sduiManifest ? { manifest: ctx.sduiManifest } : {})
|
|
7758
|
+
},
|
|
7759
|
+
// ADR-0081 — a `kind:'react'` page's `source` is real React executed at
|
|
7760
|
+
// render. Transpiled here (Sucrase, never executed) so a syntax error fails at
|
|
7761
|
+
// author time, not at render. Lazy: only a stack with such a page pays.
|
|
7762
|
+
{
|
|
7763
|
+
name: "validateReactPages",
|
|
7764
|
+
tier: "gating",
|
|
7765
|
+
input: "parsed",
|
|
7766
|
+
commands: ALL,
|
|
7767
|
+
source: "packages/lint/src/validate-react-pages.ts",
|
|
7768
|
+
surfaces: CLI_ONLY,
|
|
7769
|
+
surfaceReason: RUNTIME_HEAVY_SOURCE_PARSE,
|
|
7770
|
+
run: (stack) => validateReactPages(stack)
|
|
7771
|
+
},
|
|
7772
|
+
// ADR-0065, source tier — Tailwind `className` in a `kind:'html'`/`'react'`
|
|
7773
|
+
// page silently no-ops (the build never scans authored metadata).
|
|
7774
|
+
{
|
|
7775
|
+
name: "validatePageSourceStyling",
|
|
7776
|
+
tier: "advisory",
|
|
7777
|
+
input: "parsed",
|
|
7778
|
+
commands: ALL,
|
|
7779
|
+
source: "packages/lint/src/validate-page-source-styling.ts",
|
|
7780
|
+
surfaces: CLI_ONLY,
|
|
7781
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7782
|
+
run: (stack) => validatePageSourceStyling(stack)
|
|
7783
|
+
},
|
|
7784
|
+
// ADR-0066 ⑨ — a `requiredPermissions` entry naming a capability registered
|
|
7785
|
+
// nowhere fails closed at runtime. Advisory: another installed package may
|
|
7786
|
+
// legitimately provide it.
|
|
7787
|
+
{
|
|
7788
|
+
name: "validateCapabilityReferences",
|
|
7789
|
+
tier: "advisory",
|
|
7790
|
+
input: "parsed",
|
|
7791
|
+
commands: ALL,
|
|
7792
|
+
source: "packages/lint/src/validate-capability-references.ts",
|
|
7793
|
+
surfaces: CLI_ONLY,
|
|
7794
|
+
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.',
|
|
7795
|
+
run: (stack) => validateCapabilityReferences(stack)
|
|
7796
|
+
},
|
|
7797
|
+
// A record-change flow whose start-node objectName matches nothing never
|
|
7798
|
+
// fires — silently. Reads the pre-parse tier so an author sees what they
|
|
7799
|
+
// wrote. Advisory: the object may come from another installed package.
|
|
7800
|
+
{
|
|
7801
|
+
name: "validateFlowTriggerReadiness",
|
|
7802
|
+
tier: "advisory",
|
|
7803
|
+
input: "normalized",
|
|
7804
|
+
commands: ALL,
|
|
7805
|
+
source: "packages/lint/src/validate-flow-trigger-readiness.ts",
|
|
7806
|
+
// Runtime publish gate (#4463): the FLOW family. Advisory at this surface
|
|
7807
|
+
// too — its findings are logged, not thrown (P1 gates on `error` only; P2
|
|
7808
|
+
// puts advisories on the response for Studio to render).
|
|
7809
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7810
|
+
runtimeTypes: ["flow"],
|
|
7811
|
+
run: (stack) => validateFlowTriggerReadiness(stack)
|
|
7812
|
+
},
|
|
7813
|
+
// ADR-0090 D3 fallout — an approval `{ type: 'role' }` resolves against the
|
|
7814
|
+
// better-auth org-membership tier, not positions, so a position name authored
|
|
7815
|
+
// there routes the approval to nobody; and an expression approver that does
|
|
7816
|
+
// not parse can never resolve. The rule whose absence from `os build` and
|
|
7817
|
+
// `os validate` was #4409's worked example: it gates, and it ran on `os lint`
|
|
7818
|
+
// alone, so a broken approval flow built and published green.
|
|
7819
|
+
{
|
|
7820
|
+
name: "validateApprovalApprovers",
|
|
7821
|
+
tier: "gating",
|
|
7822
|
+
input: "parsed",
|
|
7823
|
+
commands: ALL,
|
|
7824
|
+
source: "packages/lint/src/validate-approval-approvers.ts",
|
|
7825
|
+
// Runtime publish gate (#4463): the APPROVAL family, and the issue's worked
|
|
7826
|
+
// example both times. #4409 fixed it for `os build`; a tenant saving the
|
|
7827
|
+
// same broken expression approver from Studio still sailed through, because
|
|
7828
|
+
// `approver.value` is just a string to Zod. It is not just a string here.
|
|
7829
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7830
|
+
runtimeTypes: ["flow"],
|
|
7831
|
+
run: (stack) => validateApprovalApprovers(stack)
|
|
7832
|
+
},
|
|
7833
|
+
// ADR-0079 — `titleFormat` is retired in favour of `nameField`, and an object
|
|
7834
|
+
// with no resolvable title ships records with no meaningful name. Advisory:
|
|
7835
|
+
// auto-provision and the `Record #<id>` floor keep it from ever being fatal.
|
|
7836
|
+
{
|
|
7837
|
+
name: "validateRecordTitle",
|
|
7838
|
+
tier: "advisory",
|
|
7839
|
+
input: "parsed",
|
|
7840
|
+
commands: ALL,
|
|
7841
|
+
source: "packages/lint/src/validate-record-title.ts",
|
|
7842
|
+
surfaces: CLI_ONLY,
|
|
7843
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7844
|
+
run: (stack) => validateRecordTitle(stack)
|
|
7845
|
+
},
|
|
7846
|
+
// ADR-0085 — `stageField` / `highlightFields` / `Field.group` are pointers
|
|
7847
|
+
// into the object's field map; a dangling one is Zod-valid and silently inert
|
|
7848
|
+
// at render. Advisory: every consumer degrades gracefully.
|
|
7849
|
+
{
|
|
7850
|
+
name: "validateSemanticRoles",
|
|
7851
|
+
tier: "advisory",
|
|
7852
|
+
input: "parsed",
|
|
7853
|
+
commands: ALL,
|
|
7854
|
+
source: "packages/lint/src/validate-semantic-roles.ts",
|
|
7855
|
+
surfaces: CLI_ONLY,
|
|
7856
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7857
|
+
run: (stack) => validateSemanticRoles(stack)
|
|
7858
|
+
},
|
|
7859
|
+
// #2578 / #4449 — a form section's field reference that resolves to nothing
|
|
7860
|
+
// (silently not rendered) and an absolute `colSpan` under a per-surface
|
|
7861
|
+
// derived column count. Advisory: the renderer skips the unknown field and
|
|
7862
|
+
// clamps the span, so nothing is broken — but each is almost certainly an
|
|
7863
|
+
// authoring mistake, and until #4449 this rule ran on no command at all.
|
|
7864
|
+
// Pure structured-metadata walk (no lazy dependency), so wiring it to all
|
|
7865
|
+
// three costs nothing measurable.
|
|
7866
|
+
{
|
|
7867
|
+
name: "validateFormLayout",
|
|
7868
|
+
tier: "advisory",
|
|
7869
|
+
input: "parsed",
|
|
7870
|
+
commands: ALL,
|
|
7871
|
+
source: "packages/lint/src/validate-form-layout.ts",
|
|
7872
|
+
surfaces: CLI_ONLY,
|
|
7873
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7874
|
+
run: (stack) => validateFormLayout(stack)
|
|
7875
|
+
},
|
|
7876
|
+
// ADR-0078 Phase 3 (Tier-A `action-locations`) — an action that declares no
|
|
7877
|
+
// `locations` and that no view places by name renders on no surface at all.
|
|
7878
|
+
// objectui#3142 made that measurable: four renderers used to show an
|
|
7879
|
+
// undeclared action anyway, and now none does. Advisory: a view in another
|
|
7880
|
+
// installed package may be the one placing it, and `locations: []` (the
|
|
7881
|
+
// documented headless shape) is deliberately never flagged.
|
|
7882
|
+
{
|
|
7883
|
+
name: "validateActionLocations",
|
|
7884
|
+
tier: "advisory",
|
|
7885
|
+
input: "parsed",
|
|
7886
|
+
commands: ALL,
|
|
7887
|
+
source: "packages/lint/src/validate-action-locations.ts",
|
|
7888
|
+
surfaces: CLI_ONLY,
|
|
7889
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7890
|
+
run: (stack) => validateActionLocations(stack)
|
|
7891
|
+
},
|
|
7892
|
+
// framework#3434 — seeds replay on every boot, so a `mode: 'insert'` dataset
|
|
7893
|
+
// duplicates its table on every restart.
|
|
7894
|
+
{
|
|
7895
|
+
name: "validateSeedReplaySafety",
|
|
7896
|
+
tier: "advisory",
|
|
7897
|
+
input: "parsed",
|
|
7898
|
+
commands: ALL,
|
|
7899
|
+
source: "packages/lint/src/validate-seed-replay-safety.ts",
|
|
7900
|
+
surfaces: CLI_ONLY,
|
|
7901
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7902
|
+
run: (stack) => validateSeedReplaySafety(stack)
|
|
7903
|
+
},
|
|
7904
|
+
// framework#3433 follow-up — #3433 exempts seed writes from the
|
|
7905
|
+
// `state_machine` rule, so a seeded status the FSM does not declare is no
|
|
7906
|
+
// longer rejected at write time. Re-added at author time; advisory, because
|
|
7907
|
+
// the exemption itself is legitimate.
|
|
7908
|
+
{
|
|
7909
|
+
name: "validateSeedStateMachine",
|
|
7910
|
+
tier: "advisory",
|
|
7911
|
+
input: "parsed",
|
|
7912
|
+
commands: ALL,
|
|
7913
|
+
source: "packages/lint/src/validate-seed-state-machine.ts",
|
|
7914
|
+
surfaces: CLI_ONLY,
|
|
7915
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7916
|
+
run: (stack) => validateSeedStateMachine(stack)
|
|
7917
|
+
},
|
|
7918
|
+
// ADR-0089 D3b — deprecated visibility aliases and a mis-layered binding root.
|
|
7919
|
+
// Pre-parse: the schema folds `visibleOn`/`visibility` into `visibleWhen`
|
|
7920
|
+
// during parse, so the alias the author wrote is gone from `result.data`.
|
|
7921
|
+
{
|
|
7922
|
+
name: "validateVisibilityPredicates",
|
|
7923
|
+
tier: "advisory",
|
|
7924
|
+
input: "normalized",
|
|
7925
|
+
commands: ALL,
|
|
7926
|
+
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
7927
|
+
surfaces: CLI_ONLY,
|
|
7928
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
7929
|
+
run: (stack) => validateVisibilityPredicates(stack)
|
|
7930
|
+
},
|
|
7931
|
+
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
7932
|
+
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
7933
|
+
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
7934
|
+
// `flow-default-edge-with-condition` (#4414 — a declaration that is inert, so
|
|
7935
|
+
// the route silently differs from what the author wrote). The bar for
|
|
7936
|
+
// promoting one is stated at the top of `lint-flow-patterns.ts`.
|
|
7937
|
+
{
|
|
7938
|
+
name: "lintFlowPatterns",
|
|
7939
|
+
tier: "gating",
|
|
7940
|
+
input: "parsed",
|
|
7941
|
+
commands: ALL,
|
|
7942
|
+
source: "packages/lint/src/lint-flow-patterns.ts",
|
|
7943
|
+
// Runtime publish gate (#4463): the FLOW family's anti-pattern half. Its
|
|
7944
|
+
// three `error` rules are the sharpest fit for a publish gate that exists —
|
|
7945
|
+
// `flow-runas-unscoped` is metadata the automation engine REFUSES to
|
|
7946
|
+
// execute, so publishing it can only ever produce a broken flow.
|
|
7947
|
+
surfaces: CLI_AND_RUNTIME,
|
|
7948
|
+
runtimeTypes: ["flow"],
|
|
7949
|
+
run: (stack) => lintFlowPatterns(stack).map((f) => ({
|
|
7950
|
+
severity: f.severity ?? "warning",
|
|
7951
|
+
rule: f.rule,
|
|
7952
|
+
where: f.where,
|
|
7953
|
+
path: f.where,
|
|
7954
|
+
message: f.message,
|
|
7955
|
+
hint: f.hint
|
|
7956
|
+
}))
|
|
7957
|
+
},
|
|
7958
|
+
// The spec-liveness loop on the author side: a property the ledger marks
|
|
7959
|
+
// dead-and-misleading or experimental is set hopefully and does nothing.
|
|
7960
|
+
// Ledger-driven (entries opt in via `authorWarn`), so it is high-signal and
|
|
7961
|
+
// never fatal.
|
|
7962
|
+
{
|
|
7963
|
+
name: "lintLivenessProperties",
|
|
7964
|
+
tier: "advisory",
|
|
7965
|
+
input: "parsed",
|
|
7966
|
+
commands: ALL,
|
|
7967
|
+
source: "packages/lint/src/lint-liveness-properties.ts",
|
|
7968
|
+
surfaces: CLI_ONLY,
|
|
7969
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7970
|
+
run: (stack) => lintLivenessProperties(stack).map((f) => ({
|
|
7971
|
+
severity: "warning",
|
|
7972
|
+
rule: f.rule,
|
|
7973
|
+
where: f.where,
|
|
7974
|
+
path: f.where,
|
|
7975
|
+
message: f.message,
|
|
7976
|
+
hint: f.hint
|
|
7977
|
+
}))
|
|
7978
|
+
},
|
|
7979
|
+
// A format like `{plan_no}{000}` makes the referenced field part of the
|
|
7980
|
+
// counter scope, so it must exist and be set at create time. Unknown field →
|
|
7981
|
+
// broken (error); optional field → fragile (warning).
|
|
7982
|
+
{
|
|
7983
|
+
name: "lintAutonumberFormats",
|
|
7984
|
+
tier: "gating",
|
|
7985
|
+
input: "parsed",
|
|
7986
|
+
commands: ALL,
|
|
7987
|
+
source: "packages/lint/src/lint-autonumber-formats.ts",
|
|
7988
|
+
surfaces: CLI_ONLY,
|
|
7989
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
7990
|
+
run: (stack) => lintAutonumberFormats(stack).map((f) => ({
|
|
7991
|
+
severity: f.severity,
|
|
7992
|
+
rule: f.rule,
|
|
7993
|
+
where: f.where,
|
|
7994
|
+
path: f.where,
|
|
7995
|
+
message: f.message,
|
|
7996
|
+
hint: f.hint
|
|
7997
|
+
}))
|
|
7998
|
+
},
|
|
7999
|
+
// #2554 — a `type:'form'` action target naming a missing or LIST view opens a
|
|
8000
|
+
// broken form at runtime; a list/form view-key collision silently renames one
|
|
8001
|
+
// view so references resolve to the OTHER. Both are broken.
|
|
8002
|
+
{
|
|
8003
|
+
name: "lintViewRefs",
|
|
8004
|
+
tier: "gating",
|
|
8005
|
+
input: "parsed",
|
|
8006
|
+
commands: ALL,
|
|
8007
|
+
source: "packages/lint/src/lint-view-refs.ts",
|
|
8008
|
+
surfaces: CLI_ONLY,
|
|
8009
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8010
|
+
run: (stack) => lintViewRefs(stack).map((f) => ({
|
|
8011
|
+
severity: f.severity,
|
|
8012
|
+
rule: f.rule,
|
|
8013
|
+
where: f.where,
|
|
8014
|
+
path: f.where,
|
|
8015
|
+
message: f.message,
|
|
8016
|
+
hint: f.hint
|
|
8017
|
+
}))
|
|
8018
|
+
},
|
|
8019
|
+
// #3991 — a column carrying BOTH a field-level `unique: true` and a
|
|
8020
|
+
// single-column declared unique index has two intents, of which exactly one
|
|
8021
|
+
// takes effect (the global index wins; the tenant composite is unreachable).
|
|
8022
|
+
{
|
|
8023
|
+
name: "lintUniqueDeclarations",
|
|
8024
|
+
tier: "advisory",
|
|
8025
|
+
input: "parsed",
|
|
8026
|
+
commands: ["validate", "build"],
|
|
8027
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
8028
|
+
surfaces: CLI_ONLY,
|
|
8029
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8030
|
+
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.",
|
|
8031
|
+
run: (stack) => lintUniqueDeclarations(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
8032
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
8033
|
+
rule: f.rule,
|
|
8034
|
+
where: f.path,
|
|
8035
|
+
path: f.path,
|
|
8036
|
+
message: f.message,
|
|
8037
|
+
hint: f.fix ?? ""
|
|
8038
|
+
}))
|
|
8039
|
+
},
|
|
8040
|
+
// ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
|
|
8041
|
+
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
8042
|
+
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
8043
|
+
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
8044
|
+
{
|
|
8045
|
+
name: "validateSecurityPosture",
|
|
8046
|
+
tier: "gating",
|
|
8047
|
+
input: "parsed",
|
|
8048
|
+
commands: ALL,
|
|
8049
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
8050
|
+
surfaces: CLI_ONLY,
|
|
8051
|
+
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.",
|
|
8052
|
+
run: (stack) => validateSecurityPosture(stack)
|
|
8053
|
+
},
|
|
8054
|
+
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
8055
|
+
// sharing rule that walks it builds a second permission hierarchy (the
|
|
8056
|
+
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
8057
|
+
// so it grants nothing it appears to.
|
|
8058
|
+
{
|
|
8059
|
+
name: "validateOrgAxisRedLines",
|
|
8060
|
+
tier: "gating",
|
|
8061
|
+
input: "parsed",
|
|
8062
|
+
commands: ALL,
|
|
8063
|
+
source: "packages/lint/src/validate-org-axis-red-lines.ts",
|
|
8064
|
+
surfaces: CLI_ONLY,
|
|
8065
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8066
|
+
run: (stack) => validateOrgAxisRedLines(stack)
|
|
8067
|
+
}
|
|
8068
|
+
];
|
|
8069
|
+
function authoringRulesFor(command) {
|
|
8070
|
+
return AUTHORING_RULES.filter((r) => r.commands.includes(command));
|
|
8071
|
+
}
|
|
8072
|
+
function runAuthoringRules(command, run) {
|
|
8073
|
+
const findings = [];
|
|
8074
|
+
const ctx = { sduiManifest: run.sduiManifest };
|
|
8075
|
+
for (const rule of authoringRulesFor(command)) {
|
|
8076
|
+
const stack = rule.input === "normalized" ? run.normalized : run.parsed ?? run.normalized;
|
|
8077
|
+
findings.push(...rule.run(stack, ctx));
|
|
8078
|
+
}
|
|
8079
|
+
return findings;
|
|
8080
|
+
}
|
|
8081
|
+
function splitBySeverity(findings) {
|
|
8082
|
+
return {
|
|
8083
|
+
errors: findings.filter((f) => f.severity === "error"),
|
|
8084
|
+
advisories: findings.filter((f) => f.severity !== "error")
|
|
8085
|
+
};
|
|
8086
|
+
}
|
|
8087
|
+
|
|
8088
|
+
// src/runtime-gate.ts
|
|
8089
|
+
var TYPE_TO_STACK_KEY = {
|
|
8090
|
+
flow: "flows",
|
|
8091
|
+
object: "objects",
|
|
8092
|
+
view: "views",
|
|
8093
|
+
action: "actions",
|
|
8094
|
+
page: "pages",
|
|
8095
|
+
dashboard: "dashboards",
|
|
8096
|
+
agent: "agents",
|
|
8097
|
+
hook: "hooks",
|
|
8098
|
+
seed: "seeds"
|
|
8099
|
+
};
|
|
8100
|
+
function runtimeAuthoringRulesFor(type) {
|
|
8101
|
+
return AUTHORING_RULES.filter(
|
|
8102
|
+
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
8103
|
+
);
|
|
8104
|
+
}
|
|
8105
|
+
function runtimeGatedTypes() {
|
|
8106
|
+
const types = /* @__PURE__ */ new Set();
|
|
8107
|
+
for (const rule of AUTHORING_RULES) {
|
|
8108
|
+
if (!rule.surfaces.includes("runtime-publish")) continue;
|
|
8109
|
+
for (const t of rule.runtimeTypes ?? []) types.add(t);
|
|
8110
|
+
}
|
|
8111
|
+
return [...types].sort();
|
|
8112
|
+
}
|
|
8113
|
+
function stackKeyForType(type) {
|
|
8114
|
+
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
8115
|
+
}
|
|
8116
|
+
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
8117
|
+
function runRules(rules, stack, ctx) {
|
|
8118
|
+
const findings = [];
|
|
8119
|
+
for (const rule of rules) {
|
|
8120
|
+
try {
|
|
8121
|
+
findings.push(...rule.run(stack, ctx));
|
|
8122
|
+
} catch (err) {
|
|
8123
|
+
findings.push({
|
|
8124
|
+
severity: "warning",
|
|
8125
|
+
rule: "authoring-rule-threw",
|
|
8126
|
+
where: rule.name,
|
|
8127
|
+
path: rule.source,
|
|
8128
|
+
message: `rule ${rule.name} threw while judging this write: ${err instanceof Error ? err.message : String(err)}`,
|
|
8129
|
+
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."
|
|
8130
|
+
});
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
return findings;
|
|
8134
|
+
}
|
|
8135
|
+
function runRuntimeAuthoringRules(args) {
|
|
8136
|
+
const rules = runtimeAuthoringRulesFor(args.type);
|
|
8137
|
+
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
8138
|
+
if (rules.length === 0) return empty;
|
|
8139
|
+
const stackKey = stackKeyForType(args.type);
|
|
8140
|
+
if (!stackKey) return empty;
|
|
8141
|
+
if (!args.item || typeof args.item !== "object") return empty;
|
|
8142
|
+
const item = args.item;
|
|
8143
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
8144
|
+
const contextObjects = args.context?.objects ?? [];
|
|
8145
|
+
const ctx = { sduiManifest: args.sduiManifest };
|
|
8146
|
+
const writesIntoContext = stackKey === "objects";
|
|
8147
|
+
const baselineObjects = writesIntoContext ? contextObjects.filter((o) => !itemName || o?.name !== itemName) : contextObjects;
|
|
8148
|
+
const baseline = { objects: baselineObjects };
|
|
8149
|
+
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
8150
|
+
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
8151
|
+
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
8152
|
+
return {
|
|
8153
|
+
errors: added.filter((f) => f.severity === "error"),
|
|
8154
|
+
advisories: added.filter((f) => f.severity !== "error"),
|
|
8155
|
+
rulesRun: rules.map((r) => r.name)
|
|
8156
|
+
};
|
|
8157
|
+
}
|
|
5956
8158
|
// Annotate the CommonJS export names for ESM import in node:
|
|
5957
8159
|
0 && (module.exports = {
|
|
5958
8160
|
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
@@ -5960,6 +8162,7 @@ function validateReferenceIntegrity(stack) {
|
|
|
5960
8162
|
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
5961
8163
|
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
5962
8164
|
ACTION_NAME_UNDEFINED,
|
|
8165
|
+
ACTION_NO_PLACEMENT,
|
|
5963
8166
|
ACTION_RECORD_WRITE_DISCARDED,
|
|
5964
8167
|
ACTION_RECORD_WRITE_PATTERNS,
|
|
5965
8168
|
ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
@@ -5975,6 +8178,13 @@ function validateReferenceIntegrity(stack) {
|
|
|
5975
8178
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
5976
8179
|
APPROVAL_EXPRESSION_INVALID,
|
|
5977
8180
|
APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
|
|
8181
|
+
AUTHORING_COMMANDS,
|
|
8182
|
+
AUTHORING_RULES,
|
|
8183
|
+
AUTHORING_SURFACES,
|
|
8184
|
+
AUTONUMBER_LITERAL_TOKEN,
|
|
8185
|
+
AUTONUMBER_OPTIONAL_FIELD,
|
|
8186
|
+
AUTONUMBER_SELF_REFERENCE,
|
|
8187
|
+
AUTONUMBER_UNKNOWN_FIELD,
|
|
5978
8188
|
CAPABILITY_REFERENCE_UNKNOWN,
|
|
5979
8189
|
CHART_AXIS_NOT_SELECTED,
|
|
5980
8190
|
CHART_CONFIG_MISSING,
|
|
@@ -5985,13 +8195,29 @@ function validateReferenceIntegrity(stack) {
|
|
|
5985
8195
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
5986
8196
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
5987
8197
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
8198
|
+
EXPRESSION_INVALID,
|
|
5988
8199
|
FIELD_GROUP_EMPTY,
|
|
5989
8200
|
FIELD_GROUP_UNDECLARED,
|
|
5990
8201
|
FILTER_TOKEN_UNKNOWN,
|
|
8202
|
+
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
8203
|
+
FLOW_APPROVAL_REVISE_DISABLED,
|
|
8204
|
+
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
8205
|
+
FLOW_BARE_DOLLAR_REF,
|
|
8206
|
+
FLOW_BRANCH_LABEL_UNMATCHED,
|
|
8207
|
+
FLOW_DATE_EQUALITY_FILTER,
|
|
8208
|
+
FLOW_DECISION_UNCONDITIONAL_BRANCH,
|
|
8209
|
+
FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
8210
|
+
FLOW_DOUBLE_BRACE_INTERP,
|
|
5991
8211
|
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
8212
|
+
FLOW_ERROR_LABEL_NOT_FAULT,
|
|
8213
|
+
FLOW_INERT_NODE_CONDITION,
|
|
8214
|
+
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
5992
8215
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
8216
|
+
FLOW_PHANTOM_AGGREGATION,
|
|
8217
|
+
FLOW_RUNAS_UNSCOPED,
|
|
5993
8218
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
5994
8219
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
8220
|
+
FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
5995
8221
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
5996
8222
|
FLOW_UPDATE_READONLY_FIELD,
|
|
5997
8223
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -6004,19 +8230,25 @@ function validateReferenceIntegrity(stack) {
|
|
|
6004
8230
|
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
6005
8231
|
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
6006
8232
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
8233
|
+
LIVENESS_DEAD_PROPERTY,
|
|
8234
|
+
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
6007
8235
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
6008
8236
|
NAV_OBJECT_UNGRANTED,
|
|
8237
|
+
NAV_TARGET_UNRESOLVED,
|
|
8238
|
+
NULL_GUARD_HINT,
|
|
6009
8239
|
OBJECT_REFERENCE_UNKNOWN,
|
|
6010
8240
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
6011
8241
|
ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
6012
8242
|
ORG_AXIS_PERMISSION_INHERITANCE,
|
|
6013
8243
|
PAGE_FIELD_UNKNOWN,
|
|
6014
8244
|
PAGE_SOURCE_CLASSNAME,
|
|
8245
|
+
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
6015
8246
|
REACT_CHART_AGGREGATE_INVALID,
|
|
6016
8247
|
REACT_CHART_AXIS_UNKNOWN,
|
|
6017
8248
|
REACT_CHART_FIELD_UNKNOWN,
|
|
6018
8249
|
REFERENCE_INTEGRITY_RULES,
|
|
6019
8250
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
8251
|
+
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
6020
8252
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
6021
8253
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
6022
8254
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -6041,17 +8273,37 @@ function validateReferenceIntegrity(stack) {
|
|
|
6041
8273
|
TITLE_UNRESOLVABLE,
|
|
6042
8274
|
TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
6043
8275
|
TRANSLATION_TARGET_UNKNOWN,
|
|
8276
|
+
UNIQUE_DOUBLE_DECLARATION,
|
|
6044
8277
|
VIEW_CONTAINER_SHAPE,
|
|
8278
|
+
VIEW_KEY_COLLISION,
|
|
8279
|
+
VIEW_REF_FORM_TARGET_KIND,
|
|
8280
|
+
VIEW_REF_FORM_TARGET_MISSING,
|
|
6045
8281
|
VISIBILITY_ALIAS_DEPRECATED,
|
|
6046
8282
|
VISIBILITY_ROOT_MISLAYERED,
|
|
6047
8283
|
WIDGET_DATASET_UNKNOWN,
|
|
6048
8284
|
WIDGET_DIMENSION_UNKNOWN,
|
|
6049
8285
|
WIDGET_MEASURE_UNKNOWN,
|
|
8286
|
+
authoringRulesFor,
|
|
6050
8287
|
buildAccessMatrix,
|
|
6051
8288
|
diffAccessMatrix,
|
|
6052
8289
|
extractHookBodyWriteSet,
|
|
6053
8290
|
extractHookBodyWrites,
|
|
8291
|
+
findUnguardedNullableOperands,
|
|
8292
|
+
lintAutonumberFormats,
|
|
8293
|
+
lintDataModel,
|
|
8294
|
+
lintFlowPatterns,
|
|
8295
|
+
lintLivenessProperties,
|
|
8296
|
+
lintUniqueDeclarations,
|
|
8297
|
+
lintViewRefs,
|
|
8298
|
+
nullGuardMessage,
|
|
8299
|
+
runAuthoringRules,
|
|
8300
|
+
runRuntimeAuthoringRules,
|
|
8301
|
+
runtimeAuthoringRulesFor,
|
|
8302
|
+
runtimeGatedTypes,
|
|
8303
|
+
splitBySeverity,
|
|
8304
|
+
stackKeyForType,
|
|
6054
8305
|
validateActionBodyWrites,
|
|
8306
|
+
validateActionLocations,
|
|
6055
8307
|
validateActionNameRefs,
|
|
6056
8308
|
validateAiAgentAuthoring,
|
|
6057
8309
|
validateAiSurfaceAffinity,
|
|
@@ -6065,10 +8317,12 @@ function validateReferenceIntegrity(stack) {
|
|
|
6065
8317
|
validateFlowTemplatePaths,
|
|
6066
8318
|
validateFlowTriggerReadiness,
|
|
6067
8319
|
validateFormLayout,
|
|
8320
|
+
validateFunctionalCompleteness,
|
|
6068
8321
|
validateHookBodyWrites,
|
|
6069
8322
|
validateJsxPages,
|
|
6070
8323
|
validateListViewMode,
|
|
6071
8324
|
validateNavAccess,
|
|
8325
|
+
validateNavTargetRefs,
|
|
6072
8326
|
validateObjectReferences,
|
|
6073
8327
|
validateOrgAxisRedLines,
|
|
6074
8328
|
validatePageFieldBindings,
|