@objectstack/lint 17.0.0-rc.2 → 17.0.0-rc.4
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 +1929 -0
- package/dist/index.cjs +1651 -459
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +527 -155
- package/dist/index.d.ts +527 -155
- package/dist/index.js +1601 -440
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +1466 -350
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.js +1449 -328
- package/dist/runtime.js.map +1 -1
- package/package.json +8 -6
package/dist/index.cjs
CHANGED
|
@@ -37,6 +37,7 @@ __export(index_exports, {
|
|
|
37
37
|
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER: () => APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
|
|
38
38
|
APPROVAL_APPROVER_TYPE_DEPRECATED: () => APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
39
39
|
APPROVAL_APPROVER_TYPE_UNKNOWN: () => APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
40
|
+
APPROVAL_APPROVER_TYPE_UNSUPPORTED: () => APPROVAL_APPROVER_TYPE_UNSUPPORTED,
|
|
40
41
|
APPROVAL_DECISION_OUTPUTS_RESERVED: () => APPROVAL_DECISION_OUTPUTS_RESERVED,
|
|
41
42
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET: () => APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
42
43
|
APPROVAL_EXPRESSION_INVALID: () => APPROVAL_EXPRESSION_INVALID,
|
|
@@ -55,15 +56,19 @@ __export(index_exports, {
|
|
|
55
56
|
CHART_DIMENSION_UNKNOWN: () => CHART_DIMENSION_UNKNOWN,
|
|
56
57
|
CHART_FIELD_UNKNOWN: () => CHART_FIELD_UNKNOWN,
|
|
57
58
|
CHART_MEASURE_UNKNOWN: () => CHART_MEASURE_UNKNOWN,
|
|
59
|
+
COMPONENT_PROPS_INVALID: () => COMPONENT_PROPS_INVALID,
|
|
60
|
+
COMPONENT_PROPS_UNKNOWN_KEY: () => COMPONENT_PROPS_UNKNOWN_KEY,
|
|
58
61
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
59
62
|
DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
60
63
|
DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
61
64
|
EXPRESSION_INVALID: () => EXPRESSION_INVALID,
|
|
62
65
|
FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
|
|
66
|
+
FIELD_GROUP_SHADOWED: () => FIELD_GROUP_SHADOWED,
|
|
63
67
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
64
68
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
65
69
|
FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
|
|
66
70
|
FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
|
|
71
|
+
FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED: () => FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
|
|
67
72
|
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE: () => FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
68
73
|
FLOW_BARE_DOLLAR_REF: () => FLOW_BARE_DOLLAR_REF,
|
|
69
74
|
FLOW_BRANCH_LABEL_UNMATCHED: () => FLOW_BRANCH_LABEL_UNMATCHED,
|
|
@@ -75,12 +80,16 @@ __export(index_exports, {
|
|
|
75
80
|
FLOW_ERROR_LABEL_NOT_FAULT: () => FLOW_ERROR_LABEL_NOT_FAULT,
|
|
76
81
|
FLOW_INERT_NODE_CONDITION: () => FLOW_INERT_NODE_CONDITION,
|
|
77
82
|
FLOW_MULTIPLE_DEFAULT_EDGES: () => FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
83
|
+
FLOW_MULTI_WRITE_UNFILTERED: () => FLOW_MULTI_WRITE_UNFILTERED,
|
|
78
84
|
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
79
85
|
FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
|
|
80
86
|
FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
|
|
81
87
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
82
88
|
FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
83
89
|
FLOW_TIME_RELATIVE_ANTIPATTERN: () => FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
90
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID: () => FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
|
|
91
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE: () => FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
92
|
+
FLOW_TRIGGER_UNKNOWN_EVENT: () => FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
84
93
|
FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
85
94
|
FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
|
|
86
95
|
FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -95,6 +104,7 @@ __export(index_exports, {
|
|
|
95
104
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
96
105
|
LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
|
|
97
106
|
LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
107
|
+
MAX_SCHEMA_WALK_DEPTH: () => MAX_SCHEMA_WALK_DEPTH,
|
|
98
108
|
MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
|
|
99
109
|
NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
|
|
100
110
|
NAV_TARGET_UNRESOLVED: () => NAV_TARGET_UNRESOLVED,
|
|
@@ -108,14 +118,19 @@ __export(index_exports, {
|
|
|
108
118
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
109
119
|
REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
|
|
110
120
|
REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
|
|
121
|
+
REACT_CHART_DRILLDOWN_INVALID: () => REACT_CHART_DRILLDOWN_INVALID,
|
|
111
122
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
112
123
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
124
|
+
RLS_PREDICATE_UNENFORCEABLE: () => RLS_PREDICATE_UNENFORCEABLE,
|
|
125
|
+
RLS_PREDICATE_UNPARSEABLE: () => RLS_PREDICATE_UNPARSEABLE,
|
|
126
|
+
RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
|
|
113
127
|
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
114
128
|
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
115
129
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
116
130
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
117
131
|
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
118
132
|
SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
|
|
133
|
+
SECURITY_FLS_UNQUALIFIED_KEY: () => SECURITY_FLS_UNQUALIFIED_KEY,
|
|
119
134
|
SECURITY_GRANT_EXPIRED_AT_AUTHORING: () => SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
120
135
|
SECURITY_MASTER_DETAIL_UNGRANTED: () => SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
121
136
|
SECURITY_OWD_ALIAS: () => SECURITY_OWD_ALIAS,
|
|
@@ -126,6 +141,8 @@ __export(index_exports, {
|
|
|
126
141
|
SEED_INSERT_MODE_DUPLICATES_ON_REPLAY: () => SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
|
|
127
142
|
SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
128
143
|
SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
144
|
+
SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
145
|
+
SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
129
146
|
STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
|
|
130
147
|
STYLE_NODE_MISSING_ID: () => STYLE_NODE_MISSING_ID,
|
|
131
148
|
STYLE_RESPONSIVE_NO_BASE: () => STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -135,8 +152,14 @@ __export(index_exports, {
|
|
|
135
152
|
TITLE_FORMAT_RETIRED: () => TITLE_FORMAT_RETIRED,
|
|
136
153
|
TITLE_UNRESOLVABLE: () => TITLE_UNRESOLVABLE,
|
|
137
154
|
TRANSLATION_OPTION_KEY_UNKNOWN: () => TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
155
|
+
TRANSLATION_SECTION_NAME_MISSING: () => TRANSLATION_SECTION_NAME_MISSING,
|
|
138
156
|
TRANSLATION_TARGET_UNKNOWN: () => TRANSLATION_TARGET_UNKNOWN,
|
|
139
157
|
UNIQUE_DOUBLE_DECLARATION: () => UNIQUE_DOUBLE_DECLARATION,
|
|
158
|
+
UNIQUE_LEGACY_ORGANIZATION_COMPOSITE: () => UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
159
|
+
UNIQUE_UNSCOPED_DECLARED_INDEX: () => UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
160
|
+
VALIDATION_RULE_REGEX_UNCOMPILABLE: () => VALIDATION_RULE_REGEX_UNCOMPILABLE,
|
|
161
|
+
VALIDATION_RULE_SCHEMA_UNCOMPILABLE: () => VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
|
|
162
|
+
VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT: () => VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
|
|
140
163
|
VIEW_CONTAINER_SHAPE: () => VIEW_CONTAINER_SHAPE,
|
|
141
164
|
VIEW_KEY_COLLISION: () => VIEW_KEY_COLLISION,
|
|
142
165
|
VIEW_REF_FORM_TARGET_KIND: () => VIEW_REF_FORM_TARGET_KIND,
|
|
@@ -145,6 +168,8 @@ __export(index_exports, {
|
|
|
145
168
|
VISIBILITY_ROOT_MISLAYERED: () => VISIBILITY_ROOT_MISLAYERED,
|
|
146
169
|
WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
|
|
147
170
|
WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
|
|
171
|
+
WIDGET_LEGACY_ANALYTICS_SHAPE: () => WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
172
|
+
WIDGET_LEGACY_ANALYTICS_UNRENDERABLE: () => WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
148
173
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
149
174
|
authoringRulesFor: () => authoringRulesFor,
|
|
150
175
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
@@ -152,12 +177,16 @@ __export(index_exports, {
|
|
|
152
177
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
153
178
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
154
179
|
findUnguardedNullableOperands: () => findUnguardedNullableOperands,
|
|
180
|
+
isSourceAuthoredPage: () => isSourceAuthoredPage,
|
|
155
181
|
lintAutonumberFormats: () => lintAutonumberFormats,
|
|
156
182
|
lintDataModel: () => lintDataModel,
|
|
157
183
|
lintFlowPatterns: () => lintFlowPatterns,
|
|
184
|
+
lintLegacyOrganizationComposites: () => lintLegacyOrganizationComposites,
|
|
158
185
|
lintLivenessProperties: () => lintLivenessProperties,
|
|
159
186
|
lintUniqueDeclarations: () => lintUniqueDeclarations,
|
|
187
|
+
lintUnscopedDeclaredIndexes: () => lintUnscopedDeclaredIndexes,
|
|
160
188
|
lintViewRefs: () => lintViewRefs,
|
|
189
|
+
nearestRegisteredFormat: () => nearestRegisteredFormat,
|
|
161
190
|
nullGuardMessage: () => nullGuardMessage,
|
|
162
191
|
runAuthoringRules: () => runAuthoringRules,
|
|
163
192
|
runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
|
|
@@ -174,6 +203,7 @@ __export(index_exports, {
|
|
|
174
203
|
validateApprovalApprovers: () => validateApprovalApprovers,
|
|
175
204
|
validateCapabilityReferences: () => validateCapabilityReferences,
|
|
176
205
|
validateChartBindings: () => validateChartBindings,
|
|
206
|
+
validateComponentProps: () => validateComponentProps,
|
|
177
207
|
validateDashboardActionRefs: () => validateDashboardActionRefs,
|
|
178
208
|
validateFilterTokens: () => validateFilterTokens,
|
|
179
209
|
validateFlowNodeWrites: () => validateFlowNodeWrites,
|
|
@@ -196,16 +226,22 @@ __export(index_exports, {
|
|
|
196
226
|
validateRecordTitle: () => validateRecordTitle,
|
|
197
227
|
validateReferenceIntegrity: () => validateReferenceIntegrity,
|
|
198
228
|
validateResponsiveStyles: () => validateResponsiveStyles,
|
|
229
|
+
validateRlsPredicateEnforceability: () => validateRlsPredicateEnforceability,
|
|
230
|
+
validateRuleCompilability: () => validateRuleCompilability,
|
|
231
|
+
validateRuleSchemaFormats: () => validateRuleSchemaFormats,
|
|
199
232
|
validateSearchableFields: () => validateSearchableFields,
|
|
200
233
|
validateSecurityPosture: () => validateSecurityPosture,
|
|
201
234
|
validateSeedReplaySafety: () => validateSeedReplaySafety,
|
|
202
235
|
validateSeedStateMachine: () => validateSeedStateMachine,
|
|
203
236
|
validateSemanticRoles: () => validateSemanticRoles,
|
|
237
|
+
validateSharingRuleEnforceability: () => validateSharingRuleEnforceability,
|
|
204
238
|
validateStackExpressions: () => validateStackExpressions,
|
|
239
|
+
validateTranslatableSections: () => validateTranslatableSections,
|
|
205
240
|
validateTranslationReferences: () => validateTranslationReferences,
|
|
206
241
|
validateViewContainers: () => validateViewContainers,
|
|
207
242
|
validateVisibilityPredicates: () => validateVisibilityPredicates,
|
|
208
|
-
validateWidgetBindings: () => validateWidgetBindings
|
|
243
|
+
validateWidgetBindings: () => validateWidgetBindings,
|
|
244
|
+
walkPageComponents: () => walkPageComponents
|
|
209
245
|
});
|
|
210
246
|
module.exports = __toCommonJS(index_exports);
|
|
211
247
|
|
|
@@ -220,6 +256,9 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
220
256
|
...import_data.FIELD_GROUP_SYSTEM_FIELDS,
|
|
221
257
|
...Object.values(import_system.SystemFieldName)
|
|
222
258
|
]);
|
|
259
|
+
function injectedColumnsFor(objectDef) {
|
|
260
|
+
return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
|
|
261
|
+
}
|
|
223
262
|
|
|
224
263
|
// src/validate-widget-bindings.ts
|
|
225
264
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
@@ -541,21 +580,14 @@ function validateWidgetBindings(stack) {
|
|
|
541
580
|
}
|
|
542
581
|
|
|
543
582
|
// src/validate-expressions.ts
|
|
544
|
-
var
|
|
583
|
+
var import_formula2 = require("@objectstack/formula");
|
|
545
584
|
var import_automation = require("@objectstack/spec/automation");
|
|
546
585
|
|
|
547
586
|
// src/validate-null-guards.ts
|
|
548
|
-
var
|
|
587
|
+
var import_formula = require("@objectstack/formula");
|
|
549
588
|
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
589
|
var FAULTING_BINARY_OPS = /* @__PURE__ */ new Set(["<", "<=", ">", ">=", "+", "-", "*", "/", "%"]);
|
|
551
590
|
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
591
|
function isNode(v) {
|
|
560
592
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
561
593
|
}
|
|
@@ -688,12 +720,8 @@ function findUnguardedNullableOperands(source, opts) {
|
|
|
688
720
|
if (typeof source !== "string" || !source.trim()) return [];
|
|
689
721
|
if (opts.nullableFields.size === 0) return [];
|
|
690
722
|
const roots = opts.roots ?? DEFAULT_RECORD_ROOTS;
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
ast = getParseEnv().parse(source).ast;
|
|
694
|
-
} catch {
|
|
695
|
-
return [];
|
|
696
|
-
}
|
|
723
|
+
const ast = (0, import_formula.parseCelToAst)(source);
|
|
724
|
+
if (!ast) return [];
|
|
697
725
|
const hasOperands = /* @__PURE__ */ new Set();
|
|
698
726
|
collectHasOperands(ast, roots, hasOperands);
|
|
699
727
|
const findings = [];
|
|
@@ -756,10 +784,14 @@ function findUnguardedNullableOperands(source, opts) {
|
|
|
756
784
|
visit(ast, /* @__PURE__ */ new Set());
|
|
757
785
|
return findings;
|
|
758
786
|
}
|
|
759
|
-
|
|
787
|
+
var OUTCOME_CLAUSE = {
|
|
788
|
+
"fail-closed": "so the rule enforces nothing and the write is rejected fail-closed (#4649/#4763)",
|
|
789
|
+
"fail-open": "so the predicate is SKIPPED fail-open \u2014 the field is never actually required, the write proceeds unchecked, and the only trace is a `requiredWhen \u2026 failed to evaluate \u2014 skipped` log line (#4649/#4811)"
|
|
790
|
+
};
|
|
791
|
+
function nullGuardMessage(subject, objectName, finding, outcome = "fail-closed") {
|
|
760
792
|
const owner = objectName ? `'${objectName}'` : "this object";
|
|
761
793
|
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
|
|
794
|
+
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 ${OUTCOME_CLAUSE[outcome]}. The predicate compares a value that is null. ${NULL_GUARD_HINT}`;
|
|
763
795
|
}
|
|
764
796
|
|
|
765
797
|
// src/validate-expressions.ts
|
|
@@ -779,7 +811,7 @@ function buildFieldIndex(objects) {
|
|
|
779
811
|
let names = [];
|
|
780
812
|
if (Array.isArray(fields)) names = fields.map((f) => f.name).filter((n) => typeof n === "string");
|
|
781
813
|
else if (fields && typeof fields === "object") names = Object.keys(fields);
|
|
782
|
-
idx.set(name, names);
|
|
814
|
+
idx.set(name, [.../* @__PURE__ */ new Set([...names, ...injectedColumnsFor(obj)])]);
|
|
783
815
|
}
|
|
784
816
|
return idx;
|
|
785
817
|
}
|
|
@@ -839,6 +871,19 @@ function buildNullableFieldIndex(objects) {
|
|
|
839
871
|
}
|
|
840
872
|
return idx;
|
|
841
873
|
}
|
|
874
|
+
function readsParentRoot(source) {
|
|
875
|
+
const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
|
|
876
|
+
return roots.ok && roots.roots.includes("parent");
|
|
877
|
+
}
|
|
878
|
+
function masterDetailCount(obj) {
|
|
879
|
+
let n = 0;
|
|
880
|
+
for (const [, def] of fieldEntries(obj)) {
|
|
881
|
+
if (def.type !== "master_detail") continue;
|
|
882
|
+
const ref = def.reference;
|
|
883
|
+
if (typeof ref === "string" && ref.trim() !== "") n += 1;
|
|
884
|
+
}
|
|
885
|
+
return n;
|
|
886
|
+
}
|
|
842
887
|
function celSourceOf(raw) {
|
|
843
888
|
if (typeof raw === "string") return raw;
|
|
844
889
|
if (raw && typeof raw === "object") {
|
|
@@ -852,7 +897,7 @@ function rulePredicates(rule, path) {
|
|
|
852
897
|
const out = [];
|
|
853
898
|
const name = typeof rule.name === "string" ? rule.name : "?";
|
|
854
899
|
const here = path ? `${path} \u2192 '${name}'` : `'${name}'`;
|
|
855
|
-
const main = rule.
|
|
900
|
+
const main = rule.condition;
|
|
856
901
|
if (main != null) out.push({ label: `validation rule ${here}`, raw: main });
|
|
857
902
|
if (rule.when != null) out.push({ label: `validation rule ${here} when-predicate`, raw: rule.when });
|
|
858
903
|
for (const branch of ["then", "otherwise"]) {
|
|
@@ -869,7 +914,7 @@ function validateStackExpressions(stack) {
|
|
|
869
914
|
const fieldIndex = buildFieldIndex(objects);
|
|
870
915
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
871
916
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
872
|
-
const checkNullGuards = (where, subject, raw, objectName) => {
|
|
917
|
+
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
873
918
|
if (!objectName) return;
|
|
874
919
|
const nullableFields = nullableIndex.get(objectName);
|
|
875
920
|
if (!nullableFields || nullableFields.size === 0) return;
|
|
@@ -878,7 +923,7 @@ function validateStackExpressions(stack) {
|
|
|
878
923
|
for (const finding of findUnguardedNullableOperands(source, { nullableFields })) {
|
|
879
924
|
issues.push({
|
|
880
925
|
where,
|
|
881
|
-
message: nullGuardMessage(subject, objectName, finding),
|
|
926
|
+
message: nullGuardMessage(subject, objectName, finding, outcome),
|
|
882
927
|
source,
|
|
883
928
|
severity: "error"
|
|
884
929
|
});
|
|
@@ -888,7 +933,7 @@ function validateStackExpressions(stack) {
|
|
|
888
933
|
if (raw == null) return;
|
|
889
934
|
const fields = objectName ? fieldIndex.get(objectName) : void 0;
|
|
890
935
|
const fieldTypes = objectName ? fieldTypeIndex.get(objectName) : void 0;
|
|
891
|
-
const res = (0,
|
|
936
|
+
const res = (0, import_formula2.validateExpression)(
|
|
892
937
|
"predicate",
|
|
893
938
|
raw,
|
|
894
939
|
objectName ? { objectName, fields, fieldTypes, scope } : { scope }
|
|
@@ -898,7 +943,7 @@ function validateStackExpressions(stack) {
|
|
|
898
943
|
};
|
|
899
944
|
const checkDeclaredPredicate = (where, raw) => {
|
|
900
945
|
if (raw == null) return;
|
|
901
|
-
const res = (0,
|
|
946
|
+
const res = (0, import_formula2.validateExpression)("predicate", raw);
|
|
902
947
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
903
948
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
904
949
|
};
|
|
@@ -947,31 +992,45 @@ function validateStackExpressions(stack) {
|
|
|
947
992
|
}
|
|
948
993
|
for (const obj of objects) {
|
|
949
994
|
const objectName = typeof obj.name === "string" ? obj.name : void 0;
|
|
950
|
-
const validations = obj.validations
|
|
995
|
+
const validations = obj.validations;
|
|
951
996
|
for (const rule of asArray2(validations)) {
|
|
952
997
|
const where = `object '${objectName}' \xB7 validation '${rule.name ?? "?"}'`;
|
|
953
|
-
check(where, rule.
|
|
998
|
+
check(where, rule.condition, objectName, "record");
|
|
954
999
|
check(`${where} when`, rule.when, objectName, "record");
|
|
955
1000
|
for (const p of rulePredicates(rule, "")) {
|
|
956
1001
|
checkNullGuards(`object '${objectName}' \xB7 ${p.label}`, p.label, p.raw, objectName);
|
|
957
1002
|
}
|
|
958
1003
|
}
|
|
959
1004
|
const fields = obj.fields;
|
|
960
|
-
const fieldList = Array.isArray(fields) ? fields : fields && typeof fields === "object" ? Object.
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1005
|
+
const fieldList = Array.isArray(fields) ? fields.filter((f) => !!f && typeof f === "object").map((f) => [typeof f.name === "string" ? f.name : "?", f]) : fields && typeof fields === "object" ? Object.entries(fields).filter(([, def]) => !!def && typeof def === "object").map(([n, def]) => [n, def]) : [];
|
|
1006
|
+
const masters = masterDetailCount(obj);
|
|
1007
|
+
for (const [fname, f] of fieldList) {
|
|
1008
|
+
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
1009
|
+
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
1010
|
+
}
|
|
1011
|
+
const roWhenSource = celSourceOf(f.readonlyWhen);
|
|
1012
|
+
if (masters !== 1 && roWhenSource && readsParentRoot(roWhenSource)) {
|
|
1013
|
+
issues.push({
|
|
1014
|
+
where: `object '${objectName}' \xB7 field '${fname}' readonlyWhen`,
|
|
1015
|
+
message: `\`readonlyWhen\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and the field would be locked on every write. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
|
|
1016
|
+
source: roWhenSource,
|
|
1017
|
+
severity: "error"
|
|
1018
|
+
});
|
|
967
1019
|
}
|
|
968
|
-
|
|
969
|
-
|
|
1020
|
+
checkNullGuards(
|
|
1021
|
+
`object '${objectName}' \xB7 field '${fname}' requiredWhen`,
|
|
1022
|
+
`field '${fname}' requiredWhen`,
|
|
1023
|
+
f.requiredWhen,
|
|
1024
|
+
objectName,
|
|
1025
|
+
"fail-open"
|
|
1026
|
+
);
|
|
1027
|
+
if (f.expression) {
|
|
1028
|
+
const res = (0, import_formula2.validateExpression)(
|
|
970
1029
|
"value",
|
|
971
|
-
f.
|
|
1030
|
+
f.expression,
|
|
972
1031
|
objectName ? { objectName, fields: fieldIndex.get(objectName), fieldTypes: fieldTypeIndex.get(objectName), scope: "record" } : { scope: "record" }
|
|
973
1032
|
);
|
|
974
|
-
const fieldWhere = `object '${objectName}' \xB7 field '${
|
|
1033
|
+
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
975
1034
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
976
1035
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
977
1036
|
}
|
|
@@ -979,7 +1038,7 @@ function validateStackExpressions(stack) {
|
|
|
979
1038
|
}
|
|
980
1039
|
const seenActions = /* @__PURE__ */ new Set();
|
|
981
1040
|
const checkAction = (where, action, objectName) => {
|
|
982
|
-
const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0)
|
|
1041
|
+
const obj = objectName ?? (typeof action.objectName === "string" ? action.objectName : void 0);
|
|
983
1042
|
const name = typeof action.name === "string" ? action.name : "?";
|
|
984
1043
|
const key = `${obj ?? ""}:${name}`;
|
|
985
1044
|
if (seenActions.has(key)) return;
|
|
@@ -998,10 +1057,10 @@ function validateStackExpressions(stack) {
|
|
|
998
1057
|
checkAction(`object '${objectName}'`, action, objectName);
|
|
999
1058
|
}
|
|
1000
1059
|
}
|
|
1001
|
-
for (const
|
|
1002
|
-
const ruleObj = typeof
|
|
1003
|
-
const where = `sharingRule '${
|
|
1004
|
-
check(where,
|
|
1060
|
+
for (const sharingRule of asArray2(stack.sharingRules)) {
|
|
1061
|
+
const ruleObj = typeof sharingRule.object === "string" ? sharingRule.object : void 0;
|
|
1062
|
+
const where = `sharingRule '${sharingRule.name ?? "?"}'${ruleObj ? ` (${ruleObj})` : ""} condition`;
|
|
1063
|
+
check(where, sharingRule.condition, ruleObj, "record");
|
|
1005
1064
|
}
|
|
1006
1065
|
for (const hook of asArray2(stack.hooks)) {
|
|
1007
1066
|
const hookName = hook.name ?? "?";
|
|
@@ -1180,9 +1239,12 @@ function validateFunctionalCompleteness(stack) {
|
|
|
1180
1239
|
}
|
|
1181
1240
|
|
|
1182
1241
|
// src/validate-flow-trigger-readiness.ts
|
|
1242
|
+
var import_automation2 = require("@objectstack/spec/automation");
|
|
1183
1243
|
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
1184
1244
|
var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
1185
1245
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
1246
|
+
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
1247
|
+
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
1186
1248
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
1187
1249
|
function asArray4(v) {
|
|
1188
1250
|
if (Array.isArray(v)) return v;
|
|
@@ -1194,6 +1256,12 @@ function asArray4(v) {
|
|
|
1194
1256
|
}
|
|
1195
1257
|
return [];
|
|
1196
1258
|
}
|
|
1259
|
+
function renderNonObject(v) {
|
|
1260
|
+
const t = typeof v;
|
|
1261
|
+
if (t === "string" || t === "number" || t === "boolean") return `${JSON.stringify(v)} (a ${t})`;
|
|
1262
|
+
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
1263
|
+
return `a ${t}`;
|
|
1264
|
+
}
|
|
1197
1265
|
function startNodeOf(flow) {
|
|
1198
1266
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
1199
1267
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -1241,10 +1309,35 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1241
1309
|
hint: `Object names match exactly. Check config.timeRelative.object against the object's registered name. If the object comes from another installed package, this warning can be ignored.`
|
|
1242
1310
|
});
|
|
1243
1311
|
}
|
|
1312
|
+
const parsed = import_automation2.TimeRelativeTriggerSchema.safeParse(tr);
|
|
1313
|
+
if (!parsed.success) {
|
|
1314
|
+
const problems = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message.replace(/\s+/g, " ").trim()}`).join("; ");
|
|
1315
|
+
findings.push({
|
|
1316
|
+
// `error` (#5762): the verdict is `TimeRelativeTriggerSchema`'s, and it
|
|
1317
|
+
// is the same schema the trigger safeParses at bind time. A descriptor
|
|
1318
|
+
// it refuses is refused at bind too — the sweep is never installed, on
|
|
1319
|
+
// every deployment, with no installed package able to change the
|
|
1320
|
+
// answer. Nothing is left for the author to weigh.
|
|
1321
|
+
severity: "error",
|
|
1322
|
+
rule: FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
|
|
1323
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1324
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
|
|
1325
|
+
message: `has a config.timeRelative descriptor the time-relative trigger REFUSES at bind time, so the sweep is never installed \u2014 the flow declares a time-relative trigger and then never runs (the only trace is one warn in the server log). ${problems}`,
|
|
1326
|
+
hint: `Those messages are TimeRelativeTriggerSchema's own \u2014 the same schema the trigger safeParses at bind time, so a descriptor that satisfies them binds. An unrecognized key names the declared key it was probably meant to be; see content/docs/references/automation/time-relative-trigger.mdx.`
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1244
1329
|
}
|
|
1245
1330
|
if (start && isRecordTriggered2 && !VALID_RECORD_TRIGGER.test((triggerType ?? "").trim())) {
|
|
1246
1331
|
findings.push({
|
|
1247
|
-
|
|
1332
|
+
// `error` (#5762). The token grammar is CLOSED and local: the engine
|
|
1333
|
+
// routes any `record-`-prefixed string to the record-change trigger by a
|
|
1334
|
+
// hardcoded prefix test (no registry lookup, so installing a package
|
|
1335
|
+
// cannot claim a new `record-*` token), and that trigger maps the token
|
|
1336
|
+
// with `triggerTypeToHookEvents` — the same regex this file's
|
|
1337
|
+
// `VALID_RECORD_TRIGGER` mirrors. Off-grammar means zero hook events,
|
|
1338
|
+
// which means bound-to-nothing on every deployment. Unlike an object
|
|
1339
|
+
// name, there is no other-package reading that rescues it.
|
|
1340
|
+
severity: "error",
|
|
1248
1341
|
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
1249
1342
|
where: `flow "${flowName}" \u203A start node`,
|
|
1250
1343
|
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
@@ -1254,7 +1347,14 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1254
1347
|
}
|
|
1255
1348
|
if (start && isArrayRecordTriggered) {
|
|
1256
1349
|
findings.push({
|
|
1257
|
-
|
|
1350
|
+
// `error` (#5762), same id and same reason as 1c: an array maps to no
|
|
1351
|
+
// hook event either. The engine routes it to the record-change trigger
|
|
1352
|
+
// for the express purpose of making it loud, and its own comment names
|
|
1353
|
+
// THIS rule as the primary catch — a primary catch that only warns is
|
|
1354
|
+
// the "declared ≠ enforced" shape the registry's tier exists to close.
|
|
1355
|
+
// Multi-event arrays are deferred, not unsupported-by-accident (#3457),
|
|
1356
|
+
// so if they land the grammar widens here in the same commit.
|
|
1357
|
+
severity: "error",
|
|
1258
1358
|
rule: FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
1259
1359
|
where: `flow "${flowName}" \u203A start node`,
|
|
1260
1360
|
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
@@ -1262,6 +1362,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1262
1362
|
hint: `Use one triggerType string. For "created or updated" use record-after-write (one flow, both events, #3427). For any other combination, author one flow per event \u2014 multi-event arrays are deferred (#3457).`
|
|
1263
1363
|
});
|
|
1264
1364
|
}
|
|
1365
|
+
if (start && config.timeRelative != null && typeof config.timeRelative !== "object") {
|
|
1366
|
+
const fallback = isRecordTriggered2 || isArrayRecordTriggered ? "its record-change trigger" : config.schedule != null || flow.type === "schedule" ? "its plain `config.schedule` cadence" : triggerType === "api" || flow.type === "api" ? "its api trigger" : void 0;
|
|
1367
|
+
const consequence2 = fallback ? `The flow still binds through ${fallback}, so the descriptor is silently DROPPED \u2014 it fires on that trigger's terms (once per firing, with no record on the context) instead of once per matching record, and nothing anywhere reports the difference.` : `Nothing else on this start node declares a trigger either, so the flow binds to NOTHING and never fires \u2014 with zero diagnostics at any layer, not even the one bind-time warn a descriptor that IS an object gets when the trigger refuses it.`;
|
|
1368
|
+
findings.push({
|
|
1369
|
+
// `error` (#5762). The criterion IS the engine's routing predicate, so a
|
|
1370
|
+
// value that fails it is not routed to the time-relative trigger by any
|
|
1371
|
+
// deployment — the strongest verdict in this file, and the one case with
|
|
1372
|
+
// no runtime channel to fall back on (not even the bind-time warn 1b-ii
|
|
1373
|
+
// moves earlier). Note the two consequences below are both defects: one
|
|
1374
|
+
// never fires, the other silently drops the descriptor. Neither is a
|
|
1375
|
+
// shape the author can have meant, so both gate.
|
|
1376
|
+
severity: "error",
|
|
1377
|
+
rule: FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
1378
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1379
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.timeRelative`,
|
|
1380
|
+
message: `has config.timeRelative = ${renderNonObject(config.timeRelative)}, which is not the descriptor OBJECT this slot takes \u2014 the engine routes a flow to the time-relative sweep only when config.timeRelative is an object, so this one is never routed there and the sweep is never installed. ${consequence2}`,
|
|
1381
|
+
hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1265
1384
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
1266
1385
|
findings.push({
|
|
1267
1386
|
severity: "warning",
|
|
@@ -1277,7 +1396,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1277
1396
|
}
|
|
1278
1397
|
|
|
1279
1398
|
// src/flow-walk.ts
|
|
1280
|
-
var
|
|
1399
|
+
var import_automation3 = require("@objectstack/spec/automation");
|
|
1281
1400
|
function isRec2(v) {
|
|
1282
1401
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1283
1402
|
}
|
|
@@ -1285,9 +1404,9 @@ function strName(v) {
|
|
|
1285
1404
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1286
1405
|
}
|
|
1287
1406
|
var REGION_SLOTS = new Map(
|
|
1288
|
-
[...
|
|
1407
|
+
[...import_automation3.FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
|
|
1289
1408
|
);
|
|
1290
|
-
var REGION_CONFIG_KEYS =
|
|
1409
|
+
var REGION_CONFIG_KEYS = import_automation3.FLOW_REGION_CONFIG_KEYS;
|
|
1291
1410
|
var MAX_REGION_DEPTH = 16;
|
|
1292
1411
|
function flowNodeLabel(node, index) {
|
|
1293
1412
|
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
@@ -2287,13 +2406,13 @@ function validateSearchableFields(stack) {
|
|
|
2287
2406
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2288
2407
|
const view = views[vi];
|
|
2289
2408
|
if (!isRec3(view)) continue;
|
|
2290
|
-
const
|
|
2409
|
+
const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
2291
2410
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
2292
2411
|
if (isRec3(view.list)) {
|
|
2293
2412
|
check(
|
|
2294
2413
|
view.list.searchableFields,
|
|
2295
2414
|
listViewObject(view.list) ?? viewObject,
|
|
2296
|
-
`view "${
|
|
2415
|
+
`view "${viewLabel2}" \u203A list`,
|
|
2297
2416
|
`views[${vi}].list.searchableFields`,
|
|
2298
2417
|
"list-view searchableFields",
|
|
2299
2418
|
"narrowing"
|
|
@@ -2305,7 +2424,7 @@ function validateSearchableFields(stack) {
|
|
|
2305
2424
|
check(
|
|
2306
2425
|
lv.searchableFields,
|
|
2307
2426
|
listViewObject(lv) ?? viewObject,
|
|
2308
|
-
`view "${
|
|
2427
|
+
`view "${viewLabel2}" \u203A listViews.${key}`,
|
|
2309
2428
|
`views[${vi}].listViews.${key}.searchableFields`,
|
|
2310
2429
|
"list-view searchableFields",
|
|
2311
2430
|
"narrowing"
|
|
@@ -2429,8 +2548,11 @@ function sortFieldRefs(value, basePath) {
|
|
|
2429
2548
|
}
|
|
2430
2549
|
var COMPONENT_FIELD_SPECS = {
|
|
2431
2550
|
"record:highlights": { props: ["fields"] },
|
|
2432
|
-
// `sections
|
|
2433
|
-
//
|
|
2551
|
+
// `sections` (object form) and `hideFields` are what every real page authors,
|
|
2552
|
+
// and since #5611 they are what `RecordDetailsProps` declares — this model and
|
|
2553
|
+
// the spec agree. (Before that, `sections` was declared as an ID `string[]`
|
|
2554
|
+
// and `hideFields` not at all; both survived only because `properties` is
|
|
2555
|
+
// unvalidated.)
|
|
2434
2556
|
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
2435
2557
|
"record:path": { props: ["statusField"] },
|
|
2436
2558
|
"element:number": { props: ["field"] },
|
|
@@ -2493,7 +2615,7 @@ function indexObjectFields(stack) {
|
|
|
2493
2615
|
}
|
|
2494
2616
|
return objectFields;
|
|
2495
2617
|
}
|
|
2496
|
-
function checkFieldRefs(refs, objectName, objectFields, where,
|
|
2618
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2497
2619
|
const findings = [];
|
|
2498
2620
|
if (!objectName) return findings;
|
|
2499
2621
|
const known = objectFields.get(objectName);
|
|
@@ -2502,11 +2624,11 @@ function checkFieldRefs(refs, objectName, objectFields, where, consequence = "sk
|
|
|
2502
2624
|
if (ref.name.includes(".")) continue;
|
|
2503
2625
|
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
|
|
2504
2626
|
findings.push({
|
|
2505
|
-
severity:
|
|
2627
|
+
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2506
2628
|
rule: PAGE_FIELD_UNKNOWN,
|
|
2507
2629
|
where,
|
|
2508
2630
|
path: ref.path,
|
|
2509
|
-
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (
|
|
2631
|
+
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence2 === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
|
|
2510
2632
|
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2511
2633
|
});
|
|
2512
2634
|
}
|
|
@@ -2560,6 +2682,42 @@ function validatePageFieldBindings(stack) {
|
|
|
2560
2682
|
return findings;
|
|
2561
2683
|
}
|
|
2562
2684
|
|
|
2685
|
+
// src/zod-issue-format.ts
|
|
2686
|
+
var isRec6 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2687
|
+
var valueAtPath = (root, path) => {
|
|
2688
|
+
let cur = root;
|
|
2689
|
+
for (const key of path) {
|
|
2690
|
+
if (!isRec6(cur) && !Array.isArray(cur)) return void 0;
|
|
2691
|
+
cur = cur[key];
|
|
2692
|
+
}
|
|
2693
|
+
return cur;
|
|
2694
|
+
};
|
|
2695
|
+
var preview = (value) => {
|
|
2696
|
+
let text;
|
|
2697
|
+
try {
|
|
2698
|
+
text = JSON.stringify(value) ?? String(value);
|
|
2699
|
+
} catch {
|
|
2700
|
+
text = String(value);
|
|
2701
|
+
}
|
|
2702
|
+
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
2703
|
+
};
|
|
2704
|
+
function describeIssue(issue, root, depth = 0) {
|
|
2705
|
+
const value = depth === 0 ? valueAtPath(root, issue.path) : void 0;
|
|
2706
|
+
const seen = depth > 0 || issue.code === "custom" || issue.message.includes("received ") ? "" : value === void 0 ? " (nothing is set there)" : ` (received ${preview(value)})`;
|
|
2707
|
+
const armIssues = issue.code === "invalid_union" ? issue.errors : void 0;
|
|
2708
|
+
if (!armIssues || armIssues.length === 0) {
|
|
2709
|
+
return `${issue.message}${seen}`;
|
|
2710
|
+
}
|
|
2711
|
+
const arms = armIssues.map(
|
|
2712
|
+
(arm) => arm.map((inner) => {
|
|
2713
|
+
const where = inner.path.length ? `${inner.path.join(".")} \u2014 ` : "";
|
|
2714
|
+
return `${where}${describeIssue(inner, root, depth + 1)}`;
|
|
2715
|
+
}).join("; ")
|
|
2716
|
+
).filter((text) => text.length > 0);
|
|
2717
|
+
if (arms.length === 0) return `${issue.message}${seen}`;
|
|
2718
|
+
return `${issue.message}${seen} \u2014 no accepted form matched: ` + arms.map((text, i) => `(${i + 1}) ${text}`).join(" ");
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2563
2721
|
// src/validate-react-page-props.ts
|
|
2564
2722
|
var import_meta2 = {};
|
|
2565
2723
|
var cachedTs = null;
|
|
@@ -2665,35 +2823,78 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
2665
2823
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
2666
2824
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2667
2825
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2668
|
-
var
|
|
2669
|
-
|
|
2826
|
+
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
2827
|
+
function checkChartDrillDown(raw, push2) {
|
|
2828
|
+
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
2829
|
+
if (!isRec7(raw)) {
|
|
2830
|
+
push2(
|
|
2831
|
+
"error",
|
|
2832
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
2833
|
+
`drillDown must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
|
|
2834
|
+
"Write drillDown={{ \u2026 }} \u2014 or, to turn the drill on with all defaults, drillDown={{}}. Omit the prop entirely to leave drill off."
|
|
2835
|
+
);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
const parsed = import_ui2.ChartDrillDownSchema.safeParse(raw);
|
|
2839
|
+
if (parsed.success) return;
|
|
2840
|
+
for (const issue of parsed.error.issues) {
|
|
2841
|
+
const at = issue.path.length ? `drillDown.${issue.path.join(".")}` : "drillDown";
|
|
2842
|
+
push2(
|
|
2843
|
+
"error",
|
|
2844
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
2845
|
+
`${at}: ${issue.message}`,
|
|
2846
|
+
"The drill config is declared by ChartDrillDownSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
function checkChartAggregate(raw, push2) {
|
|
2851
|
+
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
2852
|
+
if (!isRec7(raw)) {
|
|
2853
|
+
push2(
|
|
2854
|
+
"error",
|
|
2855
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2856
|
+
`aggregate must be a configuration object, not ${Array.isArray(raw) ? "an array" : typeof raw}.`,
|
|
2857
|
+
'Write aggregate={{ function: "count", groupBy: "<field>" }} \u2014 or bind data={\u2026} instead to chart precomputed rows.'
|
|
2858
|
+
);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
const groupByAbsent = raw.groupBy === void 0;
|
|
2862
|
+
if (groupByAbsent) {
|
|
2863
|
+
push2(
|
|
2864
|
+
"warning",
|
|
2865
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2866
|
+
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
2867
|
+
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. Deliberate single-value charts are tolerated at warning level for now: ChartAggregateSchema declares groupBy required while ObjectChart honours its absence by falling back to xAxisKey \u2014 objectstack#5583 decides which of the two moves."
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
|
|
2871
|
+
if (parsed.success) return;
|
|
2872
|
+
for (const issue of parsed.error.issues) {
|
|
2873
|
+
if (groupByAbsent && issue.path[0] === "groupBy") continue;
|
|
2874
|
+
const at = issue.path.length ? `aggregate.${issue.path.join(".")}` : "aggregate";
|
|
2875
|
+
push2(
|
|
2876
|
+
"error",
|
|
2877
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2878
|
+
`${at}: ${describeIssue(issue, raw)}`,
|
|
2879
|
+
"The aggregate is declared by ChartAggregateSchema (@objectstack/spec/ui) \u2014 the rejection above carries the fix."
|
|
2880
|
+
);
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2670
2884
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2671
2885
|
function checkObjectChart(attrs, objectFields, findings) {
|
|
2672
2886
|
const { values, where, path } = attrs;
|
|
2673
2887
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2888
|
+
checkChartDrillDown(values.get("drillDown"), push2);
|
|
2674
2889
|
if (values.has("data")) return;
|
|
2675
2890
|
const aggregate = values.get("aggregate");
|
|
2891
|
+
checkChartAggregate(aggregate, push2);
|
|
2676
2892
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2677
|
-
if (!
|
|
2893
|
+
if (!isRec7(aggregate)) return;
|
|
2678
2894
|
const fn = strOf(aggregate.function);
|
|
2679
2895
|
const field = strOf(aggregate.field);
|
|
2680
2896
|
const groupBy = aggregate.groupBy;
|
|
2681
|
-
const groupByField = strOf(groupBy) ?? (
|
|
2682
|
-
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2683
|
-
push2(
|
|
2684
|
-
"error",
|
|
2685
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
2686
|
-
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2687
|
-
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2688
|
-
);
|
|
2689
|
-
} else if (fn && fn !== "count" && !field) {
|
|
2690
|
-
push2(
|
|
2691
|
-
"error",
|
|
2692
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
2693
|
-
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
2694
|
-
'Add aggregate.field, or use function "count" (the only one that may omit it).'
|
|
2695
|
-
);
|
|
2696
|
-
}
|
|
2897
|
+
const groupByField = strOf(groupBy) ?? (isRec7(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2697
2898
|
const objectName = strOf(values.get("objectName"));
|
|
2698
2899
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
2699
2900
|
if (objectName && known) {
|
|
@@ -2726,18 +2927,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
2726
2927
|
);
|
|
2727
2928
|
};
|
|
2728
2929
|
const xAxisRaw = values.get("xAxis");
|
|
2729
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
2930
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec7(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
2730
2931
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
2731
2932
|
axisRef(categoryAxis, categoryProp);
|
|
2732
2933
|
const yAxisRaw = values.get("yAxis");
|
|
2733
2934
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
2734
2935
|
for (const a of yAxisList) {
|
|
2735
|
-
axisRef(strOf(a) ?? (
|
|
2936
|
+
axisRef(strOf(a) ?? (isRec7(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
2736
2937
|
}
|
|
2737
2938
|
const series = values.get("series");
|
|
2738
2939
|
if (Array.isArray(series)) {
|
|
2739
2940
|
for (const s of series) {
|
|
2740
|
-
if (!
|
|
2941
|
+
if (!isRec7(s)) continue;
|
|
2741
2942
|
const dataKey = strOf(s.dataKey);
|
|
2742
2943
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
2743
2944
|
}
|
|
@@ -2793,7 +2994,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
2793
2994
|
if (!Array.isArray(value)) return { child, parent };
|
|
2794
2995
|
for (let i = 0; i < value.length; i++) {
|
|
2795
2996
|
const sub = value[i];
|
|
2796
|
-
if (!
|
|
2997
|
+
if (!isRec7(sub)) continue;
|
|
2797
2998
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2798
2999
|
child.push({
|
|
2799
3000
|
objectName: strOf(sub.childObject),
|
|
@@ -2838,20 +3039,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
2838
3039
|
}
|
|
2839
3040
|
for (const key of spec.nestedFields ?? []) {
|
|
2840
3041
|
const v = readable(key);
|
|
2841
|
-
if (
|
|
3042
|
+
if (isRec7(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2842
3043
|
}
|
|
2843
3044
|
for (const key of spec.sections ?? []) {
|
|
2844
3045
|
const v = readable(key);
|
|
2845
3046
|
if (!Array.isArray(v)) continue;
|
|
2846
3047
|
for (let i = 0; i < v.length; i++) {
|
|
2847
3048
|
const section = v[i];
|
|
2848
|
-
if (!
|
|
3049
|
+
if (!isRec7(section)) continue;
|
|
2849
3050
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2850
3051
|
}
|
|
2851
3052
|
}
|
|
2852
3053
|
for (const key of spec.keyedByField ?? []) {
|
|
2853
3054
|
const v = readable(key);
|
|
2854
|
-
if (!
|
|
3055
|
+
if (!isRec7(v)) continue;
|
|
2855
3056
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2856
3057
|
}
|
|
2857
3058
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -3122,7 +3323,7 @@ function validateSemanticRoles(stack) {
|
|
|
3122
3323
|
const where = `object "${objName}"`;
|
|
3123
3324
|
const path = `objects[${i}]`;
|
|
3124
3325
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
3125
|
-
const fieldNames = new Set(Object.keys(fields));
|
|
3326
|
+
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
3126
3327
|
const declaredGroups = new Set(
|
|
3127
3328
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
3128
3329
|
);
|
|
@@ -3505,9 +3706,9 @@ function validateCapabilityReferences(stack) {
|
|
|
3505
3706
|
}
|
|
3506
3707
|
|
|
3507
3708
|
// src/validate-approval-approvers.ts
|
|
3508
|
-
var
|
|
3709
|
+
var import_automation4 = require("@objectstack/spec/automation");
|
|
3509
3710
|
var import_spec = require("@objectstack/spec");
|
|
3510
|
-
var
|
|
3711
|
+
var import_formula3 = require("@objectstack/formula");
|
|
3511
3712
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
3512
3713
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
3513
3714
|
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
@@ -3538,7 +3739,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3538
3739
|
const findings = [];
|
|
3539
3740
|
if (!stack || typeof stack !== "object") return findings;
|
|
3540
3741
|
const flows = asArray19(stack.flows);
|
|
3541
|
-
const validTypes = new Set(
|
|
3742
|
+
const validTypes = new Set(import_automation4.ApproverType.options);
|
|
3542
3743
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
3543
3744
|
const flow = flows[fi];
|
|
3544
3745
|
if (!flow || typeof flow !== "object") continue;
|
|
@@ -3546,7 +3747,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3546
3747
|
const walked = walkFlowNodes(flow, `flows[${fi}]`);
|
|
3547
3748
|
for (let ni = 0; ni < walked.length; ni++) {
|
|
3548
3749
|
const { node, path: nodePath } = walked[ni];
|
|
3549
|
-
if (!node || node.type !==
|
|
3750
|
+
if (!node || node.type !== import_automation4.APPROVAL_NODE_TYPE) continue;
|
|
3550
3751
|
const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
|
|
3551
3752
|
const cfg = node.config ?? {};
|
|
3552
3753
|
const approvers = Array.isArray(cfg.approvers) ? cfg.approvers : [];
|
|
@@ -3564,12 +3765,12 @@ function validateApprovalApprovers(stack) {
|
|
|
3564
3765
|
rule: APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
3565
3766
|
where,
|
|
3566
3767
|
path: `${path}.type`,
|
|
3567
|
-
message: `approver type '${type}' is not an ApproverType (${
|
|
3768
|
+
message: `approver type '${type}' is not an ApproverType (${import_automation4.ApproverType.options.join(" | ")}).`,
|
|
3568
3769
|
hint: fix ? `Use the spec value: { type: '${fix}', value: '${value}' }.` : `Pick one of the spec values; unmapped types degrade to an inert '${type}:${value}' literal at runtime.`
|
|
3569
3770
|
});
|
|
3570
3771
|
continue;
|
|
3571
3772
|
}
|
|
3572
|
-
const canonical = (0,
|
|
3773
|
+
const canonical = (0, import_automation4.canonicalApproverType)(type);
|
|
3573
3774
|
if (canonical === "expression") {
|
|
3574
3775
|
const source = value.trim();
|
|
3575
3776
|
if (!source) {
|
|
@@ -3582,7 +3783,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3582
3783
|
hint: `Write a CEL expression over current.* (the record's live state at node entry), trigger.* (the submit-time snapshot) or vars.* (flow variables), e.g. current.approvers_dynamic or vars.approval_lead.picked_departments.`
|
|
3583
3784
|
});
|
|
3584
3785
|
} else {
|
|
3585
|
-
const parsed = (0,
|
|
3786
|
+
const parsed = (0, import_formula3.collectCelRootIdentifiers)(source);
|
|
3586
3787
|
if (!parsed.ok) {
|
|
3587
3788
|
findings.push({
|
|
3588
3789
|
severity: "error",
|
|
@@ -3626,8 +3827,8 @@ function validateApprovalApprovers(stack) {
|
|
|
3626
3827
|
message: `approver { type: '${type}', value: '${value}' } resolves against the better-auth org-membership tier (sys_member.role: ${MEMBERSHIP_TIER_LIST}) \u2014 '${value}' is not a membership tier, so this approver matches nobody and the request stalls.`,
|
|
3627
3828
|
hint: `If '${value}' is an org position, author { type: 'position', value: '${value}' } (resolved via sys_user_position, ADR-0090 D3). Keep type 'org_membership_level' only for membership tiers (${MEMBERSHIP_TIER_LIST}) \u2014 the vocabulary is closed (ADR-0108), so a business role is always a position.`
|
|
3628
3829
|
});
|
|
3629
|
-
} else if (type in
|
|
3630
|
-
const fix = (0,
|
|
3830
|
+
} else if (type in import_automation4.DEPRECATED_APPROVER_TYPES) {
|
|
3831
|
+
const fix = (0, import_automation4.canonicalApproverType)(type);
|
|
3631
3832
|
findings.push({
|
|
3632
3833
|
severity: "warning",
|
|
3633
3834
|
rule: APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
@@ -3636,7 +3837,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3636
3837
|
message: `approver type '${type}' is the deprecated spelling of '${fix}' (ADR-0090 D3) and is removed in the next major.`,
|
|
3637
3838
|
hint: `Author { type: '${fix}', value: '${value}' }. It resolves identically today.`
|
|
3638
3839
|
});
|
|
3639
|
-
} else if (
|
|
3840
|
+
} else if (import_automation4.APPROVER_VALUE_BINDINGS[canonical]?.source === "unsupported") {
|
|
3640
3841
|
findings.push({
|
|
3641
3842
|
severity: "warning",
|
|
3642
3843
|
rule: APPROVAL_APPROVER_TYPE_UNSUPPORTED,
|
|
@@ -3647,7 +3848,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3647
3848
|
});
|
|
3648
3849
|
}
|
|
3649
3850
|
const declaredOrg = a.organization;
|
|
3650
|
-
if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" &&
|
|
3851
|
+
if (typeof declaredOrg === "string" && declaredOrg.trim() !== "" && import_automation4.ApproverType.options.includes(canonical) && !(0, import_automation4.approverTypeIsOrgScoped)(canonical)) {
|
|
3651
3852
|
findings.push({
|
|
3652
3853
|
severity: "error",
|
|
3653
3854
|
rule: APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED,
|
|
@@ -3661,7 +3862,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3661
3862
|
const routable = approvers.filter(
|
|
3662
3863
|
(a) => a && typeof a === "object" && typeof a.type === "string"
|
|
3663
3864
|
);
|
|
3664
|
-
if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0,
|
|
3865
|
+
if (routable.length > 0 && routable.every((a) => GROUP_ROUTED_TYPES.has((0, import_automation4.canonicalApproverType)(String(a.type))))) {
|
|
3665
3866
|
const locks = cfg.lockRecord !== false;
|
|
3666
3867
|
findings.push({
|
|
3667
3868
|
severity: "info",
|
|
@@ -3673,7 +3874,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3673
3874
|
});
|
|
3674
3875
|
}
|
|
3675
3876
|
const hasExpression = approvers.some(
|
|
3676
|
-
(a) => a && typeof a === "object" && (0,
|
|
3877
|
+
(a) => a && typeof a === "object" && (0, import_automation4.canonicalApproverType)(String(a.type ?? "")) === "expression"
|
|
3677
3878
|
);
|
|
3678
3879
|
if (hasExpression && cfg.onEmptyApprovers == null) {
|
|
3679
3880
|
findings.push({
|
|
@@ -3685,7 +3886,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3685
3886
|
hint: `Declare the empty-slate policy explicitly: onEmptyApprovers: 'admin_rescue' (hold for admin takeover), 'fail' (fail the node \u2014 config bug), or 'auto_approve' (wave through, output.autoApproved = true).`
|
|
3686
3887
|
});
|
|
3687
3888
|
}
|
|
3688
|
-
const declaredOutputs = (0,
|
|
3889
|
+
const declaredOutputs = (0, import_automation4.normalizeDecisionOutputs)(cfg.decisionOutputs).map((d) => d.key);
|
|
3689
3890
|
const reserved = declaredOutputs.filter((k) => RESERVED_OUTPUT_KEYS.has(k));
|
|
3690
3891
|
if (reserved.length) {
|
|
3691
3892
|
findings.push({
|
|
@@ -3840,7 +4041,7 @@ function asArray20(v) {
|
|
|
3840
4041
|
return [];
|
|
3841
4042
|
}
|
|
3842
4043
|
function owdOf(obj) {
|
|
3843
|
-
return obj.sharingModel
|
|
4044
|
+
return obj.sharingModel;
|
|
3844
4045
|
}
|
|
3845
4046
|
function isSystemObject(obj) {
|
|
3846
4047
|
return obj.isSystem === true || String(obj.name ?? "").startsWith("sys_");
|
|
@@ -3854,7 +4055,7 @@ function labelHasRoleWord(label2) {
|
|
|
3854
4055
|
return /\brole(s)?\b/i.test(label2);
|
|
3855
4056
|
}
|
|
3856
4057
|
function refOf(def) {
|
|
3857
|
-
const r = def.reference
|
|
4058
|
+
const r = def.reference;
|
|
3858
4059
|
return typeof r === "string" && r ? r : void 0;
|
|
3859
4060
|
}
|
|
3860
4061
|
function firstMasterDetailField(obj) {
|
|
@@ -4147,6 +4348,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4147
4348
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
4148
4349
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
4149
4350
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
4351
|
+
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
4150
4352
|
function asArray21(v) {
|
|
4151
4353
|
if (Array.isArray(v)) return v;
|
|
4152
4354
|
if (v && typeof v === "object") {
|
|
@@ -4157,6 +4359,15 @@ function asArray21(v) {
|
|
|
4157
4359
|
function str(v) {
|
|
4158
4360
|
return typeof v === "string" ? v : "";
|
|
4159
4361
|
}
|
|
4362
|
+
function expressionText(v) {
|
|
4363
|
+
if (typeof v === "string") return v;
|
|
4364
|
+
if (v && typeof v === "object") {
|
|
4365
|
+
const rec = v;
|
|
4366
|
+
if (typeof rec.source === "string") return rec.source;
|
|
4367
|
+
if (rec.ast !== void 0) return JSON.stringify(rec.ast) ?? "";
|
|
4368
|
+
}
|
|
4369
|
+
return "";
|
|
4370
|
+
}
|
|
4160
4371
|
function isTenancyDisabled(object) {
|
|
4161
4372
|
const tenancy = object.tenancy;
|
|
4162
4373
|
if (tenancy && typeof tenancy === "object" && tenancy.enabled === false) return true;
|
|
@@ -4168,7 +4379,7 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
4168
4379
|
function validateOrgAxisRedLines(stack) {
|
|
4169
4380
|
const findings = [];
|
|
4170
4381
|
const cfg = stack ?? {};
|
|
4171
|
-
const permissionSets = asArray21(cfg.permissions
|
|
4382
|
+
const permissionSets = asArray21(cfg.permissions);
|
|
4172
4383
|
permissionSets.forEach((ps, psIndex) => {
|
|
4173
4384
|
asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4174
4385
|
for (const clause of ["using", "check"]) {
|
|
@@ -4184,53 +4395,167 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4184
4395
|
}
|
|
4185
4396
|
});
|
|
4186
4397
|
});
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
severity: "error",
|
|
4195
|
-
rule: ORG_AXIS_PERMISSION_INHERITANCE,
|
|
4196
|
-
where: `object "${objectName}" policy "${str(policy.name) || pIndex}"`,
|
|
4197
|
-
path: `objects[${oIndex}].rowLevelSecurity[${pIndex}].${clause}`,
|
|
4198
|
-
message: `RLS ${clause} reads \`${ORG_PARENT_FIELD}\`, which builds a permission hierarchy along the organization axis. ADR-0105 D6 forbids it: the org tree is a REPORTING dimension only.`,
|
|
4199
|
-
hint: INHERITANCE_HINT
|
|
4200
|
-
});
|
|
4201
|
-
}
|
|
4202
|
-
});
|
|
4203
|
-
});
|
|
4204
|
-
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
4205
|
-
const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
|
|
4206
|
-
const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
|
|
4207
|
-
if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
|
|
4398
|
+
asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4399
|
+
const slots = [
|
|
4400
|
+
{ key: "condition", text: expressionText(rule.condition) },
|
|
4401
|
+
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
4402
|
+
];
|
|
4403
|
+
for (const slot of slots) {
|
|
4404
|
+
if (!slot.text.includes(ORG_PARENT_FIELD)) continue;
|
|
4208
4405
|
findings.push({
|
|
4209
4406
|
severity: "error",
|
|
4210
4407
|
rule: ORG_AXIS_PERMISSION_INHERITANCE,
|
|
4211
4408
|
where: `sharing rule "${str(rule.name) || rIndex}"`,
|
|
4212
|
-
path: `sharingRules[${rIndex}]`,
|
|
4213
|
-
message: `Sharing rule reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
|
|
4409
|
+
path: `sharingRules[${rIndex}].${slot.key}`,
|
|
4410
|
+
message: `Sharing rule ${slot.key} reads \`${ORG_PARENT_FIELD}\`, granting access by walking the organization tree. ADR-0105 D6 forbids permission inheritance along the org axis.`,
|
|
4214
4411
|
hint: INHERITANCE_HINT
|
|
4215
4412
|
});
|
|
4216
4413
|
}
|
|
4217
4414
|
});
|
|
4218
4415
|
const tenancyDisabledObjects = new Set(
|
|
4219
|
-
objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4416
|
+
asArray21(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4220
4417
|
);
|
|
4221
|
-
asArray21(cfg.sharingRules
|
|
4222
|
-
const target = str(rule.object
|
|
4418
|
+
asArray21(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4419
|
+
const target = str(rule.object);
|
|
4223
4420
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
4224
|
-
const
|
|
4225
|
-
const recipientType = str(
|
|
4226
|
-
if (recipientType
|
|
4421
|
+
const sharedWith = rule.sharedWith;
|
|
4422
|
+
const recipientType = str(sharedWith?.type);
|
|
4423
|
+
if (!BU_TREE_RECIPIENT_TYPES.has(recipientType)) return;
|
|
4424
|
+
const reach = recipientType === "unit_and_subordinates" ? "a business unit AND every descendant unit" : "a business unit";
|
|
4227
4425
|
findings.push({
|
|
4228
4426
|
severity: "error",
|
|
4229
4427
|
rule: ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
4230
4428
|
where: `sharing rule "${str(rule.name) || rIndex}" on object "${target}"`,
|
|
4231
|
-
path: `sharingRules[${rIndex}].
|
|
4232
|
-
message: `
|
|
4233
|
-
hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a
|
|
4429
|
+
path: `sharingRules[${rIndex}].sharedWith`,
|
|
4430
|
+
message: `Sharing rule recipient \`${recipientType}\` (${reach}) targets "${target}", which opted out of tenancy (\`tenancy.enabled: false\`). Platform-global objects carry no organization column, so this grant spans EVERY organization \u2014 a cross-organization business-unit grant, which ADR-0105 D6 forbids (BU trees are org-internal).`,
|
|
4431
|
+
hint: `Either scope the object to organizations (drop \`tenancy.enabled: false\` so Layer 0 walls it), or share it to a \`user\` / \`team\` / \`position\` audience instead \u2014 those expand flat, with no business-unit tree to resolve. A platform-global catalog that everyone should read wants an OWD of \`public_read\`, not a BU grant.`
|
|
4432
|
+
});
|
|
4433
|
+
});
|
|
4434
|
+
return findings;
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
// src/validate-sharing-rule-enforceability.ts
|
|
4438
|
+
var import_formula4 = require("@objectstack/formula");
|
|
4439
|
+
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
4440
|
+
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
4441
|
+
function asArray22(v) {
|
|
4442
|
+
if (Array.isArray(v)) return v;
|
|
4443
|
+
if (v && typeof v === "object") {
|
|
4444
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4445
|
+
}
|
|
4446
|
+
return [];
|
|
4447
|
+
}
|
|
4448
|
+
function str2(v) {
|
|
4449
|
+
return typeof v === "string" ? v : "";
|
|
4450
|
+
}
|
|
4451
|
+
function toCompilerInput(condition) {
|
|
4452
|
+
if (typeof condition === "string") return condition.trim() ? condition : null;
|
|
4453
|
+
if (condition && typeof condition === "object") {
|
|
4454
|
+
const source = condition.source;
|
|
4455
|
+
if (typeof source === "string" && source.trim()) return { source };
|
|
4456
|
+
}
|
|
4457
|
+
return null;
|
|
4458
|
+
}
|
|
4459
|
+
function sourceOf(condition) {
|
|
4460
|
+
const input = toCompilerInput(condition);
|
|
4461
|
+
if (typeof input === "string") return input;
|
|
4462
|
+
return str2(input?.source);
|
|
4463
|
+
}
|
|
4464
|
+
var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column `record.<field>` paths (ADR-0058 D2).";
|
|
4465
|
+
function validateSharingRuleEnforceability(stack) {
|
|
4466
|
+
const findings = [];
|
|
4467
|
+
const cfg = stack ?? {};
|
|
4468
|
+
asArray22(cfg.sharingRules).forEach((rule, index) => {
|
|
4469
|
+
const input = toCompilerInput(rule.condition);
|
|
4470
|
+
if (input === null) return;
|
|
4471
|
+
const result = (0, import_formula4.compileCelToFilter)(input, { variables: {} });
|
|
4472
|
+
if (result.ok) return;
|
|
4473
|
+
if (result.reason === "parse-error") return;
|
|
4474
|
+
const name = str2(rule.name) || String(index);
|
|
4475
|
+
const object = str2(rule.object);
|
|
4476
|
+
const where = `sharing rule "${name}"${object ? ` on object "${object}"` : ""}`;
|
|
4477
|
+
const path = `sharingRules[${index}].condition`;
|
|
4478
|
+
const source = sourceOf(rule.condition);
|
|
4479
|
+
const skipped = "so `bootstrapDeclaredSharingRules` SKIPS the rule at boot: it is never written to `sys_sharing_rule`, no `sys_record_share` grant is ever materialised, and the only signal is one WARN line in the boot log. The rule is declared and grants nothing (ADR-0049: an unlowerable condition is never seeded as a permissive match-all).";
|
|
4480
|
+
if (result.reason === "unresolved-variable") {
|
|
4481
|
+
findings.push({
|
|
4482
|
+
severity: "error",
|
|
4483
|
+
rule: SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
4484
|
+
where,
|
|
4485
|
+
path,
|
|
4486
|
+
message: `Sharing-rule condition \`${source}\` reads a runtime variable (${result.detail}), ` + skipped,
|
|
4487
|
+
hint: "A criteria sharing rule is MATERIALISED: the seeder compiles ONE static `criteria_json` per rule and the evaluator writes `sys_record_share` rows from it, so there is no \"current user\" for the condition to read. Express per-user access with the mechanism that runs per request instead \u2014 an RLS policy on a permission set (`rowLevelSecurity[].using`, where `current_user.*` IS resolved), or the record-ownership path. Keep this rule for the part of the predicate that is a property of the RECORD (e.g. `record.stage == 'closed_won'`) and name the audience through `sharedWith`."
|
|
4488
|
+
});
|
|
4489
|
+
return;
|
|
4490
|
+
}
|
|
4491
|
+
findings.push({
|
|
4492
|
+
severity: "error",
|
|
4493
|
+
rule: SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
4494
|
+
where,
|
|
4495
|
+
path,
|
|
4496
|
+
message: `Sharing-rule condition \`${source}\` is outside the pushdown subset the runtime can compile (${result.detail}), ` + skipped,
|
|
4497
|
+
hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET + " Two traps in particular: (1) `has(record.x)` is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the condition is COMPILED \u2014 write the null test as `record.x != null`; (2) a related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column, or share the related object instead."
|
|
4498
|
+
});
|
|
4499
|
+
});
|
|
4500
|
+
return findings;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
// src/validate-rls-predicate-enforceability.ts
|
|
4504
|
+
var import_formula5 = require("@objectstack/formula");
|
|
4505
|
+
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
4506
|
+
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
4507
|
+
function asArray23(v) {
|
|
4508
|
+
if (Array.isArray(v)) return v;
|
|
4509
|
+
if (v && typeof v === "object") {
|
|
4510
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4511
|
+
}
|
|
4512
|
+
return [];
|
|
4513
|
+
}
|
|
4514
|
+
function str3(v) {
|
|
4515
|
+
return typeof v === "string" ? v : "";
|
|
4516
|
+
}
|
|
4517
|
+
var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
|
|
4518
|
+
function consequence(clause) {
|
|
4519
|
+
const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
|
|
4520
|
+
return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
|
|
4521
|
+
}
|
|
4522
|
+
function validateRlsPredicateEnforceability(stack) {
|
|
4523
|
+
const findings = [];
|
|
4524
|
+
const cfg = stack ?? {};
|
|
4525
|
+
asArray23(cfg.permissions).forEach((ps, psIndex) => {
|
|
4526
|
+
asArray23(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4527
|
+
for (const clause of ["using", "check"]) {
|
|
4528
|
+
const source = str3(policy[clause]);
|
|
4529
|
+
if (!source.trim()) continue;
|
|
4530
|
+
if ((0, import_formula5.isSupportedRlsExpression)(source)) continue;
|
|
4531
|
+
const why = (0, import_formula5.isPushdownableCel)((0, import_formula5.sqlPredicateToCel)(source));
|
|
4532
|
+
const detail = why.ok ? "" : why.detail;
|
|
4533
|
+
const parseError = !why.ok && why.reason === "parse-error";
|
|
4534
|
+
const psName = str3(ps.name) || String(psIndex);
|
|
4535
|
+
const policyName = str3(policy.name) || String(pIndex);
|
|
4536
|
+
const object = str3(policy.object);
|
|
4537
|
+
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
4538
|
+
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
4539
|
+
if (parseError) {
|
|
4540
|
+
findings.push({
|
|
4541
|
+
severity: "error",
|
|
4542
|
+
rule: RLS_PREDICATE_UNPARSEABLE,
|
|
4543
|
+
where,
|
|
4544
|
+
path,
|
|
4545
|
+
message: `RLS ${clause} \`${source}\` does not parse as CEL even after the legacy SQL bridge (\`=\` \u2192 \`==\`, \`IN\` \u2192 \`in\`) has been applied (${detail}), ` + consequence(clause),
|
|
4546
|
+
hint: "Author the predicate in canonical CEL (ADR-0058 D1). The bridge covers only the historic SQL subset \u2014 a bare `=` and `IN` \u2014 so everything else must already be CEL: combine with `&&` / `||` rather than SQL `AND` / `OR`, negate with `!`, and use `startsWith` / `endsWith` / `contains` rather than `LIKE`. A subquery has no CEL spelling at all: RLS cannot join (ADR-0055), so pre-resolve the set into a membership key the runtime exposes (`field in current_user.<key>`, ADR-0105 D11) or denormalise the value onto this object."
|
|
4547
|
+
});
|
|
4548
|
+
continue;
|
|
4549
|
+
}
|
|
4550
|
+
findings.push({
|
|
4551
|
+
severity: "error",
|
|
4552
|
+
rule: RLS_PREDICATE_UNENFORCEABLE,
|
|
4553
|
+
where,
|
|
4554
|
+
path,
|
|
4555
|
+
message: `RLS ${clause} \`${source}\` is outside the pushdown subset the runtime can compile (${detail}), ` + consequence(clause),
|
|
4556
|
+
hint: "Rewrite the predicate inside the lowerable subset. " + PUSHDOWN_SUBSET2 + " Three traps in particular: (1) a function call \u2014 `size(record.tags) > 0`, `has(record.x)` \u2014 is correct in an object VALIDATION rule, which is INTERPRETED, and wrong here, where the predicate is COMPILED to a filter; write the null test as `field != null`. (2) A related-record path (`record.account.region`) is a join, which the compiler refuses by design (ADR-0055) \u2014 denormalise the value onto this object (a formula/rollup field) and test that column. (3) Arithmetic on a column (`amount * 2 > 100`) never lowers \u2014 precompute it into a field, or compare the column against the literal directly."
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4234
4559
|
});
|
|
4235
4560
|
});
|
|
4236
4561
|
return findings;
|
|
@@ -4239,7 +4564,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4239
4564
|
// src/validate-dashboard-action-refs.ts
|
|
4240
4565
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
4241
4566
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
4242
|
-
function
|
|
4567
|
+
function asArray24(v) {
|
|
4243
4568
|
if (Array.isArray(v)) return v;
|
|
4244
4569
|
if (v && typeof v === "object") {
|
|
4245
4570
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4273,14 +4598,14 @@ function collectKnownTargets(stack) {
|
|
|
4273
4598
|
const pages = /* @__PURE__ */ new Set();
|
|
4274
4599
|
const views = /* @__PURE__ */ new Set();
|
|
4275
4600
|
const collectNames = (v, into, name) => {
|
|
4276
|
-
for (const item of
|
|
4601
|
+
for (const item of asArray24(v)) {
|
|
4277
4602
|
if (!item || typeof item !== "object") continue;
|
|
4278
4603
|
const n = name(item);
|
|
4279
4604
|
if (n) into.add(n);
|
|
4280
4605
|
}
|
|
4281
4606
|
};
|
|
4282
4607
|
collectNames(stack.actions, actions, (a) => strName5(a.name));
|
|
4283
|
-
for (const obj of
|
|
4608
|
+
for (const obj of asArray24(stack.objects)) {
|
|
4284
4609
|
if (!obj || typeof obj !== "object") continue;
|
|
4285
4610
|
const n = strName5(obj.name);
|
|
4286
4611
|
if (n) objects.add(n);
|
|
@@ -4320,7 +4645,7 @@ function resolveUrlRoute(target, known) {
|
|
|
4320
4645
|
function validateDashboardActionRefs(stack) {
|
|
4321
4646
|
const findings = [];
|
|
4322
4647
|
if (!stack || typeof stack !== "object") return findings;
|
|
4323
|
-
const dashboards =
|
|
4648
|
+
const dashboards = asArray24(stack.dashboards);
|
|
4324
4649
|
if (dashboards.length === 0) return findings;
|
|
4325
4650
|
const known = collectKnownTargets(stack);
|
|
4326
4651
|
const checkOne = (action, where, path) => {
|
|
@@ -4360,7 +4685,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
4360
4685
|
if (!dash || typeof dash !== "object") continue;
|
|
4361
4686
|
const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
|
|
4362
4687
|
const dashPath = `dashboards[${di}]`;
|
|
4363
|
-
const headerActions =
|
|
4688
|
+
const headerActions = asArray24(dash.header?.actions);
|
|
4364
4689
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
4365
4690
|
const action = headerActions[ai];
|
|
4366
4691
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4371,18 +4696,6 @@ function validateDashboardActionRefs(stack) {
|
|
|
4371
4696
|
`${dashPath}.header.actions[${ai}].actionUrl`
|
|
4372
4697
|
);
|
|
4373
4698
|
}
|
|
4374
|
-
const widgets = asArray22(dash.widgets);
|
|
4375
|
-
for (let wi = 0; wi < widgets.length; wi++) {
|
|
4376
|
-
const widget = widgets[wi];
|
|
4377
|
-
if (!widget || typeof widget !== "object") continue;
|
|
4378
|
-
if (!strName5(widget.actionUrl)) continue;
|
|
4379
|
-
const widgetId = strName5(widget.id) ?? `#${wi}`;
|
|
4380
|
-
checkOne(
|
|
4381
|
-
{ actionType: widget.actionType, actionUrl: widget.actionUrl },
|
|
4382
|
-
`dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
|
|
4383
|
-
`${dashPath}.widgets[${wi}].actionUrl`
|
|
4384
|
-
);
|
|
4385
|
-
}
|
|
4386
4699
|
}
|
|
4387
4700
|
return findings;
|
|
4388
4701
|
}
|
|
@@ -4391,7 +4704,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
4391
4704
|
var import_data6 = require("@objectstack/spec/data");
|
|
4392
4705
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
4393
4706
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
4394
|
-
function
|
|
4707
|
+
function asArray25(v) {
|
|
4395
4708
|
if (Array.isArray(v)) return v;
|
|
4396
4709
|
if (v && typeof v === "object") {
|
|
4397
4710
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4460,7 +4773,7 @@ function validateFilterTokens(stack) {
|
|
|
4460
4773
|
["apps", "app"]
|
|
4461
4774
|
];
|
|
4462
4775
|
for (const [key, kind] of surfaces) {
|
|
4463
|
-
const items =
|
|
4776
|
+
const items = asArray25(stack[key]);
|
|
4464
4777
|
items.forEach((item, i) => {
|
|
4465
4778
|
const name = label(item.name ?? item.id, `#${i}`);
|
|
4466
4779
|
if (kind === "dashboard") {
|
|
@@ -4490,7 +4803,7 @@ var import_system2 = require("@objectstack/spec/system");
|
|
|
4490
4803
|
var PLATFORM_NAMES = [...import_system2.PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
4491
4804
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
4492
4805
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
4493
|
-
function
|
|
4806
|
+
function asArray26(v) {
|
|
4494
4807
|
if (Array.isArray(v)) return v;
|
|
4495
4808
|
if (v && typeof v === "object") {
|
|
4496
4809
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4537,7 +4850,7 @@ function distance2(a, b) {
|
|
|
4537
4850
|
function validateObjectReferences(stack) {
|
|
4538
4851
|
const findings = [];
|
|
4539
4852
|
if (!stack || typeof stack !== "object") return findings;
|
|
4540
|
-
const objects =
|
|
4853
|
+
const objects = asArray26(stack.objects);
|
|
4541
4854
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4542
4855
|
for (const obj of objects) {
|
|
4543
4856
|
const n = strName6(obj.name);
|
|
@@ -4570,7 +4883,7 @@ function validateObjectReferences(stack) {
|
|
|
4570
4883
|
});
|
|
4571
4884
|
};
|
|
4572
4885
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
4573
|
-
const params =
|
|
4886
|
+
const params = asArray26(action.params);
|
|
4574
4887
|
for (let pi = 0; pi < params.length; pi++) {
|
|
4575
4888
|
const param = params[pi];
|
|
4576
4889
|
if (!param || typeof param !== "object") continue;
|
|
@@ -4592,7 +4905,7 @@ function validateObjectReferences(stack) {
|
|
|
4592
4905
|
);
|
|
4593
4906
|
}
|
|
4594
4907
|
};
|
|
4595
|
-
const globalActions =
|
|
4908
|
+
const globalActions = asArray26(stack.actions);
|
|
4596
4909
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
4597
4910
|
const action = globalActions[ai];
|
|
4598
4911
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4602,7 +4915,7 @@ function validateObjectReferences(stack) {
|
|
|
4602
4915
|
const obj = objects[oi];
|
|
4603
4916
|
if (!obj || typeof obj !== "object") continue;
|
|
4604
4917
|
const objName = strName6(obj.name) ?? `#${oi}`;
|
|
4605
|
-
const objActions =
|
|
4918
|
+
const objActions = asArray26(obj.actions);
|
|
4606
4919
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
4607
4920
|
const action = objActions[ai];
|
|
4608
4921
|
if (!action || typeof action !== "object") continue;
|
|
@@ -4613,12 +4926,12 @@ function validateObjectReferences(stack) {
|
|
|
4613
4926
|
);
|
|
4614
4927
|
}
|
|
4615
4928
|
}
|
|
4616
|
-
const dashboards =
|
|
4929
|
+
const dashboards = asArray26(stack.dashboards);
|
|
4617
4930
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4618
4931
|
const dash = dashboards[di];
|
|
4619
4932
|
if (!dash || typeof dash !== "object") continue;
|
|
4620
4933
|
const dashName = strName6(dash.name) ?? `#${di}`;
|
|
4621
|
-
const filters =
|
|
4934
|
+
const filters = asArray26(dash.globalFilters);
|
|
4622
4935
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
4623
4936
|
const filter = filters[fi];
|
|
4624
4937
|
if (!filter || typeof filter !== "object") continue;
|
|
@@ -4633,13 +4946,13 @@ function validateObjectReferences(stack) {
|
|
|
4633
4946
|
);
|
|
4634
4947
|
}
|
|
4635
4948
|
}
|
|
4636
|
-
const apps =
|
|
4949
|
+
const apps = asArray26(stack.apps);
|
|
4637
4950
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4638
4951
|
const app = apps[ai];
|
|
4639
4952
|
if (!app || typeof app !== "object") continue;
|
|
4640
4953
|
const appName = strName6(app.name) ?? `#${ai}`;
|
|
4641
4954
|
const walkNav = (items, basePath) => {
|
|
4642
|
-
const navItems =
|
|
4955
|
+
const navItems = asArray26(items);
|
|
4643
4956
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4644
4957
|
const nav = navItems[ni];
|
|
4645
4958
|
if (!nav || typeof nav !== "object") continue;
|
|
@@ -4666,7 +4979,7 @@ function validateObjectReferences(stack) {
|
|
|
4666
4979
|
}
|
|
4667
4980
|
};
|
|
4668
4981
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4669
|
-
const areas =
|
|
4982
|
+
const areas = asArray26(app.areas);
|
|
4670
4983
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4671
4984
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4672
4985
|
}
|
|
@@ -4676,10 +4989,10 @@ function validateObjectReferences(stack) {
|
|
|
4676
4989
|
|
|
4677
4990
|
// src/validate-nav-target-refs.ts
|
|
4678
4991
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4679
|
-
var
|
|
4680
|
-
function
|
|
4681
|
-
if (Array.isArray(v)) return v.filter(
|
|
4682
|
-
if (
|
|
4992
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4993
|
+
function asArray27(v) {
|
|
4994
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
4995
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
4683
4996
|
return [];
|
|
4684
4997
|
}
|
|
4685
4998
|
function strName7(v) {
|
|
@@ -4693,7 +5006,7 @@ var NAV_TARGETS = [
|
|
|
4693
5006
|
];
|
|
4694
5007
|
function namesOf(collection) {
|
|
4695
5008
|
const out = /* @__PURE__ */ new Set();
|
|
4696
|
-
for (const entry of
|
|
5009
|
+
for (const entry of asArray27(collection)) {
|
|
4697
5010
|
const n = strName7(entry.name);
|
|
4698
5011
|
if (n) out.add(n);
|
|
4699
5012
|
}
|
|
@@ -4701,8 +5014,8 @@ function namesOf(collection) {
|
|
|
4701
5014
|
}
|
|
4702
5015
|
function validateNavTargetRefs(stack) {
|
|
4703
5016
|
const findings = [];
|
|
4704
|
-
if (!
|
|
4705
|
-
const apps =
|
|
5017
|
+
if (!isRec8(stack)) return findings;
|
|
5018
|
+
const apps = asArray27(stack.apps);
|
|
4706
5019
|
if (apps.length === 0) return findings;
|
|
4707
5020
|
const declared = /* @__PURE__ */ new Map();
|
|
4708
5021
|
for (const [, , collection] of NAV_TARGETS) {
|
|
@@ -4713,7 +5026,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4713
5026
|
const walk = (items, basePath) => {
|
|
4714
5027
|
if (!Array.isArray(items)) return;
|
|
4715
5028
|
for (const [ni, raw] of items.entries()) {
|
|
4716
|
-
if (!
|
|
5029
|
+
if (!isRec8(raw)) continue;
|
|
4717
5030
|
const nav = raw;
|
|
4718
5031
|
const navPath = `${basePath}[${ni}]`;
|
|
4719
5032
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -4736,7 +5049,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4736
5049
|
}
|
|
4737
5050
|
};
|
|
4738
5051
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4739
|
-
for (const [ari, area] of
|
|
5052
|
+
for (const [ari, area] of asArray27(app.areas).entries()) {
|
|
4740
5053
|
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
4741
5054
|
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
4742
5055
|
}
|
|
@@ -4746,7 +5059,7 @@ function validateNavTargetRefs(stack) {
|
|
|
4746
5059
|
|
|
4747
5060
|
// src/validate-action-name-refs.ts
|
|
4748
5061
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
4749
|
-
function
|
|
5062
|
+
function asArray28(v) {
|
|
4750
5063
|
if (Array.isArray(v)) return v;
|
|
4751
5064
|
if (v && typeof v === "object") {
|
|
4752
5065
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4790,13 +5103,13 @@ function suggest4(target, known) {
|
|
|
4790
5103
|
}
|
|
4791
5104
|
function collectActionNames(stack) {
|
|
4792
5105
|
const names = /* @__PURE__ */ new Set();
|
|
4793
|
-
for (const action of
|
|
5106
|
+
for (const action of asArray28(stack.actions)) {
|
|
4794
5107
|
const n = strName8(action?.name);
|
|
4795
5108
|
if (n) names.add(n);
|
|
4796
5109
|
}
|
|
4797
|
-
for (const obj of
|
|
5110
|
+
for (const obj of asArray28(stack.objects)) {
|
|
4798
5111
|
if (!obj || typeof obj !== "object") continue;
|
|
4799
|
-
for (const action of
|
|
5112
|
+
for (const action of asArray28(obj.actions)) {
|
|
4800
5113
|
const n = strName8(action?.name);
|
|
4801
5114
|
if (n) names.add(n);
|
|
4802
5115
|
}
|
|
@@ -4851,7 +5164,7 @@ function validateActionNameRefs(stack) {
|
|
|
4851
5164
|
);
|
|
4852
5165
|
}
|
|
4853
5166
|
};
|
|
4854
|
-
const views =
|
|
5167
|
+
const views = asArray28(stack.views);
|
|
4855
5168
|
for (let vi = 0; vi < views.length; vi++) {
|
|
4856
5169
|
const view = views[vi];
|
|
4857
5170
|
if (!view || typeof view !== "object") continue;
|
|
@@ -4865,7 +5178,7 @@ function validateActionNameRefs(stack) {
|
|
|
4865
5178
|
}
|
|
4866
5179
|
}
|
|
4867
5180
|
}
|
|
4868
|
-
const objects =
|
|
5181
|
+
const objects = asArray28(stack.objects);
|
|
4869
5182
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4870
5183
|
const obj = objects[oi];
|
|
4871
5184
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -4876,7 +5189,7 @@ function validateActionNameRefs(stack) {
|
|
|
4876
5189
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
4877
5190
|
}
|
|
4878
5191
|
}
|
|
4879
|
-
const pages =
|
|
5192
|
+
const pages = asArray28(stack.pages);
|
|
4880
5193
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4881
5194
|
const page = pages[pi];
|
|
4882
5195
|
if (!page || typeof page !== "object") continue;
|
|
@@ -4895,13 +5208,13 @@ function validateActionNameRefs(stack) {
|
|
|
4895
5208
|
}
|
|
4896
5209
|
}
|
|
4897
5210
|
}
|
|
4898
|
-
const apps =
|
|
5211
|
+
const apps = asArray28(stack.apps);
|
|
4899
5212
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4900
5213
|
const app = apps[ai];
|
|
4901
5214
|
if (!app || typeof app !== "object") continue;
|
|
4902
5215
|
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4903
5216
|
const walkNav = (items, basePath) => {
|
|
4904
|
-
const navItems =
|
|
5217
|
+
const navItems = asArray28(items);
|
|
4905
5218
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4906
5219
|
const nav = navItems[ni];
|
|
4907
5220
|
if (!nav || typeof nav !== "object") continue;
|
|
@@ -4920,7 +5233,7 @@ function validateActionNameRefs(stack) {
|
|
|
4920
5233
|
}
|
|
4921
5234
|
};
|
|
4922
5235
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4923
|
-
const areas =
|
|
5236
|
+
const areas = asArray28(app.areas);
|
|
4924
5237
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4925
5238
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4926
5239
|
}
|
|
@@ -4930,7 +5243,7 @@ function validateActionNameRefs(stack) {
|
|
|
4930
5243
|
|
|
4931
5244
|
// src/validate-action-locations.ts
|
|
4932
5245
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
4933
|
-
function
|
|
5246
|
+
function asArray29(v) {
|
|
4934
5247
|
if (Array.isArray(v)) return v;
|
|
4935
5248
|
if (v && typeof v === "object") {
|
|
4936
5249
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4951,7 +5264,7 @@ function collectNamePlacedActions(stack) {
|
|
|
4951
5264
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
4952
5265
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
4953
5266
|
}
|
|
4954
|
-
for (const def of
|
|
5267
|
+
for (const def of asArray29(list3.bulkActionDefs)) {
|
|
4955
5268
|
const n = strName9(def?.name);
|
|
4956
5269
|
if (n) placed.add(n);
|
|
4957
5270
|
}
|
|
@@ -4960,12 +5273,12 @@ function collectNamePlacedActions(stack) {
|
|
|
4960
5273
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
4961
5274
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
4962
5275
|
};
|
|
4963
|
-
for (const view of
|
|
5276
|
+
for (const view of asArray29(stack.views)) {
|
|
4964
5277
|
if (!view || typeof view !== "object") continue;
|
|
4965
5278
|
harvest(view.list);
|
|
4966
5279
|
harvestListViews(view.listViews);
|
|
4967
5280
|
}
|
|
4968
|
-
for (const obj of
|
|
5281
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4969
5282
|
if (!obj || typeof obj !== "object") continue;
|
|
4970
5283
|
harvestListViews(obj.listViews);
|
|
4971
5284
|
}
|
|
@@ -4990,37 +5303,121 @@ function validateActionLocations(stack) {
|
|
|
4990
5303
|
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
5304
|
});
|
|
4992
5305
|
};
|
|
4993
|
-
const actions =
|
|
5306
|
+
const actions = asArray29(stack.actions);
|
|
4994
5307
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
4995
|
-
const objects =
|
|
5308
|
+
const objects = asArray29(stack.objects);
|
|
4996
5309
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4997
5310
|
const obj = objects[oi];
|
|
4998
5311
|
if (!obj || typeof obj !== "object") continue;
|
|
4999
|
-
const own =
|
|
5312
|
+
const own = asArray29(obj.actions);
|
|
5000
5313
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
5001
5314
|
}
|
|
5002
5315
|
return findings;
|
|
5003
5316
|
}
|
|
5004
5317
|
|
|
5318
|
+
// src/validate-component-props.ts
|
|
5319
|
+
var import_ui3 = require("@objectstack/spec/ui");
|
|
5320
|
+
var import_spec2 = require("@objectstack/spec");
|
|
5321
|
+
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5322
|
+
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5323
|
+
function isRec9(v) {
|
|
5324
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5325
|
+
}
|
|
5326
|
+
function strName10(v) {
|
|
5327
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5328
|
+
}
|
|
5329
|
+
function asArray30(v) {
|
|
5330
|
+
if (Array.isArray(v)) return v;
|
|
5331
|
+
if (v && typeof v === "object") {
|
|
5332
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5333
|
+
}
|
|
5334
|
+
return [];
|
|
5335
|
+
}
|
|
5336
|
+
var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
5337
|
+
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5338
|
+
function suppliedByDataSource(issue, component) {
|
|
5339
|
+
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5340
|
+
const dataSource = isRec9(component.dataSource) ? component.dataSource : void 0;
|
|
5341
|
+
return strName10(dataSource?.object) !== void 0;
|
|
5342
|
+
}
|
|
5343
|
+
function validateComponentProps(stack) {
|
|
5344
|
+
const findings = [];
|
|
5345
|
+
if (!isRec9(stack)) return findings;
|
|
5346
|
+
const pages = asArray30(stack.pages);
|
|
5347
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
5348
|
+
const page = pages[pi];
|
|
5349
|
+
if (!isRec9(page)) continue;
|
|
5350
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
5351
|
+
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5352
|
+
const type = strName10(component.type);
|
|
5353
|
+
if (!type) continue;
|
|
5354
|
+
const schema = PROPS_SCHEMAS[type];
|
|
5355
|
+
if (!schema) continue;
|
|
5356
|
+
const props = isRec9(component.properties) ? component.properties : void 0;
|
|
5357
|
+
if (!props) continue;
|
|
5358
|
+
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5359
|
+
const base = `${path}.properties`;
|
|
5360
|
+
for (const f of (0, import_spec2.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
|
|
5361
|
+
findings.push({
|
|
5362
|
+
severity: "warning",
|
|
5363
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5364
|
+
where,
|
|
5365
|
+
path: f.path,
|
|
5366
|
+
message: `\`${f.key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui), so nothing verifies it: \`properties\` is an untyped bag, the renderer spreads whatever it carries, and a key it does not read is ignored in silence.` + (f.suggestion ? ` Did you mean \`${f.suggestion}\`?` : ""),
|
|
5367
|
+
hint: f.guidance ?? (f.suggestion ? `Rename \`${f.key}\` \u2192 \`${f.suggestion}\`.` : `Remove \`${f.key}\`, or \u2014 if the component really does honour it \u2014 declare it on \`${type}\`'s props schema so the declaration and the renderer agree.`)
|
|
5368
|
+
});
|
|
5369
|
+
}
|
|
5370
|
+
const parsed = schema.safeParse(props);
|
|
5371
|
+
if (parsed.success) continue;
|
|
5372
|
+
for (const issue of parsed.error?.issues ?? []) {
|
|
5373
|
+
if (suppliedByDataSource(issue, component)) continue;
|
|
5374
|
+
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5375
|
+
if (issue.code === "unrecognized_keys") {
|
|
5376
|
+
for (const key of issue.keys ?? []) {
|
|
5377
|
+
findings.push({
|
|
5378
|
+
severity: "warning",
|
|
5379
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5380
|
+
where,
|
|
5381
|
+
path: `${at}.${key}`,
|
|
5382
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${issue.message}`,
|
|
5383
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5386
|
+
continue;
|
|
5387
|
+
}
|
|
5388
|
+
findings.push({
|
|
5389
|
+
severity: "warning",
|
|
5390
|
+
rule: COMPONENT_PROPS_INVALID,
|
|
5391
|
+
where,
|
|
5392
|
+
path: at,
|
|
5393
|
+
message: `${at.slice(base.length + 1) || "properties"}: ${describeIssue(issue, props)}`,
|
|
5394
|
+
hint: `\`${type}\`'s props are declared by ComponentPropsMap (@objectstack/spec/ui) \u2014 the rejection above carries the fix. Advisory for now: the props bag is not parsed on the storage path either, so nothing rejects this today (objectstack#5068).`
|
|
5395
|
+
});
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
return findings;
|
|
5400
|
+
}
|
|
5401
|
+
|
|
5005
5402
|
// src/validate-chart-bindings.ts
|
|
5006
5403
|
var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
5007
5404
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
5008
5405
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
5009
5406
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
5010
|
-
function
|
|
5407
|
+
function asArray31(v) {
|
|
5011
5408
|
if (Array.isArray(v)) return v;
|
|
5012
5409
|
if (v && typeof v === "object") {
|
|
5013
5410
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5014
5411
|
}
|
|
5015
5412
|
return [];
|
|
5016
5413
|
}
|
|
5017
|
-
function
|
|
5414
|
+
function strName11(v) {
|
|
5018
5415
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5019
5416
|
}
|
|
5020
5417
|
function strList3(v) {
|
|
5021
5418
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
5022
5419
|
}
|
|
5023
|
-
function
|
|
5420
|
+
function isRec10(v) {
|
|
5024
5421
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5025
5422
|
}
|
|
5026
5423
|
function distance4(a, b) {
|
|
@@ -5058,17 +5455,17 @@ function list2(names) {
|
|
|
5058
5455
|
}
|
|
5059
5456
|
function indexDatasets(stack) {
|
|
5060
5457
|
const out = /* @__PURE__ */ new Map();
|
|
5061
|
-
for (const ds of
|
|
5062
|
-
const name =
|
|
5458
|
+
for (const ds of asArray31(stack.datasets)) {
|
|
5459
|
+
const name = strName11(ds.name);
|
|
5063
5460
|
if (!name) continue;
|
|
5064
5461
|
const dimensions = /* @__PURE__ */ new Set();
|
|
5065
|
-
for (const d of
|
|
5066
|
-
const n =
|
|
5462
|
+
for (const d of asArray31(ds.dimensions)) {
|
|
5463
|
+
const n = strName11(d.name);
|
|
5067
5464
|
if (n) dimensions.add(n);
|
|
5068
5465
|
}
|
|
5069
5466
|
const measures = /* @__PURE__ */ new Set();
|
|
5070
|
-
for (const m of
|
|
5071
|
-
const n =
|
|
5467
|
+
for (const m of asArray31(ds.measures)) {
|
|
5468
|
+
const n = strName11(m.name);
|
|
5072
5469
|
if (n) measures.add(n);
|
|
5073
5470
|
}
|
|
5074
5471
|
out.set(name, { dimensions, measures });
|
|
@@ -5146,29 +5543,29 @@ function validateChartBindings(stack) {
|
|
|
5146
5543
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
5147
5544
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
5148
5545
|
};
|
|
5149
|
-
const reports =
|
|
5546
|
+
const reports = asArray31(stack.reports);
|
|
5150
5547
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
5151
5548
|
const report = reports[ri];
|
|
5152
|
-
if (!
|
|
5153
|
-
const reportName =
|
|
5549
|
+
if (!isRec10(report)) continue;
|
|
5550
|
+
const reportName = strName11(report.name) ?? `#${ri}`;
|
|
5154
5551
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
5155
|
-
if (!
|
|
5552
|
+
if (!isRec10(chart)) return;
|
|
5156
5553
|
check({
|
|
5157
5554
|
dataset,
|
|
5158
5555
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
5159
5556
|
// it in lets the yAxis "declared but not selected" check work without
|
|
5160
5557
|
// reporting the selection itself twice.
|
|
5161
5558
|
values: { names: values, path: `${path}.values` },
|
|
5162
|
-
xAxis:
|
|
5163
|
-
yAxis:
|
|
5164
|
-
series:
|
|
5559
|
+
xAxis: strName11(chart.xAxis) ? { name: strName11(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
5560
|
+
yAxis: strName11(chart.yAxis) ? { name: strName11(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
5561
|
+
series: asArray31(chart.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
5165
5562
|
where,
|
|
5166
5563
|
path: `${path}.chart`
|
|
5167
5564
|
});
|
|
5168
5565
|
};
|
|
5169
5566
|
checkReportChart(
|
|
5170
5567
|
report.chart,
|
|
5171
|
-
|
|
5568
|
+
strName11(report.dataset),
|
|
5172
5569
|
strList3(report.values),
|
|
5173
5570
|
`report "${reportName}" \xB7 chart`,
|
|
5174
5571
|
`reports[${ri}]`
|
|
@@ -5176,45 +5573,45 @@ function validateChartBindings(stack) {
|
|
|
5176
5573
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
5177
5574
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
5178
5575
|
const block = blocks[bi];
|
|
5179
|
-
if (!
|
|
5576
|
+
if (!isRec10(block)) continue;
|
|
5180
5577
|
checkReportChart(
|
|
5181
5578
|
block.chart,
|
|
5182
|
-
|
|
5579
|
+
strName11(block.dataset),
|
|
5183
5580
|
strList3(block.values),
|
|
5184
|
-
`report "${reportName}" \xB7 block "${
|
|
5581
|
+
`report "${reportName}" \xB7 block "${strName11(block.name) ?? `#${bi}`}" chart`,
|
|
5185
5582
|
`reports[${ri}].blocks[${bi}]`
|
|
5186
5583
|
);
|
|
5187
5584
|
}
|
|
5188
5585
|
}
|
|
5189
5586
|
const checkListChart = (container, where, path) => {
|
|
5190
|
-
if (!
|
|
5587
|
+
if (!isRec10(container)) return;
|
|
5191
5588
|
const chart = container.chart;
|
|
5192
|
-
if (!
|
|
5589
|
+
if (!isRec10(chart)) return;
|
|
5193
5590
|
check({
|
|
5194
|
-
dataset:
|
|
5591
|
+
dataset: strName11(chart.dataset),
|
|
5195
5592
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5196
5593
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
5197
5594
|
where,
|
|
5198
5595
|
path: `${path}.chart`
|
|
5199
5596
|
});
|
|
5200
5597
|
};
|
|
5201
|
-
const views =
|
|
5598
|
+
const views = asArray31(stack.views);
|
|
5202
5599
|
for (let vi = 0; vi < views.length; vi++) {
|
|
5203
5600
|
const view = views[vi];
|
|
5204
|
-
if (!
|
|
5205
|
-
const viewName =
|
|
5601
|
+
if (!isRec10(view)) continue;
|
|
5602
|
+
const viewName = strName11(view.name) ?? strName11(view.objectName) ?? `#${vi}`;
|
|
5206
5603
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
5207
|
-
if (
|
|
5604
|
+
if (isRec10(view.listViews)) {
|
|
5208
5605
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
5209
5606
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
5210
5607
|
}
|
|
5211
5608
|
}
|
|
5212
5609
|
}
|
|
5213
|
-
const objects =
|
|
5610
|
+
const objects = asArray31(stack.objects);
|
|
5214
5611
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5215
5612
|
const obj = objects[oi];
|
|
5216
|
-
if (!
|
|
5217
|
-
const objName =
|
|
5613
|
+
if (!isRec10(obj) || !isRec10(obj.listViews)) continue;
|
|
5614
|
+
const objName = strName11(obj.name) ?? `#${oi}`;
|
|
5218
5615
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
5219
5616
|
checkListChart(
|
|
5220
5617
|
lv,
|
|
@@ -5223,22 +5620,22 @@ function validateChartBindings(stack) {
|
|
|
5223
5620
|
);
|
|
5224
5621
|
}
|
|
5225
5622
|
}
|
|
5226
|
-
const pages =
|
|
5623
|
+
const pages = asArray31(stack.pages);
|
|
5227
5624
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5228
5625
|
const page = pages[pi];
|
|
5229
|
-
if (!
|
|
5230
|
-
const pageName =
|
|
5626
|
+
if (!isRec10(page)) continue;
|
|
5627
|
+
const pageName = strName11(page.name) ?? `#${pi}`;
|
|
5231
5628
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5232
|
-
const props =
|
|
5233
|
-
if (!props || !
|
|
5234
|
-
const axisRefs =
|
|
5235
|
-
const seriesRefs =
|
|
5629
|
+
const props = isRec10(component.properties) ? component.properties : void 0;
|
|
5630
|
+
if (!props || !strName11(props.dataset)) continue;
|
|
5631
|
+
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName11(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
5632
|
+
const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName11(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
5236
5633
|
check({
|
|
5237
|
-
dataset:
|
|
5634
|
+
dataset: strName11(props.dataset),
|
|
5238
5635
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5239
5636
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
5240
5637
|
series: [...axisRefs, ...seriesRefs],
|
|
5241
|
-
where: `page "${pageName}" \xB7 ${
|
|
5638
|
+
where: `page "${pageName}" \xB7 ${strName11(component.type) ?? "chart"}`,
|
|
5242
5639
|
path: `${path}.properties`
|
|
5243
5640
|
});
|
|
5244
5641
|
}
|
|
@@ -5246,12 +5643,265 @@ function validateChartBindings(stack) {
|
|
|
5246
5643
|
return findings;
|
|
5247
5644
|
}
|
|
5248
5645
|
|
|
5249
|
-
// src/validate-
|
|
5250
|
-
var
|
|
5251
|
-
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5646
|
+
// src/validate-rule-compilability.ts
|
|
5647
|
+
var import_node_module3 = require("module");
|
|
5648
|
+
var import_meta3 = {};
|
|
5649
|
+
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
5650
|
+
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
5651
|
+
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
5652
|
+
var isRec11 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5653
|
+
function asArray32(v) {
|
|
5654
|
+
if (Array.isArray(v)) return v.filter(isRec11);
|
|
5655
|
+
if (isRec11(v)) {
|
|
5656
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ name, ...def }));
|
|
5657
|
+
}
|
|
5658
|
+
return [];
|
|
5659
|
+
}
|
|
5660
|
+
var cachedAjv = null;
|
|
5661
|
+
var cachedAddFormats = null;
|
|
5662
|
+
function loadAjv() {
|
|
5663
|
+
if (cachedAjv) return cachedAjv;
|
|
5664
|
+
const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5665
|
+
let mod;
|
|
5666
|
+
try {
|
|
5667
|
+
mod = (0, import_node_module3.createRequire)(anchor)("ajv");
|
|
5668
|
+
} catch (err) {
|
|
5669
|
+
throw new Error(
|
|
5670
|
+
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
|
|
5671
|
+
);
|
|
5672
|
+
}
|
|
5673
|
+
const ctor = isRec11(mod) && "default" in mod ? mod.default : mod;
|
|
5674
|
+
cachedAjv = ctor;
|
|
5675
|
+
return ctor;
|
|
5676
|
+
}
|
|
5677
|
+
function loadAddFormats() {
|
|
5678
|
+
if (cachedAddFormats) return cachedAddFormats;
|
|
5679
|
+
const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5680
|
+
let mod;
|
|
5681
|
+
try {
|
|
5682
|
+
mod = (0, import_node_module3.createRequire)(anchor)("ajv-formats");
|
|
5683
|
+
} catch (err) {
|
|
5684
|
+
throw new Error(
|
|
5685
|
+
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
|
|
5686
|
+
);
|
|
5687
|
+
}
|
|
5688
|
+
const plugin = isRec11(mod) && "default" in mod ? mod.default : mod;
|
|
5689
|
+
cachedAddFormats = plugin;
|
|
5690
|
+
return plugin;
|
|
5691
|
+
}
|
|
5692
|
+
function createRuntimeAjv() {
|
|
5693
|
+
const instance = new (loadAjv())(RUNTIME_AJV_OPTIONS);
|
|
5694
|
+
loadAddFormats()(instance);
|
|
5695
|
+
return instance;
|
|
5696
|
+
}
|
|
5697
|
+
function registeredFormatNames() {
|
|
5698
|
+
const names = Object.keys(createRuntimeAjv().formats).sort();
|
|
5699
|
+
if (names.length === 0) {
|
|
5700
|
+
throw new Error(
|
|
5701
|
+
`@objectstack/lint: the runtime-parity ajv instance has no \`format\` registered. "ajv-formats" loaded but added nothing, so the set of legitimate format names is unknown \u2014 refusing to judge format names against an empty vocabulary, which would report every \`format\` in the stack as misspelled. Check that the installed "ajv-formats" is the real package and matches the version @objectstack/lint declares.`
|
|
5702
|
+
);
|
|
5703
|
+
}
|
|
5704
|
+
return names;
|
|
5705
|
+
}
|
|
5706
|
+
function errorText(err) {
|
|
5707
|
+
return err instanceof Error ? err.message : String(err);
|
|
5708
|
+
}
|
|
5709
|
+
var MAX_RULE_NESTING_DEPTH = 16;
|
|
5710
|
+
function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
5711
|
+
const name = typeof rule.name === "string" && rule.name ? rule.name : "?";
|
|
5712
|
+
const label2 = labelTrail ? `${labelTrail} \u2192 '${name}'` : `'${name}'`;
|
|
5713
|
+
const path = pathTrail ? `${pathTrail}.${name}` : name;
|
|
5714
|
+
const out = [{ rule, label: label2, path }];
|
|
5715
|
+
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
5716
|
+
for (const branch of ["then", "otherwise"]) {
|
|
5717
|
+
const nested = rule[branch];
|
|
5718
|
+
if (isRec11(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
5719
|
+
}
|
|
5720
|
+
return out;
|
|
5721
|
+
}
|
|
5722
|
+
function walkObjectValidationRules(stack) {
|
|
5723
|
+
const walked = [];
|
|
5724
|
+
if (!isRec11(stack)) return walked;
|
|
5725
|
+
for (const obj of asArray32(stack.objects)) {
|
|
5726
|
+
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
5727
|
+
const validations = obj.validations;
|
|
5728
|
+
for (const authored of asArray32(validations)) {
|
|
5729
|
+
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
5730
|
+
walked.push({
|
|
5731
|
+
rule,
|
|
5732
|
+
objectName,
|
|
5733
|
+
label: label2,
|
|
5734
|
+
where: `object '${objectName}' \xB7 validation ${label2}`,
|
|
5735
|
+
basePath: `objects.${objectName}.validations.${path}`
|
|
5736
|
+
});
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
}
|
|
5740
|
+
return walked;
|
|
5741
|
+
}
|
|
5742
|
+
function validateRuleCompilability(stack) {
|
|
5743
|
+
const findings = [];
|
|
5744
|
+
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5745
|
+
if (rule.type === "format" && typeof rule.regex === "string" && rule.regex !== "") {
|
|
5746
|
+
try {
|
|
5747
|
+
new RegExp(rule.regex);
|
|
5748
|
+
} catch (err) {
|
|
5749
|
+
findings.push({
|
|
5750
|
+
severity: "error",
|
|
5751
|
+
rule: VALIDATION_RULE_REGEX_UNCOMPILABLE,
|
|
5752
|
+
where,
|
|
5753
|
+
path: `${basePath}.regex`,
|
|
5754
|
+
message: `\`format\` validation ${label2} on object '${objectName}' declares a \`regex\` that does not compile: ${errorText(err)}. The write path builds it with \`new RegExp(rule.regex)\` and SKIPS the rule when that throws (rule-validator.ts \`checkFormat\`), so the rule is declared, listed in the metadata, and enforces nothing on any record.`,
|
|
5755
|
+
hint: `Fix the pattern so \`new RegExp('${rule.regex}')\` compiles \u2014 a literal \`(\`, \`[\` or \`\\\` must be escaped (\`\\\\(\`, \`\\\\[\`, \`\\\\\\\\\`), and the source is a STRING, so a backslash is written twice in TypeScript ('^\\\\d{2}-\\\\d{7}$'). Or drop \`regex\` and use a named \`format\` ('email' | 'url' | 'phone' | 'json').`
|
|
5756
|
+
});
|
|
5757
|
+
}
|
|
5758
|
+
}
|
|
5759
|
+
if (rule.type === "json_schema" && isRec11(rule.schema)) {
|
|
5760
|
+
try {
|
|
5761
|
+
createRuntimeAjv().compile(rule.schema);
|
|
5762
|
+
} catch (err) {
|
|
5763
|
+
findings.push({
|
|
5764
|
+
severity: "error",
|
|
5765
|
+
rule: VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
|
|
5766
|
+
where,
|
|
5767
|
+
path: `${basePath}.schema`,
|
|
5768
|
+
message: `\`json_schema\` validation ${label2} on object '${objectName}' declares a \`schema\` ajv cannot compile: ${errorText(err)}. The write path compiles it with the same ajv (\`new Ajv({ allErrors: true, strict: false })\` + \`ajv-formats\`) and SKIPS the rule when that throws (rule-validator.ts \`checkJsonSchema\`), so the rule is declared and enforces nothing on any record.`,
|
|
5769
|
+
hint: `Correct the schema so ajv compiles it \u2014 the message above names the offending keyword. \`type\` must be one of null|boolean|object|array|number|string|integer (or an array of those), \`required\` an array of strings, and every \`$ref\` must resolve. Vendor keywords are fine (the runtime runs \`strict: false\`); a MALFORMED standard keyword is not.`
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
return findings;
|
|
5775
|
+
}
|
|
5776
|
+
|
|
5777
|
+
// src/validate-rule-schema-formats.ts
|
|
5778
|
+
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
5779
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5780
|
+
var SUBSCHEMA_KEYS = [
|
|
5781
|
+
"additionalItems",
|
|
5782
|
+
"additionalProperties",
|
|
5783
|
+
"contains",
|
|
5784
|
+
"propertyNames",
|
|
5785
|
+
"if",
|
|
5786
|
+
"then",
|
|
5787
|
+
"else",
|
|
5788
|
+
"not",
|
|
5789
|
+
"unevaluatedItems",
|
|
5790
|
+
"unevaluatedProperties"
|
|
5791
|
+
];
|
|
5792
|
+
var SUBSCHEMA_LIST_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
5793
|
+
var SUBSCHEMA_MAP_KEYS = [
|
|
5794
|
+
"properties",
|
|
5795
|
+
"patternProperties",
|
|
5796
|
+
"$defs",
|
|
5797
|
+
"definitions",
|
|
5798
|
+
"dependentSchemas"
|
|
5799
|
+
];
|
|
5800
|
+
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
5801
|
+
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
5802
|
+
function collectFormatUses(schema, pointer, out, depth) {
|
|
5803
|
+
if (!isRec12(schema)) return;
|
|
5804
|
+
if (typeof schema.format === "string") {
|
|
5805
|
+
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
5806
|
+
}
|
|
5807
|
+
if (depth >= MAX_SCHEMA_WALK_DEPTH) return;
|
|
5808
|
+
for (const key of SUBSCHEMA_KEYS) {
|
|
5809
|
+
if (key in schema) {
|
|
5810
|
+
collectFormatUses(schema[key], `${pointer}/${escapePointerSegment(key)}`, out, depth + 1);
|
|
5811
|
+
}
|
|
5812
|
+
}
|
|
5813
|
+
for (const key of SUBSCHEMA_LIST_KEYS) {
|
|
5814
|
+
const value = schema[key];
|
|
5815
|
+
if (!Array.isArray(value)) continue;
|
|
5816
|
+
value.forEach((entry, index) => {
|
|
5817
|
+
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${index}`, out, depth + 1);
|
|
5818
|
+
});
|
|
5819
|
+
}
|
|
5820
|
+
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
5821
|
+
const value = schema[key];
|
|
5822
|
+
if (!isRec12(value)) continue;
|
|
5823
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
5824
|
+
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5825
|
+
}
|
|
5826
|
+
}
|
|
5827
|
+
const items = schema.items;
|
|
5828
|
+
if (Array.isArray(items)) {
|
|
5829
|
+
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
5830
|
+
} else if (isRec12(items)) {
|
|
5831
|
+
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
5832
|
+
}
|
|
5833
|
+
const dependencies = schema.dependencies;
|
|
5834
|
+
if (isRec12(dependencies)) {
|
|
5835
|
+
for (const [name, entry] of Object.entries(dependencies)) {
|
|
5836
|
+
if (!isRec12(entry)) continue;
|
|
5837
|
+
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
}
|
|
5841
|
+
function editDistance2(a, b) {
|
|
5842
|
+
let previous = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
5843
|
+
for (let i = 1; i <= a.length; i++) {
|
|
5844
|
+
const current = [i];
|
|
5845
|
+
for (let j = 1; j <= b.length; j++) {
|
|
5846
|
+
current[j] = Math.min(
|
|
5847
|
+
previous[j] + 1,
|
|
5848
|
+
current[j - 1] + 1,
|
|
5849
|
+
previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1)
|
|
5850
|
+
);
|
|
5851
|
+
}
|
|
5852
|
+
previous = current;
|
|
5853
|
+
}
|
|
5854
|
+
return previous[b.length];
|
|
5855
|
+
}
|
|
5856
|
+
function nearestRegisteredFormat(name, registered) {
|
|
5857
|
+
const budget = Math.min(3, Math.floor(name.length / 2));
|
|
5858
|
+
if (budget < 1) return null;
|
|
5859
|
+
const authored = name.toLowerCase();
|
|
5860
|
+
let best = null;
|
|
5861
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
5862
|
+
for (const candidate of [...registered].sort()) {
|
|
5863
|
+
const distance7 = editDistance2(authored, candidate);
|
|
5864
|
+
if (distance7 < bestDistance) {
|
|
5865
|
+
bestDistance = distance7;
|
|
5866
|
+
best = candidate;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
return bestDistance <= budget ? best : null;
|
|
5870
|
+
}
|
|
5871
|
+
function validateRuleSchemaFormats(stack) {
|
|
5872
|
+
const findings = [];
|
|
5873
|
+
const pending = [];
|
|
5874
|
+
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5875
|
+
if (rule.type !== "json_schema" || !isRec12(rule.schema)) continue;
|
|
5876
|
+
const uses = [];
|
|
5877
|
+
collectFormatUses(rule.schema, "", uses, 0);
|
|
5878
|
+
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
5879
|
+
}
|
|
5880
|
+
if (pending.length === 0) return findings;
|
|
5881
|
+
const registered = registeredFormatNames();
|
|
5882
|
+
const known = new Set(registered);
|
|
5883
|
+
for (const { use, where, label: label2, objectName, basePath } of pending) {
|
|
5884
|
+
if (known.has(use.name)) continue;
|
|
5885
|
+
const suggestion = nearestRegisteredFormat(use.name, registered);
|
|
5886
|
+
const pointer = `#${use.pointer}`;
|
|
5887
|
+
findings.push({
|
|
5888
|
+
severity: "error",
|
|
5889
|
+
rule: VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
|
|
5890
|
+
where,
|
|
5891
|
+
path: `${basePath}.schema${pointer}`,
|
|
5892
|
+
message: `\`json_schema\` validation ${label2} on object '${objectName}' names \`format: '${use.name}'\` at \`${pointer}\`, which is not a registered format. ajv logs \`unknown format "${use.name}" ignored\` once at compile time and DROPS the keyword \u2014 in the write path (rule-validator.ts, \`strict: false\`) and in the publish gate alike \u2014 so the schema compiles, the rule ships and runs on every write, its \`type\`/\`required\` keywords are enforced, and this constraint is enforced on no record, ever. The record is ACCEPTED, so nothing downstream reports the gap either.`,
|
|
5893
|
+
hint: (suggestion ? `Did you mean \`format: '${suggestion}'\`? ` : "") + `The registered names are: ${registered.join(", ")} \u2014 the default \`ajv-formats\` set, the one \`rule-validator.ts\` registers (#5029). Names are case-sensitive and hyphenated (\`date-time\`, not \`datetime\`). If you meant a constraint ajv has no format for, express it with \`pattern\` instead \u2014 a regex is enforced, an unknown format name is not.`
|
|
5894
|
+
});
|
|
5895
|
+
}
|
|
5896
|
+
return findings;
|
|
5897
|
+
}
|
|
5898
|
+
|
|
5899
|
+
// src/validate-nav-access.ts
|
|
5900
|
+
var import_system3 = require("@objectstack/spec/system");
|
|
5901
|
+
|
|
5902
|
+
// src/build-access-matrix.ts
|
|
5903
|
+
function asArray33(v) {
|
|
5904
|
+
if (Array.isArray(v)) return v;
|
|
5255
5905
|
if (v && typeof v === "object") {
|
|
5256
5906
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5257
5907
|
}
|
|
@@ -5261,13 +5911,13 @@ function buildAccessMatrix(stack) {
|
|
|
5261
5911
|
const entries = [];
|
|
5262
5912
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
5263
5913
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
5264
|
-
for (const obj of
|
|
5914
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5265
5915
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
5266
5916
|
if (!name) continue;
|
|
5267
5917
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
5268
5918
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
5269
5919
|
}
|
|
5270
|
-
for (const ps of
|
|
5920
|
+
for (const ps of asArray33(stack.permissions)) {
|
|
5271
5921
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
5272
5922
|
if (!psName) continue;
|
|
5273
5923
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -5340,34 +5990,34 @@ function diffAccessMatrix(before, after) {
|
|
|
5340
5990
|
|
|
5341
5991
|
// src/validate-nav-access.ts
|
|
5342
5992
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
5343
|
-
function
|
|
5993
|
+
function asArray34(v) {
|
|
5344
5994
|
if (Array.isArray(v)) return v;
|
|
5345
5995
|
if (v && typeof v === "object") {
|
|
5346
5996
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5347
5997
|
}
|
|
5348
5998
|
return [];
|
|
5349
5999
|
}
|
|
5350
|
-
function
|
|
6000
|
+
function strName12(v) {
|
|
5351
6001
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5352
6002
|
}
|
|
5353
6003
|
function collectNavExposures(stack) {
|
|
5354
6004
|
const out = [];
|
|
5355
|
-
const apps =
|
|
6005
|
+
const apps = asArray34(stack.apps);
|
|
5356
6006
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
5357
6007
|
const app = apps[ai];
|
|
5358
6008
|
if (!app || typeof app !== "object") continue;
|
|
5359
|
-
const appName =
|
|
6009
|
+
const appName = strName12(app.name) ?? `#${ai}`;
|
|
5360
6010
|
const walk = (items, basePath) => {
|
|
5361
|
-
const navItems =
|
|
6011
|
+
const navItems = asArray34(items);
|
|
5362
6012
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
5363
6013
|
const nav = navItems[ni];
|
|
5364
6014
|
if (!nav || typeof nav !== "object") continue;
|
|
5365
6015
|
const navPath = `${basePath}[${ni}]`;
|
|
5366
|
-
const objectName =
|
|
6016
|
+
const objectName = strName12(nav.objectName);
|
|
5367
6017
|
if (nav.type === "object" && objectName) {
|
|
5368
6018
|
out.push({
|
|
5369
6019
|
objectName,
|
|
5370
|
-
where: `app "${appName}" \xB7 nav "${
|
|
6020
|
+
where: `app "${appName}" \xB7 nav "${strName12(nav.id) ?? `#${ni}`}"`,
|
|
5371
6021
|
path: `${navPath}.objectName`
|
|
5372
6022
|
});
|
|
5373
6023
|
}
|
|
@@ -5375,7 +6025,7 @@ function collectNavExposures(stack) {
|
|
|
5375
6025
|
}
|
|
5376
6026
|
};
|
|
5377
6027
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
5378
|
-
const areas =
|
|
6028
|
+
const areas = asArray34(app.areas);
|
|
5379
6029
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
5380
6030
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
5381
6031
|
}
|
|
@@ -5385,13 +6035,13 @@ function collectNavExposures(stack) {
|
|
|
5385
6035
|
function validateNavAccess(stack) {
|
|
5386
6036
|
const findings = [];
|
|
5387
6037
|
if (!stack || typeof stack !== "object") return findings;
|
|
5388
|
-
const permissionSets =
|
|
6038
|
+
const permissionSets = asArray34(stack.permissions);
|
|
5389
6039
|
if (permissionSets.length === 0) return findings;
|
|
5390
6040
|
const exposures = collectNavExposures(stack);
|
|
5391
6041
|
if (exposures.length === 0) return findings;
|
|
5392
6042
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
5393
|
-
for (const obj of
|
|
5394
|
-
const n =
|
|
6043
|
+
for (const obj of asArray34(stack.objects)) {
|
|
6044
|
+
const n = strName12(obj.name);
|
|
5395
6045
|
if (n) ownObjects.add(n);
|
|
5396
6046
|
}
|
|
5397
6047
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -5423,15 +6073,15 @@ function validateNavAccess(stack) {
|
|
|
5423
6073
|
var import_system4 = require("@objectstack/spec/system");
|
|
5424
6074
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
5425
6075
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
5426
|
-
function
|
|
6076
|
+
function isRec13(v) {
|
|
5427
6077
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5428
6078
|
}
|
|
5429
|
-
function
|
|
6079
|
+
function asArray35(v) {
|
|
5430
6080
|
if (Array.isArray(v)) return v;
|
|
5431
|
-
if (
|
|
6081
|
+
if (isRec13(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec13(def) ? def : {} }));
|
|
5432
6082
|
return [];
|
|
5433
6083
|
}
|
|
5434
|
-
function
|
|
6084
|
+
function strName13(v) {
|
|
5435
6085
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5436
6086
|
}
|
|
5437
6087
|
function distance5(a, b) {
|
|
@@ -5489,35 +6139,32 @@ function collectViewRecord(view, factsFor) {
|
|
|
5489
6139
|
const addView = (objectName, name) => {
|
|
5490
6140
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
5491
6141
|
};
|
|
5492
|
-
const
|
|
5493
|
-
|
|
5494
|
-
|
|
6142
|
+
const addSections = (container, binding) => {
|
|
6143
|
+
if (!binding) return;
|
|
6144
|
+
for (const section of asArray35(container.sections)) {
|
|
6145
|
+
const sectionName = strName13(section.name);
|
|
6146
|
+
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
6147
|
+
}
|
|
6148
|
+
};
|
|
6149
|
+
const listBinding = isRec13(view.list) ? bindingOf(view.list) : void 0;
|
|
6150
|
+
if (isRec13(view.list)) addView(listBinding, strName13(view.list.name));
|
|
6151
|
+
addView(recordObject ?? listBinding, strName13(view.name));
|
|
5495
6152
|
for (const key of ["listViews", "formViews"]) {
|
|
5496
6153
|
const container = view[key];
|
|
5497
|
-
if (!
|
|
6154
|
+
if (!isRec13(container)) continue;
|
|
5498
6155
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
5499
|
-
if (!
|
|
6156
|
+
if (!isRec13(sub)) continue;
|
|
5500
6157
|
const binding = bindingOf(sub) ?? listBinding;
|
|
5501
6158
|
addView(binding, subKey);
|
|
5502
|
-
addView(binding,
|
|
5503
|
-
|
|
5504
|
-
for (const section of asArray31(sub.sections)) {
|
|
5505
|
-
const sectionName = strName12(section.name);
|
|
5506
|
-
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
5507
|
-
}
|
|
5508
|
-
}
|
|
5509
|
-
}
|
|
5510
|
-
}
|
|
5511
|
-
const sectionBinding = recordObject ?? listBinding;
|
|
5512
|
-
if (sectionBinding) {
|
|
5513
|
-
for (const section of asArray31(view.sections)) {
|
|
5514
|
-
const sectionName = strName12(section.name);
|
|
5515
|
-
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
6159
|
+
addView(binding, strName13(sub.name));
|
|
6160
|
+
addSections(sub, binding);
|
|
5516
6161
|
}
|
|
5517
6162
|
}
|
|
6163
|
+
if (isRec13(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
6164
|
+
addSections(view, recordObject ?? listBinding);
|
|
5518
6165
|
}
|
|
5519
6166
|
function viewObjectName(view) {
|
|
5520
|
-
return
|
|
6167
|
+
return strName13(view.objectName) ?? strName13(view.object) ?? (isRec13(view.data) ? strName13(view.data.object) : void 0);
|
|
5521
6168
|
}
|
|
5522
6169
|
function readOptions(field) {
|
|
5523
6170
|
const raw = field.options;
|
|
@@ -5529,14 +6176,14 @@ function readOptions(field) {
|
|
|
5529
6176
|
values.add(opt);
|
|
5530
6177
|
continue;
|
|
5531
6178
|
}
|
|
5532
|
-
if (!
|
|
5533
|
-
const value =
|
|
6179
|
+
if (!isRec13(opt)) continue;
|
|
6180
|
+
const value = strName13(opt.value);
|
|
5534
6181
|
if (!value) continue;
|
|
5535
6182
|
values.add(value);
|
|
5536
|
-
const label2 =
|
|
6183
|
+
const label2 = strName13(opt.label);
|
|
5537
6184
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
5538
6185
|
}
|
|
5539
|
-
} else if (
|
|
6186
|
+
} else if (isRec13(raw)) {
|
|
5540
6187
|
for (const [value, label2] of Object.entries(raw)) {
|
|
5541
6188
|
values.add(value);
|
|
5542
6189
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -5556,48 +6203,48 @@ function buildUniverse(stack) {
|
|
|
5556
6203
|
}
|
|
5557
6204
|
return facts;
|
|
5558
6205
|
};
|
|
5559
|
-
for (const obj of
|
|
5560
|
-
const objectName =
|
|
6206
|
+
for (const obj of asArray35(stack.objects)) {
|
|
6207
|
+
const objectName = strName13(obj.name);
|
|
5561
6208
|
if (!objectName) continue;
|
|
5562
6209
|
const facts = factsFor(objectName);
|
|
5563
|
-
for (const field of
|
|
5564
|
-
const fieldName =
|
|
6210
|
+
for (const field of asArray35(obj.fields)) {
|
|
6211
|
+
const fieldName = strName13(field.name);
|
|
5565
6212
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
5566
6213
|
}
|
|
5567
|
-
for (const action of
|
|
5568
|
-
const actionName =
|
|
6214
|
+
for (const action of asArray35(obj.actions)) {
|
|
6215
|
+
const actionName = strName13(action.name);
|
|
5569
6216
|
if (actionName) facts.actions.set(actionName, action);
|
|
5570
6217
|
}
|
|
5571
|
-
for (const view of
|
|
5572
|
-
collectViewRecord({ ...view, object:
|
|
6218
|
+
for (const view of asArray35(obj.views)) {
|
|
6219
|
+
collectViewRecord({ ...view, object: strName13(view.object) ?? objectName }, factsFor);
|
|
5573
6220
|
}
|
|
5574
6221
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
5575
|
-
for (const group of
|
|
5576
|
-
const key =
|
|
6222
|
+
for (const group of asArray35(obj.fieldGroups)) {
|
|
6223
|
+
const key = strName13(group.key) ?? strName13(group.name);
|
|
5577
6224
|
if (key) facts.sections.add(key);
|
|
5578
6225
|
}
|
|
5579
6226
|
}
|
|
5580
|
-
for (const view of
|
|
6227
|
+
for (const view of asArray35(stack.views)) {
|
|
5581
6228
|
collectViewRecord(view, factsFor);
|
|
5582
6229
|
}
|
|
5583
|
-
const pages =
|
|
6230
|
+
const pages = asArray35(stack.pages);
|
|
5584
6231
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5585
6232
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
5586
6233
|
if (!walked.objectName) continue;
|
|
5587
|
-
const props =
|
|
6234
|
+
const props = isRec13(walked.component.properties) ? walked.component.properties : void 0;
|
|
5588
6235
|
if (!props) continue;
|
|
5589
|
-
for (const section of
|
|
5590
|
-
const sectionName =
|
|
6236
|
+
for (const section of asArray35(props.sections)) {
|
|
6237
|
+
const sectionName = strName13(section.name);
|
|
5591
6238
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
5592
6239
|
}
|
|
5593
6240
|
}
|
|
5594
6241
|
}
|
|
5595
6242
|
const globalActions = /* @__PURE__ */ new Map();
|
|
5596
6243
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
5597
|
-
for (const action of
|
|
5598
|
-
const actionName =
|
|
6244
|
+
for (const action of asArray35(stack.actions)) {
|
|
6245
|
+
const actionName = strName13(action.name);
|
|
5599
6246
|
if (!actionName) continue;
|
|
5600
|
-
const owner =
|
|
6247
|
+
const owner = strName13(action.objectName) ?? strName13(action.object);
|
|
5601
6248
|
if (owner) {
|
|
5602
6249
|
factsFor(owner).actions.set(actionName, action);
|
|
5603
6250
|
actionOwners.set(actionName, owner);
|
|
@@ -5611,41 +6258,41 @@ function buildUniverse(stack) {
|
|
|
5611
6258
|
}
|
|
5612
6259
|
}
|
|
5613
6260
|
const apps = /* @__PURE__ */ new Map();
|
|
5614
|
-
for (const app of
|
|
5615
|
-
const appName =
|
|
6261
|
+
for (const app of asArray35(stack.apps)) {
|
|
6262
|
+
const appName = strName13(app.name);
|
|
5616
6263
|
if (!appName) continue;
|
|
5617
6264
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
5618
6265
|
const walkNav = (items) => {
|
|
5619
|
-
for (const item of
|
|
5620
|
-
const id =
|
|
6266
|
+
for (const item of asArray35(items)) {
|
|
6267
|
+
const id = strName13(item.id);
|
|
5621
6268
|
if (id) navIds.add(id);
|
|
5622
6269
|
if (item.children) walkNav(item.children);
|
|
5623
6270
|
}
|
|
5624
6271
|
};
|
|
5625
6272
|
walkNav(app.navigation);
|
|
5626
|
-
for (const area of
|
|
5627
|
-
const areaId =
|
|
6273
|
+
for (const area of asArray35(app.areas)) {
|
|
6274
|
+
const areaId = strName13(area.id);
|
|
5628
6275
|
if (areaId) navIds.add(areaId);
|
|
5629
6276
|
walkNav(area.navigation);
|
|
5630
6277
|
}
|
|
5631
6278
|
apps.set(appName, navIds);
|
|
5632
6279
|
}
|
|
5633
6280
|
const dashboards = /* @__PURE__ */ new Map();
|
|
5634
|
-
for (const dash of
|
|
5635
|
-
const dashName =
|
|
6281
|
+
for (const dash of asArray35(stack.dashboards)) {
|
|
6282
|
+
const dashName = strName13(dash.name);
|
|
5636
6283
|
if (!dashName) continue;
|
|
5637
6284
|
const widgets = /* @__PURE__ */ new Set();
|
|
5638
|
-
for (const widget of
|
|
5639
|
-
const id =
|
|
6285
|
+
for (const widget of asArray35(dash.widgets)) {
|
|
6286
|
+
const id = strName13(widget.id) ?? strName13(widget.name);
|
|
5640
6287
|
if (id) widgets.add(id);
|
|
5641
6288
|
}
|
|
5642
6289
|
const actions = /* @__PURE__ */ new Set();
|
|
5643
6290
|
const headerActions = [
|
|
5644
|
-
...
|
|
5645
|
-
...
|
|
6291
|
+
...asArray35(isRec13(dash.header) ? dash.header.actions : void 0),
|
|
6292
|
+
...asArray35(dash.actions)
|
|
5646
6293
|
];
|
|
5647
6294
|
for (const action of headerActions) {
|
|
5648
|
-
const key =
|
|
6295
|
+
const key = strName13(action.actionUrl) ?? strName13(action.url) ?? strName13(action.name);
|
|
5649
6296
|
if (key) actions.add(key);
|
|
5650
6297
|
}
|
|
5651
6298
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -5657,7 +6304,7 @@ function localePath(bundleIndex, locale) {
|
|
|
5657
6304
|
}
|
|
5658
6305
|
function validateTranslationReferences(stack) {
|
|
5659
6306
|
const findings = [];
|
|
5660
|
-
if (!
|
|
6307
|
+
if (!isRec13(stack)) return findings;
|
|
5661
6308
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
5662
6309
|
if (bundles.length === 0) return findings;
|
|
5663
6310
|
const universe = buildUniverse(stack);
|
|
@@ -5666,13 +6313,13 @@ function validateTranslationReferences(stack) {
|
|
|
5666
6313
|
};
|
|
5667
6314
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
5668
6315
|
const bundle = bundles[bi];
|
|
5669
|
-
if (!
|
|
6316
|
+
if (!isRec13(bundle)) continue;
|
|
5670
6317
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
5671
|
-
if (!
|
|
6318
|
+
if (!isRec13(rawData)) continue;
|
|
5672
6319
|
const base = localePath(bi, locale);
|
|
5673
6320
|
const inLocale = `locale "${locale}"`;
|
|
5674
6321
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
5675
|
-
if (!
|
|
6322
|
+
if (!isRec13(rawNode)) continue;
|
|
5676
6323
|
const objPath = `${base}.objects.${objectName}`;
|
|
5677
6324
|
const facts = universe.objects.get(objectName);
|
|
5678
6325
|
if (!facts) {
|
|
@@ -5698,7 +6345,7 @@ function validateTranslationReferences(stack) {
|
|
|
5698
6345
|
);
|
|
5699
6346
|
continue;
|
|
5700
6347
|
}
|
|
5701
|
-
if (!
|
|
6348
|
+
if (!isRec13(rawField)) continue;
|
|
5702
6349
|
checkOptionKeys(findings, {
|
|
5703
6350
|
optionMap: rawField.options,
|
|
5704
6351
|
field,
|
|
@@ -5780,7 +6427,7 @@ function validateTranslationReferences(stack) {
|
|
|
5780
6427
|
);
|
|
5781
6428
|
continue;
|
|
5782
6429
|
}
|
|
5783
|
-
if (!
|
|
6430
|
+
if (!isRec13(rawApp)) continue;
|
|
5784
6431
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
5785
6432
|
if (navIds.has(navId)) continue;
|
|
5786
6433
|
orphan(
|
|
@@ -5803,7 +6450,7 @@ function validateTranslationReferences(stack) {
|
|
|
5803
6450
|
);
|
|
5804
6451
|
continue;
|
|
5805
6452
|
}
|
|
5806
|
-
if (!
|
|
6453
|
+
if (!isRec13(rawDash)) continue;
|
|
5807
6454
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
5808
6455
|
if (dash.widgets.has(widgetId)) continue;
|
|
5809
6456
|
orphan(
|
|
@@ -5828,7 +6475,7 @@ function validateTranslationReferences(stack) {
|
|
|
5828
6475
|
return findings;
|
|
5829
6476
|
}
|
|
5830
6477
|
function asRecord(v) {
|
|
5831
|
-
return
|
|
6478
|
+
return isRec13(v) ? v : {};
|
|
5832
6479
|
}
|
|
5833
6480
|
function checkOptionKeys(findings, ctx) {
|
|
5834
6481
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -5840,7 +6487,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5840
6487
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
5841
6488
|
where: ctx.where,
|
|
5842
6489
|
path: ctx.path,
|
|
5843
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
6490
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName13(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
5844
6491
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
5845
6492
|
});
|
|
5846
6493
|
return;
|
|
@@ -5859,11 +6506,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
5859
6506
|
}
|
|
5860
6507
|
}
|
|
5861
6508
|
function checkActionParams(findings, ctx) {
|
|
5862
|
-
const rawParams = Object.keys(asRecord(
|
|
6509
|
+
const rawParams = Object.keys(asRecord(isRec13(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
5863
6510
|
if (rawParams.length === 0) return;
|
|
5864
6511
|
const declared = /* @__PURE__ */ new Set();
|
|
5865
|
-
for (const param of
|
|
5866
|
-
const name =
|
|
6512
|
+
for (const param of asArray35(ctx.action.params)) {
|
|
6513
|
+
const name = strName13(param.name) ?? strName13(param.field);
|
|
5867
6514
|
if (name) declared.add(name);
|
|
5868
6515
|
}
|
|
5869
6516
|
for (const paramName of rawParams) {
|
|
@@ -5879,16 +6526,161 @@ function checkActionParams(findings, ctx) {
|
|
|
5879
6526
|
}
|
|
5880
6527
|
}
|
|
5881
6528
|
|
|
6529
|
+
// src/validate-translatable-sections.ts
|
|
6530
|
+
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
6531
|
+
function isRec14(v) {
|
|
6532
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6533
|
+
}
|
|
6534
|
+
function strName14(v) {
|
|
6535
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6536
|
+
}
|
|
6537
|
+
function viewObjectName2(view) {
|
|
6538
|
+
return strName14(view.objectName) ?? strName14(view.object) ?? (isRec14(view.data) ? strName14(view.data.object) : void 0);
|
|
6539
|
+
}
|
|
6540
|
+
function collectionEntries(v, base) {
|
|
6541
|
+
if (Array.isArray(v)) {
|
|
6542
|
+
const out = [];
|
|
6543
|
+
for (let i = 0; i < v.length; i++) {
|
|
6544
|
+
if (isRec14(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
6545
|
+
}
|
|
6546
|
+
return out;
|
|
6547
|
+
}
|
|
6548
|
+
if (isRec14(v)) {
|
|
6549
|
+
return Object.entries(v).filter(([, def]) => isRec14(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
6550
|
+
}
|
|
6551
|
+
return [];
|
|
6552
|
+
}
|
|
6553
|
+
function viewLabel(view) {
|
|
6554
|
+
const name = strName14(view.name);
|
|
6555
|
+
return name ? `view "${name}"` : "";
|
|
6556
|
+
}
|
|
6557
|
+
function joinWhere(...parts) {
|
|
6558
|
+
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
6559
|
+
}
|
|
6560
|
+
function collectViewSites(view, basePath, label2, sites) {
|
|
6561
|
+
const recordObject = viewObjectName2(view);
|
|
6562
|
+
const listBinding = isRec14(view.list) ? viewObjectName2(view.list) ?? recordObject : void 0;
|
|
6563
|
+
const bindingOf = (container) => viewObjectName2(container) ?? recordObject;
|
|
6564
|
+
sites.push({
|
|
6565
|
+
path: `${basePath}.sections`,
|
|
6566
|
+
surface: label2,
|
|
6567
|
+
objectName: recordObject ?? listBinding,
|
|
6568
|
+
sections: view.sections
|
|
6569
|
+
});
|
|
6570
|
+
if (isRec14(view.form)) {
|
|
6571
|
+
sites.push({
|
|
6572
|
+
path: `${basePath}.form.sections`,
|
|
6573
|
+
surface: joinWhere(label2, "form"),
|
|
6574
|
+
objectName: bindingOf(view.form) ?? listBinding,
|
|
6575
|
+
sections: view.form.sections
|
|
6576
|
+
});
|
|
6577
|
+
}
|
|
6578
|
+
for (const key of ["listViews", "formViews"]) {
|
|
6579
|
+
const container = view[key];
|
|
6580
|
+
if (!isRec14(container)) continue;
|
|
6581
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
6582
|
+
if (!isRec14(sub)) continue;
|
|
6583
|
+
sites.push({
|
|
6584
|
+
path: `${basePath}.${key}.${subKey}.sections`,
|
|
6585
|
+
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
6586
|
+
objectName: bindingOf(sub) ?? listBinding,
|
|
6587
|
+
sections: sub.sections
|
|
6588
|
+
});
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
function translatedObjectNames(stack) {
|
|
6593
|
+
const out = /* @__PURE__ */ new Set();
|
|
6594
|
+
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6595
|
+
for (const bundle of bundles) {
|
|
6596
|
+
if (!isRec14(bundle)) continue;
|
|
6597
|
+
for (const data of Object.values(bundle)) {
|
|
6598
|
+
if (!isRec14(data) || !isRec14(data.objects)) continue;
|
|
6599
|
+
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
6600
|
+
if (isRec14(node)) out.add(objectName);
|
|
6601
|
+
}
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
return out;
|
|
6605
|
+
}
|
|
6606
|
+
function suggestedName(label2) {
|
|
6607
|
+
const slug = label2.toLowerCase().replace(/&/g, " and ").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
6608
|
+
return slug.length > 0 ? slug : void 0;
|
|
6609
|
+
}
|
|
6610
|
+
function validateTranslatableSections(stack) {
|
|
6611
|
+
const findings = [];
|
|
6612
|
+
if (!isRec14(stack)) return findings;
|
|
6613
|
+
const translated = translatedObjectNames(stack);
|
|
6614
|
+
if (translated.size === 0) return findings;
|
|
6615
|
+
const sites = [];
|
|
6616
|
+
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
6617
|
+
const objectName = strName14(obj.name);
|
|
6618
|
+
if (!objectName) continue;
|
|
6619
|
+
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
6620
|
+
collectViewSites(
|
|
6621
|
+
{ ...view, object: strName14(view.object) ?? objectName },
|
|
6622
|
+
path,
|
|
6623
|
+
viewLabel(view),
|
|
6624
|
+
sites
|
|
6625
|
+
);
|
|
6626
|
+
}
|
|
6627
|
+
if (isRec14(obj.listViews)) {
|
|
6628
|
+
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
6629
|
+
}
|
|
6630
|
+
}
|
|
6631
|
+
for (const { rec: view, path } of collectionEntries(stack.views, "views")) {
|
|
6632
|
+
collectViewSites(view, path, viewLabel(view), sites);
|
|
6633
|
+
}
|
|
6634
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6635
|
+
const pageName = strName14(page.name);
|
|
6636
|
+
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
6637
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6638
|
+
if (!walked.objectName) continue;
|
|
6639
|
+
const props = isRec14(walked.component.properties) ? walked.component.properties : void 0;
|
|
6640
|
+
if (!props) continue;
|
|
6641
|
+
const type = strName14(walked.component.type) ?? "component";
|
|
6642
|
+
sites.push({
|
|
6643
|
+
path: `${walked.path}.properties.sections`,
|
|
6644
|
+
surface: joinWhere(pageLabel, type),
|
|
6645
|
+
objectName: walked.objectName,
|
|
6646
|
+
sections: props.sections
|
|
6647
|
+
});
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
for (const site of sites) {
|
|
6651
|
+
const objectName = site.objectName;
|
|
6652
|
+
if (!objectName || !translated.has(objectName)) continue;
|
|
6653
|
+
if (!Array.isArray(site.sections)) continue;
|
|
6654
|
+
for (let i = 0; i < site.sections.length; i++) {
|
|
6655
|
+
const section = site.sections[i];
|
|
6656
|
+
if (!isRec14(section)) continue;
|
|
6657
|
+
if (strName14(section.name)) continue;
|
|
6658
|
+
const heading = strName14(section.label) ?? strName14(section.title);
|
|
6659
|
+
if (!heading) continue;
|
|
6660
|
+
const slug = suggestedName(heading);
|
|
6661
|
+
findings.push({
|
|
6662
|
+
severity: "warning",
|
|
6663
|
+
rule: TRANSLATION_SECTION_NAME_MISSING,
|
|
6664
|
+
where: joinWhere(`object "${objectName}"`, site.surface, `section "${heading}"`),
|
|
6665
|
+
path: `${site.path}[${i}]`,
|
|
6666
|
+
message: `Section "${heading}" declares a label but no \`name\`. Headings resolve through \`objects.${objectName}._sections.<name>.label\`, so a section with no name has no key a bundle can carry \u2014 this heading can never be translated and renders in the source locale in EVERY locale. Object "${objectName}" IS translated, which is what makes the hole invisible: every neighbouring label resolves and only the heading stays behind. The i18n coverage report cannot see it either \u2014 it walks \`sections[].name\`, and a nameless section contributes nothing to walk.`,
|
|
6667
|
+
hint: `Give the section a stable \`name\` (snake_case)` + (slug ? `, e.g. \`name: '${slug}'\`` : "") + `, then translate it as \`objects.${objectName}._sections.${slug ?? "<name>"}.label\` in each locale bundle. The renderers look the heading up by name only \u2014 the name above is a suggestion to write down, never a key derived from the label, so renaming the heading later cannot break the lookup.`
|
|
6668
|
+
});
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
return findings;
|
|
6672
|
+
}
|
|
6673
|
+
|
|
5882
6674
|
// src/validate-ai-surface-affinity.ts
|
|
5883
6675
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
5884
|
-
function
|
|
6676
|
+
function asArray36(v) {
|
|
5885
6677
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5886
6678
|
if (v && typeof v === "object") {
|
|
5887
6679
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5888
6680
|
}
|
|
5889
6681
|
return [];
|
|
5890
6682
|
}
|
|
5891
|
-
function
|
|
6683
|
+
function strName15(v) {
|
|
5892
6684
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5893
6685
|
}
|
|
5894
6686
|
function surfaceOf(v) {
|
|
@@ -5898,18 +6690,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5898
6690
|
const findings = [];
|
|
5899
6691
|
if (!stack || typeof stack !== "object") return findings;
|
|
5900
6692
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
5901
|
-
for (const skill of
|
|
5902
|
-
const n =
|
|
6693
|
+
for (const skill of asArray36(stack.skills)) {
|
|
6694
|
+
const n = strName15(skill.name);
|
|
5903
6695
|
if (n) skillsByName.set(n, skill);
|
|
5904
6696
|
}
|
|
5905
|
-
const agents =
|
|
6697
|
+
const agents = asArray36(stack.agents);
|
|
5906
6698
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
5907
6699
|
const agent = agents[ai];
|
|
5908
|
-
const agentName =
|
|
6700
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
5909
6701
|
const agentSurface = surfaceOf(agent.surface);
|
|
5910
6702
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
5911
6703
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
5912
|
-
const ref =
|
|
6704
|
+
const ref = strName15(skillRefs[si]);
|
|
5913
6705
|
if (!ref) continue;
|
|
5914
6706
|
const skill = skillsByName.get(ref);
|
|
5915
6707
|
if (!skill) continue;
|
|
@@ -5931,14 +6723,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
5931
6723
|
// src/validate-ai-tool-references.ts
|
|
5932
6724
|
var import_system5 = require("@objectstack/spec/system");
|
|
5933
6725
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
5934
|
-
function
|
|
6726
|
+
function asArray37(v) {
|
|
5935
6727
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
5936
6728
|
if (v && typeof v === "object") {
|
|
5937
6729
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5938
6730
|
}
|
|
5939
6731
|
return [];
|
|
5940
6732
|
}
|
|
5941
|
-
function
|
|
6733
|
+
function strName16(v) {
|
|
5942
6734
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5943
6735
|
}
|
|
5944
6736
|
function distance6(a, b) {
|
|
@@ -5979,26 +6771,26 @@ function materialisesAsTool(action) {
|
|
|
5979
6771
|
if (!ai || typeof ai !== "object") return false;
|
|
5980
6772
|
const aiRec = ai;
|
|
5981
6773
|
if (aiRec.exposed !== true) return false;
|
|
5982
|
-
if (!
|
|
5983
|
-
const type =
|
|
6774
|
+
if (!strName16(aiRec.description)) return false;
|
|
6775
|
+
const type = strName16(action.type);
|
|
5984
6776
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
5985
6777
|
if (type === "script") return Boolean(action.target || action.body);
|
|
5986
6778
|
return Boolean(action.target);
|
|
5987
6779
|
}
|
|
5988
6780
|
function collectToolUniverse(stack) {
|
|
5989
6781
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
5990
|
-
for (const tool of
|
|
5991
|
-
const n =
|
|
6782
|
+
for (const tool of asArray37(stack.tools)) {
|
|
6783
|
+
const n = strName16(tool.name);
|
|
5992
6784
|
if (n) universe.add(n);
|
|
5993
6785
|
}
|
|
5994
6786
|
const addActionFamily = (actions) => {
|
|
5995
|
-
for (const action of
|
|
5996
|
-
const n =
|
|
6787
|
+
for (const action of asArray37(actions)) {
|
|
6788
|
+
const n = strName16(action.name);
|
|
5997
6789
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
5998
6790
|
}
|
|
5999
6791
|
};
|
|
6000
6792
|
addActionFamily(stack.actions);
|
|
6001
|
-
for (const obj of
|
|
6793
|
+
for (const obj of asArray37(stack.objects)) {
|
|
6002
6794
|
addActionFamily(obj.actions);
|
|
6003
6795
|
}
|
|
6004
6796
|
return universe;
|
|
@@ -6006,13 +6798,13 @@ function collectToolUniverse(stack) {
|
|
|
6006
6798
|
function collectUnexposedActionNames(stack) {
|
|
6007
6799
|
const names = /* @__PURE__ */ new Set();
|
|
6008
6800
|
const scan = (actions) => {
|
|
6009
|
-
for (const action of
|
|
6010
|
-
const n =
|
|
6801
|
+
for (const action of asArray37(actions)) {
|
|
6802
|
+
const n = strName16(action.name);
|
|
6011
6803
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
6012
6804
|
}
|
|
6013
6805
|
};
|
|
6014
6806
|
scan(stack.actions);
|
|
6015
|
-
for (const obj of
|
|
6807
|
+
for (const obj of asArray37(stack.objects)) scan(obj.actions);
|
|
6016
6808
|
return names;
|
|
6017
6809
|
}
|
|
6018
6810
|
function validateAiToolReferences(stack) {
|
|
@@ -6030,13 +6822,13 @@ function validateAiToolReferences(stack) {
|
|
|
6030
6822
|
}
|
|
6031
6823
|
return universe.has(ref);
|
|
6032
6824
|
};
|
|
6033
|
-
const skills =
|
|
6825
|
+
const skills = asArray37(stack.skills);
|
|
6034
6826
|
for (let si = 0; si < skills.length; si++) {
|
|
6035
6827
|
const skill = skills[si];
|
|
6036
|
-
const skillName =
|
|
6828
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
6037
6829
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
6038
6830
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
6039
|
-
const ref =
|
|
6831
|
+
const ref = strName16(refs[ti]);
|
|
6040
6832
|
if (!ref || resolves(ref)) continue;
|
|
6041
6833
|
const isPattern = ref.endsWith("*");
|
|
6042
6834
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -6055,24 +6847,24 @@ function validateAiToolReferences(stack) {
|
|
|
6055
6847
|
|
|
6056
6848
|
// src/validate-ai-agent-authoring.ts
|
|
6057
6849
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
6058
|
-
function
|
|
6850
|
+
function asArray38(v) {
|
|
6059
6851
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6060
6852
|
if (v && typeof v === "object") {
|
|
6061
6853
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6062
6854
|
}
|
|
6063
6855
|
return [];
|
|
6064
6856
|
}
|
|
6065
|
-
function
|
|
6857
|
+
function strName17(v) {
|
|
6066
6858
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6067
6859
|
}
|
|
6068
6860
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
6069
6861
|
function validateAiAgentAuthoring(stack) {
|
|
6070
6862
|
const findings = [];
|
|
6071
6863
|
if (!stack || typeof stack !== "object") return findings;
|
|
6072
|
-
const agents =
|
|
6864
|
+
const agents = asArray38(stack.agents);
|
|
6073
6865
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
6074
6866
|
const agent = agents[ai];
|
|
6075
|
-
const name =
|
|
6867
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
6076
6868
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
6077
6869
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
6078
6870
|
findings.push({
|
|
@@ -6088,15 +6880,15 @@ function validateAiAgentAuthoring(stack) {
|
|
|
6088
6880
|
}
|
|
6089
6881
|
|
|
6090
6882
|
// src/validate-hook-body-writes.ts
|
|
6091
|
-
var
|
|
6883
|
+
var import_node_module4 = require("module");
|
|
6092
6884
|
var import_shared = require("@objectstack/spec/shared");
|
|
6093
|
-
var
|
|
6885
|
+
var import_meta4 = {};
|
|
6094
6886
|
var cachedTs2 = null;
|
|
6095
6887
|
function loadTypeScript2() {
|
|
6096
6888
|
if (cachedTs2) return cachedTs2;
|
|
6097
|
-
const anchor = typeof
|
|
6889
|
+
const anchor = typeof import_meta4 !== "undefined" && import_meta4.url ? import_meta4.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
6098
6890
|
try {
|
|
6099
|
-
cachedTs2 = (0,
|
|
6891
|
+
cachedTs2 = (0, import_node_module4.createRequire)(anchor)("typescript");
|
|
6100
6892
|
} catch (err) {
|
|
6101
6893
|
throw new Error(
|
|
6102
6894
|
`@objectstack/lint: checking an L2 (language:'js') hook body requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a hook with a JS body is validated.`
|
|
@@ -6177,24 +6969,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
6177
6969
|
"owner",
|
|
6178
6970
|
"record_type"
|
|
6179
6971
|
]);
|
|
6180
|
-
var
|
|
6181
|
-
function
|
|
6182
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6183
|
-
if (
|
|
6972
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6973
|
+
function asArray39(v) {
|
|
6974
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
6975
|
+
if (isRec15(v)) {
|
|
6184
6976
|
return Object.entries(v).map(([name, def]) => ({
|
|
6185
6977
|
name,
|
|
6186
|
-
...
|
|
6978
|
+
...isRec15(def) ? def : {}
|
|
6187
6979
|
}));
|
|
6188
6980
|
}
|
|
6189
6981
|
return [];
|
|
6190
6982
|
}
|
|
6191
6983
|
function indexObjectFields2(stack) {
|
|
6192
6984
|
const out = /* @__PURE__ */ new Map();
|
|
6193
|
-
for (const obj of
|
|
6985
|
+
for (const obj of asArray39(stack.objects)) {
|
|
6194
6986
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6195
6987
|
if (!name) continue;
|
|
6196
6988
|
const names = /* @__PURE__ */ new Set();
|
|
6197
|
-
for (const f of
|
|
6989
|
+
for (const f of asArray39(obj.fields)) {
|
|
6198
6990
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
6199
6991
|
}
|
|
6200
6992
|
out.set(name, names);
|
|
@@ -6324,12 +7116,12 @@ ${source}
|
|
|
6324
7116
|
}
|
|
6325
7117
|
function validateHookBodyWrites(stack) {
|
|
6326
7118
|
const findings = [];
|
|
6327
|
-
const hooks =
|
|
7119
|
+
const hooks = asArray39(stack.hooks);
|
|
6328
7120
|
if (hooks.length === 0) return findings;
|
|
6329
7121
|
let objectFields = null;
|
|
6330
7122
|
hooks.forEach((hook, hookIndex) => {
|
|
6331
7123
|
const body = hook.body;
|
|
6332
|
-
if (!
|
|
7124
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
6333
7125
|
const source = body.source;
|
|
6334
7126
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
6335
7127
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -6410,13 +7202,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
6410
7202
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
6411
7203
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
6412
7204
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
6413
|
-
var
|
|
6414
|
-
function
|
|
6415
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6416
|
-
if (
|
|
7205
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7206
|
+
function asArray40(v) {
|
|
7207
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
7208
|
+
if (isRec16(v)) {
|
|
6417
7209
|
return Object.entries(v).map(([name, def]) => ({
|
|
6418
7210
|
name,
|
|
6419
|
-
...
|
|
7211
|
+
...isRec16(def) ? def : {}
|
|
6420
7212
|
}));
|
|
6421
7213
|
}
|
|
6422
7214
|
return [];
|
|
@@ -6430,11 +7222,11 @@ function collectActionBodies(stack) {
|
|
|
6430
7222
|
const sites = [];
|
|
6431
7223
|
const seen = /* @__PURE__ */ new Set();
|
|
6432
7224
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
6433
|
-
|
|
7225
|
+
asArray40(actions).forEach((action, index) => {
|
|
6434
7226
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
6435
7227
|
if (type !== "script") return;
|
|
6436
7228
|
const body = action.body;
|
|
6437
|
-
if (!
|
|
7229
|
+
if (!isRec16(body) || body.language !== "js") return;
|
|
6438
7230
|
const source = body.source;
|
|
6439
7231
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
6440
7232
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -6445,7 +7237,7 @@ function collectActionBodies(stack) {
|
|
|
6445
7237
|
});
|
|
6446
7238
|
};
|
|
6447
7239
|
collect(stack.actions, "actions");
|
|
6448
|
-
|
|
7240
|
+
asArray40(stack.objects).forEach((obj, objIndex) => {
|
|
6449
7241
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
6450
7242
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
6451
7243
|
});
|
|
@@ -6453,7 +7245,7 @@ function collectActionBodies(stack) {
|
|
|
6453
7245
|
}
|
|
6454
7246
|
function validateActionBodyWrites(stack) {
|
|
6455
7247
|
const findings = [];
|
|
6456
|
-
if (!
|
|
7248
|
+
if (!isRec16(stack)) return findings;
|
|
6457
7249
|
const sites = collectActionBodies(stack);
|
|
6458
7250
|
if (sites.length === 0) return findings;
|
|
6459
7251
|
let objectFields = null;
|
|
@@ -6512,13 +7304,13 @@ var import_shared3 = require("@objectstack/spec/shared");
|
|
|
6512
7304
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
6513
7305
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
6514
7306
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
6515
|
-
var
|
|
6516
|
-
function
|
|
6517
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6518
|
-
if (
|
|
7307
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7308
|
+
function asArray41(v) {
|
|
7309
|
+
if (Array.isArray(v)) return v.filter((x) => isRec17(x));
|
|
7310
|
+
if (isRec17(v)) {
|
|
6519
7311
|
return Object.entries(v).map(([name, def]) => ({
|
|
6520
7312
|
name,
|
|
6521
|
-
...
|
|
7313
|
+
...isRec17(def) ? def : {}
|
|
6522
7314
|
}));
|
|
6523
7315
|
}
|
|
6524
7316
|
return [];
|
|
@@ -6531,8 +7323,8 @@ function readLiteralObjectName2(config) {
|
|
|
6531
7323
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
6532
7324
|
function validateFlowNodeWrites(stack) {
|
|
6533
7325
|
const findings = [];
|
|
6534
|
-
if (!
|
|
6535
|
-
const flows =
|
|
7326
|
+
if (!isRec17(stack)) return findings;
|
|
7327
|
+
const flows = asArray41(stack.flows);
|
|
6536
7328
|
if (flows.length === 0) return findings;
|
|
6537
7329
|
let objectFields = null;
|
|
6538
7330
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -6540,10 +7332,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
6540
7332
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
6541
7333
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
6542
7334
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
6543
|
-
const config =
|
|
7335
|
+
const config = isRec17(node.config) ? node.config : void 0;
|
|
6544
7336
|
if (!config) return;
|
|
6545
7337
|
const fields = config.fields;
|
|
6546
|
-
if (!
|
|
7338
|
+
if (!isRec17(fields)) return;
|
|
6547
7339
|
const written = Object.keys(fields);
|
|
6548
7340
|
if (written.length === 0) return;
|
|
6549
7341
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -6590,6 +7382,14 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
6590
7382
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
6591
7383
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
6592
7384
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
7385
|
+
// The same family from the other end (#5417). Its sibling above asks "does
|
|
7386
|
+
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
7387
|
+
// section authored with a `label` and no `name` renders a heading that
|
|
7388
|
+
// `_sections` (keyed by name) can never address, so neither the orphan check
|
|
7389
|
+
// nor the coverage walk can see it. A reference that cannot be written is
|
|
7390
|
+
// still a reference question, and warning-only for the same reason its
|
|
7391
|
+
// sibling is: one heading stays in the source locale, nothing breaks.
|
|
7392
|
+
{ name: "validateTranslatableSections", run: validateTranslatableSections },
|
|
6593
7393
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
6594
7394
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
6595
7395
|
{ name: "validateAiToolReferences", run: validateAiToolReferences },
|
|
@@ -6675,7 +7475,8 @@ function validateReferenceIntegrity(stack) {
|
|
|
6675
7475
|
}
|
|
6676
7476
|
|
|
6677
7477
|
// src/lint-flow-patterns.ts
|
|
6678
|
-
|
|
7478
|
+
var import_automation5 = require("@objectstack/spec/automation");
|
|
7479
|
+
function asArray42(v) {
|
|
6679
7480
|
if (Array.isArray(v)) return v;
|
|
6680
7481
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6681
7482
|
return [];
|
|
@@ -6698,6 +7499,7 @@ var FLOW_BARE_DOLLAR_REF = "flow-bare-dollar-reference";
|
|
|
6698
7499
|
var FLOW_APPROVAL_REVISE_DEAD_END = "flow-approval-revise-dead-end";
|
|
6699
7500
|
var FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE = "flow-approval-revise-unmarked-backedge";
|
|
6700
7501
|
var FLOW_APPROVAL_REVISE_DISABLED = "flow-approval-revise-disabled";
|
|
7502
|
+
var FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED = "flow-approval-revise-target-not-service-owned";
|
|
6701
7503
|
var FLOW_RUNAS_UNSCOPED = "flow-runas-unscoped";
|
|
6702
7504
|
var FLOW_ERROR_LABEL_NOT_FAULT = "flow-error-label-not-fault";
|
|
6703
7505
|
var FLOW_BRANCH_LABEL_UNMATCHED = "flow-branch-label-unmatched";
|
|
@@ -6705,6 +7507,7 @@ var FLOW_DECISION_UNCONDITIONAL_BRANCH = "flow-decision-unconditional-branch";
|
|
|
6705
7507
|
var FLOW_DEFAULT_EDGE_WITH_CONDITION = "flow-default-edge-with-condition";
|
|
6706
7508
|
var FLOW_MULTIPLE_DEFAULT_EDGES = "flow-multiple-default-edges";
|
|
6707
7509
|
var FLOW_INERT_NODE_CONDITION = "flow-inert-node-condition";
|
|
7510
|
+
var FLOW_MULTI_WRITE_UNFILTERED = "flow-multi-write-unfiltered";
|
|
6708
7511
|
var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
6709
7512
|
"decision",
|
|
6710
7513
|
"assignment",
|
|
@@ -6727,6 +7530,36 @@ var INERT_CONDITION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
6727
7530
|
"end"
|
|
6728
7531
|
]);
|
|
6729
7532
|
var DATA_NODE_TYPES = /* @__PURE__ */ new Set(["get_record", "create_record", "update_record", "delete_record"]);
|
|
7533
|
+
var RUNAS_EFFECTIVE_IDENTITY = "`runAs:'user'` (the default when none is declared)";
|
|
7534
|
+
function findDataNodeAnywhere(nodes, edges) {
|
|
7535
|
+
for (const graph of (0, import_automation5.collectFlowGraphs)({
|
|
7536
|
+
nodes,
|
|
7537
|
+
edges
|
|
7538
|
+
})) {
|
|
7539
|
+
for (const node of graph.nodes) {
|
|
7540
|
+
if (DATA_NODE_TYPES.has(typeof node.type === "string" ? node.type : "")) {
|
|
7541
|
+
return { node, scope: graph.scope };
|
|
7542
|
+
}
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
return null;
|
|
7546
|
+
}
|
|
7547
|
+
var BULK_WRITE_CONSEQUENCE = /* @__PURE__ */ new Map([
|
|
7548
|
+
["delete_record", {
|
|
7549
|
+
verb: "deleted",
|
|
7550
|
+
engineCall: "driver.deleteMany",
|
|
7551
|
+
// The delete dispatch is the one that is EXTRACTED and case-set-pinned
|
|
7552
|
+
// (`engine-delete-dispatch.ts`), so it can be cited by name.
|
|
7553
|
+
dispatchNote: "the engine's delete-dispatch case-set lists `multi with no predicate at all` as a legal `multi` call"
|
|
7554
|
+
}],
|
|
7555
|
+
["update_record", {
|
|
7556
|
+
verb: "overwritten",
|
|
7557
|
+
engineCall: "driver.updateMany",
|
|
7558
|
+
// Update has no extracted dispatch module, so the branch itself is the
|
|
7559
|
+
// authority — and its refusal fires only WITHOUT the declaration.
|
|
7560
|
+
dispatchNote: "the engine takes its bulk branch on `options.multi` alone (`Update requires an ID or options.multi=true` is refused only when the declaration is absent)"
|
|
7561
|
+
}]
|
|
7562
|
+
]);
|
|
6730
7563
|
var ERROR_LABELS = /* @__PURE__ */ new Set(["error", "fault", "failure", "failed", "catch", "on_error", "onerror", "on error"]);
|
|
6731
7564
|
var BRANCH_LABEL_NODE_TYPES = /* @__PURE__ */ new Set(["decision", "approval", "screen", "try_catch"]);
|
|
6732
7565
|
function isScheduleTriggered(flow, startCfg) {
|
|
@@ -6811,7 +7644,7 @@ function collectTemplateStrings(value, key, out) {
|
|
|
6811
7644
|
function edgeLabelOf(e) {
|
|
6812
7645
|
return typeof e.label === "string" ? e.label.trim().toLowerCase() : "";
|
|
6813
7646
|
}
|
|
6814
|
-
function scanErrorLabelledEdges(
|
|
7647
|
+
function scanErrorLabelledEdges(at, nodes, edges, findings) {
|
|
6815
7648
|
const typeById = /* @__PURE__ */ new Map();
|
|
6816
7649
|
for (const n of nodes) {
|
|
6817
7650
|
if (typeof n.id === "string") typeById.set(n.id, typeof n.type === "string" ? n.type : "");
|
|
@@ -6824,14 +7657,14 @@ function scanErrorLabelledEdges(flowName, nodes, edges, findings) {
|
|
|
6824
7657
|
const src = typeof e.source === "string" ? e.source : "";
|
|
6825
7658
|
if (BRANCH_LABEL_NODE_TYPES.has(typeById.get(src) ?? "")) continue;
|
|
6826
7659
|
findings.push({
|
|
6827
|
-
where:
|
|
7660
|
+
where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6828
7661
|
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
7662
|
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
7663
|
rule: FLOW_ERROR_LABEL_NOT_FAULT
|
|
6831
7664
|
});
|
|
6832
7665
|
}
|
|
6833
7666
|
}
|
|
6834
|
-
function scanBranchRouting(
|
|
7667
|
+
function scanBranchRouting(at, nodes, edges, findings) {
|
|
6835
7668
|
const outEdgesBySource = /* @__PURE__ */ new Map();
|
|
6836
7669
|
for (const e of edges) {
|
|
6837
7670
|
if (e.type === "fault") continue;
|
|
@@ -6844,7 +7677,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6844
7677
|
for (const e of outs) {
|
|
6845
7678
|
if (e.isDefault === true && e.condition) {
|
|
6846
7679
|
findings.push({
|
|
6847
|
-
where:
|
|
7680
|
+
where: `${at} \xB7 edge '${src}' \u2192 '${String(e.target)}'`,
|
|
6848
7681
|
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
7682
|
hint: `Drop one: keep \`condition\` for a guarded branch, or drop it and keep \`isDefault: true\` for the "otherwise" path. (#4414)`,
|
|
6850
7683
|
rule: FLOW_DEFAULT_EDGE_WITH_CONDITION,
|
|
@@ -6857,7 +7690,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6857
7690
|
const defaults = outs.filter((e) => e.isDefault === true && !e.condition);
|
|
6858
7691
|
if (defaults.length > 1) {
|
|
6859
7692
|
findings.push({
|
|
6860
|
-
where:
|
|
7693
|
+
where: `${at} \xB7 node '${src}'`,
|
|
6861
7694
|
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
7695
|
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
7696
|
rule: FLOW_MULTIPLE_DEFAULT_EDGES
|
|
@@ -6870,7 +7703,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6870
7703
|
const cfg = node.config ?? {};
|
|
6871
7704
|
if (cfg.condition == null || conditionSource(cfg.condition).trim() === "") continue;
|
|
6872
7705
|
findings.push({
|
|
6873
|
-
where:
|
|
7706
|
+
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
6874
7707
|
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
7708
|
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
7709
|
rule: FLOW_INERT_NODE_CONDITION
|
|
@@ -6890,7 +7723,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6890
7723
|
const unclaimed = [...declaredLabels].filter((l) => !edgeLabels.has(l));
|
|
6891
7724
|
if (unclaimed.length > 0) {
|
|
6892
7725
|
findings.push({
|
|
6893
|
-
where:
|
|
7726
|
+
where: `${at} \xB7 decision '${nid}'`,
|
|
6894
7727
|
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
7728
|
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
7729
|
rule: FLOW_BRANCH_LABEL_UNMATCHED,
|
|
@@ -6906,7 +7739,7 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6906
7739
|
);
|
|
6907
7740
|
if (ungated.length > 0) {
|
|
6908
7741
|
findings.push({
|
|
6909
|
-
where:
|
|
7742
|
+
where: `${at} \xB7 decision '${nid}'`,
|
|
6910
7743
|
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
7744
|
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
7745
|
rule: FLOW_DECISION_UNCONDITIONAL_BRANCH
|
|
@@ -6914,10 +7747,39 @@ function scanBranchRouting(flowName, nodes, edges, findings) {
|
|
|
6914
7747
|
}
|
|
6915
7748
|
}
|
|
6916
7749
|
}
|
|
6917
|
-
function
|
|
6918
|
-
|
|
7750
|
+
function filterCarriesNoCondition(filter) {
|
|
7751
|
+
if (filter === void 0 || filter === null) return true;
|
|
7752
|
+
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7753
|
+
return Object.keys(filter).length === 0;
|
|
7754
|
+
}
|
|
7755
|
+
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7756
|
+
for (const node of nodes) {
|
|
7757
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
7758
|
+
const consequence2 = BULK_WRITE_CONSEQUENCE.get(nodeType);
|
|
7759
|
+
if (!consequence2) continue;
|
|
7760
|
+
const cfg = node.config ?? {};
|
|
7761
|
+
if (cfg.multi !== true) continue;
|
|
7762
|
+
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7763
|
+
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7764
|
+
const filterState = cfg.filter === void 0 || cfg.filter === null ? "no `filter` key" : "an EMPTY `filter`";
|
|
7765
|
+
findings.push({
|
|
7766
|
+
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
7767
|
+
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where: {}\` plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` with no predicate. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
|
|
7768
|
+
hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
|
|
7769
|
+
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7770
|
+
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
7771
|
+
// not provably wrong — unlike the gating members of this family.
|
|
7772
|
+
rule: FLOW_MULTI_WRITE_UNFILTERED
|
|
7773
|
+
});
|
|
7774
|
+
}
|
|
7775
|
+
}
|
|
7776
|
+
function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
7777
|
+
const approvals = nodes.filter((n) => n.type === import_automation5.APPROVAL_NODE_TYPE);
|
|
6919
7778
|
if (approvals.length === 0) return;
|
|
6920
7779
|
const nodeIds = new Set(nodes.map((n) => typeof n.id === "string" ? n.id : "").filter(Boolean));
|
|
7780
|
+
const nodeTypeById = new Map(
|
|
7781
|
+
nodes.filter((n) => typeof n.id === "string").map((n) => [n.id, typeof n.type === "string" ? n.type : ""])
|
|
7782
|
+
);
|
|
6921
7783
|
const outEdges = /* @__PURE__ */ new Map();
|
|
6922
7784
|
for (const e of edges) {
|
|
6923
7785
|
const src = typeof e.source === "string" ? e.source : "";
|
|
@@ -6930,7 +7792,18 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6930
7792
|
if (!aid) continue;
|
|
6931
7793
|
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
7794
|
if (reviseTargets.length === 0) continue;
|
|
6933
|
-
const where =
|
|
7795
|
+
const where = `${at} \xB7 approval '${aid}'`;
|
|
7796
|
+
for (const target of reviseTargets) {
|
|
7797
|
+
const targetType = nodeTypeById.get(target) ?? "";
|
|
7798
|
+
if (targetType === import_automation5.APPROVAL_REVISE_NODE_TYPE) continue;
|
|
7799
|
+
findings.push({
|
|
7800
|
+
where,
|
|
7801
|
+
severity: "error",
|
|
7802
|
+
message: `has a 'revise' out-edge into node '${target}' of type '${targetType || "(untyped)"}' \u2014 the revise window must be an '${import_automation5.APPROVAL_REVISE_NODE_TYPE}' node. Send-back parks the run there while the record is unlocked, and only the approvals service may continue it (submitter-only, audited, and refusing a colliding pending request); \`sendBack\` refuses any other target, so this flow's revise branch cannot run.`,
|
|
7803
|
+
hint: `Set node '${target}' to \`type: '${import_automation5.APPROVAL_REVISE_NODE_TYPE}'\` (drop any \`waitEventConfig\` \u2014 the window is ended by POST /api/v1/approvals/requests/:id/resubmit, not by a signal). ADR-0044 D3 originally said 'wait' here; its 2026-07-28 amendment reversed that, because a 'wait' is resumable by anyone with the run id (#3823, #3801).`,
|
|
7804
|
+
rule: FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED
|
|
7805
|
+
});
|
|
7806
|
+
}
|
|
6934
7807
|
const cfg = a.config ?? {};
|
|
6935
7808
|
if (cfg.maxRevisions === 0) {
|
|
6936
7809
|
findings.push({
|
|
@@ -6958,7 +7831,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6958
7831
|
findings.push({
|
|
6959
7832
|
where,
|
|
6960
7833
|
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
|
|
7834
|
+
hint: `Close the loop: the 'revise' edge should reach an '${import_automation5.APPROVAL_REVISE_NODE_TYPE}' node whose resubmit edge returns to '${aid}' marked \`type: 'back'\` (ADR-0044). See examples/app-showcase showcase_budget_approval.`,
|
|
6962
7835
|
rule: FLOW_APPROVAL_REVISE_DEAD_END
|
|
6963
7836
|
});
|
|
6964
7837
|
} else if (!returnEdges.some((e) => e.type === "back")) {
|
|
@@ -6973,7 +7846,7 @@ function scanApprovalReviseLoops(flowName, nodes, edges, findings) {
|
|
|
6973
7846
|
}
|
|
6974
7847
|
function lintFlowPatterns(stack) {
|
|
6975
7848
|
const findings = [];
|
|
6976
|
-
for (const flow of
|
|
7849
|
+
for (const flow of asArray42(stack.flows)) {
|
|
6977
7850
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
6978
7851
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
6979
7852
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -6994,75 +7867,91 @@ function lintFlowPatterns(stack) {
|
|
|
6994
7867
|
const runAs = typeof flow.runAs === "string" ? flow.runAs : "user";
|
|
6995
7868
|
const userLessKind = userLessTriggerKind(flow, startCfg);
|
|
6996
7869
|
if (userLessKind && runAs !== "system") {
|
|
6997
|
-
const dataNode = nodes
|
|
7870
|
+
const dataNode = findDataNodeAnywhere(nodes, edges);
|
|
6998
7871
|
if (dataNode) {
|
|
6999
|
-
const
|
|
7872
|
+
const at = dataNode.scope ? `, in ${dataNode.scope},` : "";
|
|
7000
7873
|
findings.push({
|
|
7001
7874
|
where: `flow '${flowName}' \xB7 runAs`,
|
|
7002
|
-
message: `${userLessKind}-triggered flow runs
|
|
7875
|
+
message: `${userLessKind}-triggered flow runs under ${RUNAS_EFFECTIVE_IDENTITY}, but a ${userLessKind} run has no trigger user \u2014 so its data node '${dataNode.node.id}' (${dataNode.node.type})${at} has no identity to scope to and will be REFUSED at run time.`,
|
|
7003
7876
|
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
7877
|
rule: FLOW_RUNAS_UNSCOPED,
|
|
7005
7878
|
severity: "error"
|
|
7006
7879
|
});
|
|
7007
7880
|
}
|
|
7008
7881
|
}
|
|
7009
|
-
for (const
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
const
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
if (
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7882
|
+
for (const graph of (0, import_automation5.collectFlowGraphs)({
|
|
7883
|
+
// A cast, not a parse. `FlowNodeSchema.config` is an open `z.record`, so a
|
|
7884
|
+
// region's contents arrive as raw authored records even in a parsed stack —
|
|
7885
|
+
// a nested edge `condition` may still be a bare string where a top-level
|
|
7886
|
+
// one is an Expression envelope. Every rule below reads both
|
|
7887
|
+
// (`conditionSource`), and the walk itself only touches `type` / `config`.
|
|
7888
|
+
// The already-guarded arrays are passed rather than `flow` itself so a
|
|
7889
|
+
// non-array `nodes` still cannot throw: this function promises it never does.
|
|
7890
|
+
nodes,
|
|
7891
|
+
edges
|
|
7892
|
+
})) {
|
|
7893
|
+
const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
|
|
7894
|
+
const graphNodes = graph.nodes;
|
|
7895
|
+
const graphEdges = graph.edges;
|
|
7896
|
+
for (const node of graphNodes) {
|
|
7897
|
+
const nodeWhere = `${at} \xB7 node '${node.id}' (${node.type})`;
|
|
7898
|
+
const cfg = node.config ?? {};
|
|
7899
|
+
if (cfg.filter) scanFilterForDateEquality(cfg.filter, `${nodeWhere} filter`, findings);
|
|
7900
|
+
for (const key of Object.keys(cfg)) {
|
|
7901
|
+
if (PHANTOM_AGG_KEYS.has(key)) {
|
|
7902
|
+
findings.push({
|
|
7903
|
+
where: nodeWhere,
|
|
7904
|
+
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.`,
|
|
7905
|
+
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)`,
|
|
7906
|
+
rule: FLOW_PHANTOM_AGGREGATION
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7033
7909
|
}
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7910
|
+
const strings = [];
|
|
7911
|
+
collectTemplateStrings(stripRegions(node.config), void 0, strings);
|
|
7912
|
+
for (const str4 of strings) {
|
|
7913
|
+
if (DOUBLE_BRACE.test(str4)) {
|
|
7914
|
+
findings.push({
|
|
7915
|
+
where: nodeWhere,
|
|
7916
|
+
message: `double-brace interpolation \`${str4.trim().slice(0, 80)}\` \u2014 flow node values use SINGLE braces.`,
|
|
7917
|
+
hint: `Use \`{var}\` (e.g. \`{record.title}\`). Double-brace \`{{ }}\` is the formula/template-field dialect, not flow node values. (#1315)`,
|
|
7918
|
+
rule: FLOW_DOUBLE_BRACE_INTERP
|
|
7919
|
+
});
|
|
7920
|
+
}
|
|
7921
|
+
if (BARE_DOLLAR_REF.test(str4)) {
|
|
7922
|
+
findings.push({
|
|
7923
|
+
where: nodeWhere,
|
|
7924
|
+
message: `\`${str4.trim().slice(0, 80)}\` looks like a reference written as a literal \u2014 a bare \`$ref.field\` is NOT interpolated.`,
|
|
7925
|
+
hint: `Wrap it and bind a variable: \`{source.id}\` (or \`{$User.Id}\` for the current user). (#1315)`,
|
|
7926
|
+
rule: FLOW_BARE_DOLLAR_REF
|
|
7927
|
+
});
|
|
7928
|
+
}
|
|
7041
7929
|
}
|
|
7042
7930
|
}
|
|
7931
|
+
scanApprovalReviseLoops(at, graphNodes, graphEdges, findings);
|
|
7932
|
+
scanErrorLabelledEdges(at, graphNodes, graphEdges, findings);
|
|
7933
|
+
scanBranchRouting(at, graphNodes, graphEdges, findings);
|
|
7934
|
+
scanUnboundedBulkWrites(at, graphNodes, findings);
|
|
7043
7935
|
}
|
|
7044
|
-
scanApprovalReviseLoops(flowName, nodes, edges, findings);
|
|
7045
|
-
scanErrorLabelledEdges(flowName, nodes, edges, findings);
|
|
7046
|
-
scanBranchRouting(flowName, nodes, edges, findings);
|
|
7047
7936
|
}
|
|
7048
7937
|
return findings;
|
|
7049
7938
|
}
|
|
7050
7939
|
|
|
7051
7940
|
// src/lint-liveness-properties.ts
|
|
7052
|
-
var
|
|
7941
|
+
var import_node_module5 = require("module");
|
|
7053
7942
|
var import_node_path = require("path");
|
|
7054
7943
|
var import_node_fs = require("fs");
|
|
7055
|
-
var
|
|
7944
|
+
var import_meta5 = {};
|
|
7056
7945
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7057
7946
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7058
|
-
function
|
|
7947
|
+
function asArray43(v) {
|
|
7059
7948
|
if (Array.isArray(v)) return v;
|
|
7060
7949
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7061
7950
|
return [];
|
|
7062
7951
|
}
|
|
7063
7952
|
function resolveLivenessDir() {
|
|
7064
7953
|
try {
|
|
7065
|
-
const require2 = (0,
|
|
7954
|
+
const require2 = (0, import_node_module5.createRequire)(import_meta5.url);
|
|
7066
7955
|
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
7067
7956
|
const dir = (0, import_node_path.join)((0, import_node_path.dirname)(pkgJson), "liveness");
|
|
7068
7957
|
return (0, import_node_fs.existsSync)(dir) ? dir : null;
|
|
@@ -7168,7 +8057,16 @@ var TYPE_COLLECTIONS = [
|
|
|
7168
8057
|
{ type: "job", key: "jobs" },
|
|
7169
8058
|
{ type: "email_template", key: "emailTemplates" },
|
|
7170
8059
|
{ type: "mapping", key: "mappings" },
|
|
7171
|
-
{ type: "translation", key: "translations" }
|
|
8060
|
+
{ type: "translation", key: "translations" },
|
|
8061
|
+
// #4956 — dashboard joins the list the moment its ledger first warns on
|
|
8062
|
+
// anything, which is exactly the rule the comment above states. Drilling
|
|
8063
|
+
// `widgets` produced five warned keys (`colorVariant`, `actionUrl`,
|
|
8064
|
+
// `actionType`, `actionIcon`, `aria`), all under `widgets[]`; `getNested`
|
|
8065
|
+
// fans a dotted path out over an array level, so `widgets.colorVariant`
|
|
8066
|
+
// checks every widget on the dashboard. Registering it here is not optional
|
|
8067
|
+
// bookkeeping: without it the ledger would be newly correct and newly
|
|
8068
|
+
// silent, which is the shape this lint exists to prevent.
|
|
8069
|
+
{ type: "dashboard", key: "dashboards" }
|
|
7172
8070
|
];
|
|
7173
8071
|
function lintLivenessProperties(stack) {
|
|
7174
8072
|
const dir = resolveLivenessDir();
|
|
@@ -7176,11 +8074,11 @@ function lintLivenessProperties(stack) {
|
|
|
7176
8074
|
const findings = [];
|
|
7177
8075
|
const objectWarn = loadWarnMap(dir, "object");
|
|
7178
8076
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
7179
|
-
for (const obj of
|
|
8077
|
+
for (const obj of asArray43(stack.objects)) {
|
|
7180
8078
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7181
8079
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
7182
8080
|
if (fieldWarn.size > 0) {
|
|
7183
|
-
for (const field of
|
|
8081
|
+
for (const field of asArray43(obj.fields)) {
|
|
7184
8082
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
7185
8083
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
7186
8084
|
}
|
|
@@ -7189,7 +8087,7 @@ function lintLivenessProperties(stack) {
|
|
|
7189
8087
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
7190
8088
|
const warnMap = loadWarnMap(dir, type);
|
|
7191
8089
|
if (warnMap.size === 0) continue;
|
|
7192
|
-
for (const item of
|
|
8090
|
+
for (const item of asArray43(stack[key])) {
|
|
7193
8091
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
7194
8092
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
7195
8093
|
}
|
|
@@ -7203,7 +8101,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
|
7203
8101
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
7204
8102
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
7205
8103
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
7206
|
-
function
|
|
8104
|
+
function asArray44(v) {
|
|
7207
8105
|
if (Array.isArray(v)) return v;
|
|
7208
8106
|
if (v && typeof v === "object") {
|
|
7209
8107
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7212,9 +8110,9 @@ function asArray40(v) {
|
|
|
7212
8110
|
}
|
|
7213
8111
|
function lintAutonumberFormats(stack) {
|
|
7214
8112
|
const findings = [];
|
|
7215
|
-
for (const obj of
|
|
8113
|
+
for (const obj of asArray44(stack.objects)) {
|
|
7216
8114
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7217
|
-
const fields =
|
|
8115
|
+
const fields = asArray44(obj.fields);
|
|
7218
8116
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
7219
8117
|
for (const f of fields) {
|
|
7220
8118
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -7279,8 +8177,8 @@ function lintAutonumberFormats(stack) {
|
|
|
7279
8177
|
}
|
|
7280
8178
|
|
|
7281
8179
|
// src/lint-view-refs.ts
|
|
7282
|
-
var
|
|
7283
|
-
function
|
|
8180
|
+
var import_spec3 = require("@objectstack/spec");
|
|
8181
|
+
function asArray45(v) {
|
|
7284
8182
|
if (Array.isArray(v)) return v;
|
|
7285
8183
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7286
8184
|
return [];
|
|
@@ -7308,16 +8206,16 @@ function lintViewRefs(stack) {
|
|
|
7308
8206
|
s.add(kind);
|
|
7309
8207
|
};
|
|
7310
8208
|
const containers = [];
|
|
7311
|
-
for (const v of
|
|
8209
|
+
for (const v of asArray45(stack.views)) {
|
|
7312
8210
|
if (v.viewKind) {
|
|
7313
8211
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
7314
8212
|
continue;
|
|
7315
8213
|
}
|
|
7316
|
-
if (!(0,
|
|
8214
|
+
if (!(0, import_spec3.isAggregatedViewContainer)(v)) continue;
|
|
7317
8215
|
const object = viewContainerObjectName(v);
|
|
7318
8216
|
if (object) containers.push({ object, container: v });
|
|
7319
8217
|
}
|
|
7320
|
-
for (const obj of
|
|
8218
|
+
for (const obj of asArray45(stack.objects)) {
|
|
7321
8219
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7322
8220
|
if (!object) continue;
|
|
7323
8221
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -7325,7 +8223,7 @@ function lintViewRefs(stack) {
|
|
|
7325
8223
|
}
|
|
7326
8224
|
}
|
|
7327
8225
|
for (const { object, container } of containers) {
|
|
7328
|
-
const { items, collisions } = (0,
|
|
8226
|
+
const { items, collisions } = (0, import_spec3.expandViewContainerWithDiagnostics)(object, container);
|
|
7329
8227
|
for (const it of items) indexKind(it.name, it.viewKind);
|
|
7330
8228
|
for (const col of collisions) {
|
|
7331
8229
|
findings.push({
|
|
@@ -7371,11 +8269,11 @@ function lintViewRefs(stack) {
|
|
|
7371
8269
|
});
|
|
7372
8270
|
}
|
|
7373
8271
|
};
|
|
7374
|
-
for (const obj of
|
|
8272
|
+
for (const obj of asArray45(stack.objects)) {
|
|
7375
8273
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
7376
|
-
for (const action of
|
|
8274
|
+
for (const action of asArray45(obj.actions)) checkAction(action, object);
|
|
7377
8275
|
}
|
|
7378
|
-
for (const action of
|
|
8276
|
+
for (const action of asArray45(stack.actions)) checkAction(action);
|
|
7379
8277
|
return findings;
|
|
7380
8278
|
}
|
|
7381
8279
|
|
|
@@ -7427,8 +8325,43 @@ function refOf2(def) {
|
|
|
7427
8325
|
return def?.reference || def?.reference_to;
|
|
7428
8326
|
}
|
|
7429
8327
|
var UNIQUE_DOUBLE_DECLARATION = "unique/double-declaration";
|
|
8328
|
+
var UNIQUE_UNSCOPED_DECLARED_INDEX = "unique/unscoped-declared-index";
|
|
8329
|
+
var UNIQUE_LEGACY_ORGANIZATION_COMPOSITE = "unique/legacy-organization-composite";
|
|
8330
|
+
function authoredTenantColumn(obj) {
|
|
8331
|
+
const declared = obj?.tenancy?.tenantField;
|
|
8332
|
+
return typeof declared === "string" && declared.trim() ? declared.trim() : "organization_id";
|
|
8333
|
+
}
|
|
7430
8334
|
function uniqueDeclared(u) {
|
|
7431
|
-
return u === true || u === "global";
|
|
8335
|
+
return u === true || u === "global" || u === "organization";
|
|
8336
|
+
}
|
|
8337
|
+
function fieldUniqueScope(u) {
|
|
8338
|
+
return u === "global" ? "global" : "organization";
|
|
8339
|
+
}
|
|
8340
|
+
function indexUniqueScope(u) {
|
|
8341
|
+
return u === "organization" ? "organization" : "global";
|
|
8342
|
+
}
|
|
8343
|
+
function lintUnscopedDeclaredIndexes(objects) {
|
|
8344
|
+
const issues = [];
|
|
8345
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
8346
|
+
for (let i = 0; i < objects.length; i++) {
|
|
8347
|
+
const obj = objects[i];
|
|
8348
|
+
if (!obj?.name) continue;
|
|
8349
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
8350
|
+
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
8351
|
+
const idx = declaredIndexes[j];
|
|
8352
|
+
if (idx?.unique !== true) continue;
|
|
8353
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string").join(", ") : "";
|
|
8354
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8355
|
+
issues.push({
|
|
8356
|
+
severity: "warning",
|
|
8357
|
+
rule: UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
8358
|
+
message: `"${obj.name}" declares index${indexLabel} [${cols}] with bare \`unique: true\` \u2014 a unique index whose scope is unstated (ADR-0120). Today the bare spelling materializes over exactly its \`fields\`, i.e. installation-wide; an author who meant "unique per organization" gets no per-organization constraint and no error. Protocol 18 rejects this spelling (#5082).`,
|
|
8359
|
+
path: `objects[${i}].indexes[${j}]`,
|
|
8360
|
+
fix: `State the scope: \`unique: 'global'\` (installation-wide \u2014 exactly today's behavior) or \`unique: 'organization'\` (one holder per organization \u2014 the driver prepends the NULL-safe organization key part at registration).`
|
|
8361
|
+
});
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
8364
|
+
return issues;
|
|
7432
8365
|
}
|
|
7433
8366
|
function lintUniqueDeclarations(objects) {
|
|
7434
8367
|
const issues = [];
|
|
@@ -7448,23 +8381,70 @@ function lintUniqueDeclarations(objects) {
|
|
|
7448
8381
|
if (singleColumnUniqueIndexes.size === 0) continue;
|
|
7449
8382
|
for (const { name, def } of fieldEntries2(obj.fields)) {
|
|
7450
8383
|
if (!uniqueDeclared(def?.unique)) continue;
|
|
7451
|
-
if (def.unique === "global") continue;
|
|
7452
8384
|
const idx = singleColumnUniqueIndexes.get(name);
|
|
7453
8385
|
if (!idx) continue;
|
|
8386
|
+
const fScope = fieldUniqueScope(def.unique);
|
|
8387
|
+
const iScope = indexUniqueScope(idx.unique);
|
|
7454
8388
|
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8389
|
+
const fieldSpelling = `\`unique: ${typeof def.unique === "string" ? `'${def.unique}'` : def.unique}\``;
|
|
8390
|
+
const indexSpelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
|
|
8391
|
+
let message;
|
|
8392
|
+
let fix;
|
|
8393
|
+
if (fScope === iScope) {
|
|
8394
|
+
const boundary = fScope === "global" ? "installation-wide" : "per-organization";
|
|
8395
|
+
message = `"${obj.name}.${name}" declares field-level ${fieldSpelling} AND a single-column unique index${indexLabel} (${indexSpelling}) on the same column. Both ask for the same ${boundary} boundary \u2014 the same unique index declared twice (ADR-0120 D5b). Redundant, not contradictory: drop one so the intent has a single home.`;
|
|
8396
|
+
fix = fScope === "global" ? `Keep ONE spelling of installation-wide uniqueness: \`unique: 'global'\` on '${name}', or the declared index \u2014 not both.` : `Keep ONE spelling of per-organization uniqueness: \`unique: 'organization'\` on '${name}' (preferred), or the declared \`'organization'\` index \u2014 not both.`;
|
|
8397
|
+
} else {
|
|
8398
|
+
const globalSide = fScope === "global" ? `field-level ${fieldSpelling}` : `declared index${indexLabel} (${indexSpelling})`;
|
|
8399
|
+
const orgSide = fScope === "global" ? `declared index${indexLabel} (${indexSpelling})` : `field-level ${fieldSpelling}`;
|
|
8400
|
+
message = `"${obj.name}.${name}" declares an installation-wide unique (${globalSide}) AND a per-organization unique (${orgSide}) on the same column \u2014 the two scopes CONTRADICT (ADR-0120 D5b). The installation-wide index is physically stricter and wins; the per-organization constraint can never be tripped, so one of the two intents you wrote is silently dead.`;
|
|
8401
|
+
fix = `Pick ONE scope and say it once: for installation-wide uniqueness keep \`unique: 'global'\` and drop the per-organization declaration; for per-organization uniqueness set \`unique: 'organization'\` (field-level on '${name}', or on the declared index) and drop the installation-wide one.`;
|
|
8402
|
+
}
|
|
7455
8403
|
issues.push({
|
|
7456
8404
|
severity: "warning",
|
|
7457
8405
|
rule: UNIQUE_DOUBLE_DECLARATION,
|
|
7458
|
-
message
|
|
8406
|
+
message,
|
|
7459
8407
|
path: `objects[${i}]`,
|
|
7460
|
-
fix
|
|
8408
|
+
fix
|
|
8409
|
+
});
|
|
8410
|
+
}
|
|
8411
|
+
}
|
|
8412
|
+
return issues;
|
|
8413
|
+
}
|
|
8414
|
+
function lintLegacyOrganizationComposites(objects) {
|
|
8415
|
+
const issues = [];
|
|
8416
|
+
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
8417
|
+
for (let i = 0; i < objects.length; i++) {
|
|
8418
|
+
const obj = objects[i];
|
|
8419
|
+
if (!obj?.name) continue;
|
|
8420
|
+
const tenantColumn = authoredTenantColumn(obj);
|
|
8421
|
+
const declaredIndexes = Array.isArray(obj.indexes) ? obj.indexes : [];
|
|
8422
|
+
for (let j = 0; j < declaredIndexes.length; j++) {
|
|
8423
|
+
const idx = declaredIndexes[j];
|
|
8424
|
+
if (!uniqueDeclared(idx?.unique) || idx.unique === "organization") continue;
|
|
8425
|
+
const cols = Array.isArray(idx?.fields) ? idx.fields.filter((f) => typeof f === "string") : [];
|
|
8426
|
+
if (cols.length < 2) continue;
|
|
8427
|
+
if (!cols.includes(tenantColumn)) continue;
|
|
8428
|
+
const indexLabel = typeof idx?.name === "string" && idx.name.trim() ? ` '${idx.name.trim()}'` : "";
|
|
8429
|
+
const spelling = `\`unique: ${typeof idx.unique === "string" ? `'${idx.unique}'` : idx.unique}\``;
|
|
8430
|
+
const rest = cols.filter((c) => c !== tenantColumn);
|
|
8431
|
+
issues.push({
|
|
8432
|
+
severity: "warning",
|
|
8433
|
+
rule: UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
8434
|
+
message: `"${obj.name}" declares index${indexLabel} [${cols.join(", ")}] with ${spelling} and lists the organization column '${tenantColumn}' itself \u2014 the hand-written per-organization composite that predates the scope vocabulary (ADR-0120 S6). It reads as "unique per organization" but materializes as a plain composite, and SQL UNIQUE is NULL-distinct: on every row whose '${tenantColumn}' is NULL it enforces nothing (#5030) \u2014 which on a single-organization deployment is every row.`,
|
|
8435
|
+
path: `objects[${i}].indexes[${j}]`,
|
|
8436
|
+
fix: `State the scope instead: \`unique: 'organization'\` on this index (keep \`fields\` exactly as they are \u2014 the driver makes the listed '${tenantColumn}' NULL-safe in place rather than prepending a second organization key part). ${rest.length > 0 ? `The constraint then really is "one ${rest.join(" + ")} per organization". ` : ""}Opting in is a physical tightening: it surfaces as a \`recreate_index\` drift op gated by the duplicate pre-flight probe (ADR-0120 D4), so pre-existing duplicate NULL-organization rows block it with a report rather than failing a boot. Leaving it as-is stays valid indefinitely and forces no drift.`
|
|
7461
8437
|
});
|
|
7462
8438
|
}
|
|
7463
8439
|
}
|
|
7464
8440
|
return issues;
|
|
7465
8441
|
}
|
|
7466
8442
|
function lintDataModel(objects) {
|
|
7467
|
-
const issues =
|
|
8443
|
+
const issues = [
|
|
8444
|
+
...lintUnscopedDeclaredIndexes(objects),
|
|
8445
|
+
...lintUniqueDeclarations(objects),
|
|
8446
|
+
...lintLegacyOrganizationComposites(objects)
|
|
8447
|
+
];
|
|
7468
8448
|
if (!Array.isArray(objects) || objects.length === 0) return issues;
|
|
7469
8449
|
const childrenByParent = {};
|
|
7470
8450
|
for (const child of objects) {
|
|
@@ -7731,6 +8711,42 @@ var AUTHORING_RULES = [
|
|
|
7731
8711
|
runtimeTypes: ["flow"],
|
|
7732
8712
|
run: (stack) => validateReferenceIntegrity(stack)
|
|
7733
8713
|
},
|
|
8714
|
+
// ADR-0078 / #5068 — the SDUI component-props gate. `PageComponent.properties`
|
|
8715
|
+
// is `z.record(z.string(), z.unknown())` and ADR-0089 D3a strictness does not
|
|
8716
|
+
// recurse into it, so until this entry existed the 31 typed prop schemas in
|
|
8717
|
+
// `ComponentPropsMap` were parsed by NOTHING (#4001 批 17's `no gate`
|
|
8718
|
+
// verdict): an undeclared or wrongly-typed prop parsed clean, was retained,
|
|
8719
|
+
// and reached objectui's renderer to be ignored there. This dispatches on
|
|
8720
|
+
// `type` and judges the bag; unregistered types are skipped, which is a
|
|
8721
|
+
// required semantic (`type` is an open union — the example corpus authors 87
|
|
8722
|
+
// nodes of 10 types this map does not carry).
|
|
8723
|
+
//
|
|
8724
|
+
// `normalized` for a reason worth stating, since the props bag survives the
|
|
8725
|
+
// Zod parse UNCHANGED and both tiers would otherwise carry the same data: the
|
|
8726
|
+
// ADR-0087 conversion layer runs inside `normalizeStackInput`, so a converted
|
|
8727
|
+
// alias (`page-header-subtitle-alias` rewrites `properties.description` →
|
|
8728
|
+
// `subtitle`) is already canonical here and is never reported as undeclared —
|
|
8729
|
+
// while a schema error elsewhere in the stack cannot take these findings down
|
|
8730
|
+
// with it.
|
|
8731
|
+
//
|
|
8732
|
+
// Advisory, deliberately, and this is the whole shape of #5068's first step:
|
|
8733
|
+
// wiring the parse is the precondition for enforcement, not the enforcement
|
|
8734
|
+
// (#5020, one surface over). The live corpus violates the declarations in two
|
|
8735
|
+
// places that are open contract questions — inline i18n label maps on three
|
|
8736
|
+
// published platform pages (#5728) and the record picker's declared-but-unread
|
|
8737
|
+
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8738
|
+
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8739
|
+
// separate step, once the warning-period inventory is empty.
|
|
8740
|
+
{
|
|
8741
|
+
name: "validateComponentProps",
|
|
8742
|
+
tier: "advisory",
|
|
8743
|
+
input: "normalized",
|
|
8744
|
+
commands: ALL,
|
|
8745
|
+
source: "packages/lint/src/validate-component-props.ts",
|
|
8746
|
+
surfaces: CLI_ONLY,
|
|
8747
|
+
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8748
|
+
run: (stack) => validateComponentProps(stack)
|
|
8749
|
+
},
|
|
7734
8750
|
// ADR-0065 — a styled node's responsiveStyles must be scopable (needs an
|
|
7735
8751
|
// `id`), name real CSS properties + design tokens, and carry a `large` base.
|
|
7736
8752
|
{
|
|
@@ -7794,18 +8810,32 @@ var AUTHORING_RULES = [
|
|
|
7794
8810
|
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
8811
|
run: (stack) => validateCapabilityReferences(stack)
|
|
7796
8812
|
},
|
|
7797
|
-
// A
|
|
7798
|
-
//
|
|
7799
|
-
//
|
|
8813
|
+
// A flow that LOOKS armed and never launches — silently. Reads the pre-parse
|
|
8814
|
+
// tier so an author sees what they wrote.
|
|
8815
|
+
//
|
|
8816
|
+
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8817
|
+
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8818
|
+
// dead? Three rules answer yes and now emit `error` — a `config.timeRelative`
|
|
8819
|
+
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8820
|
+
// predicate cannot route at all, and a `record-*` triggerType outside the
|
|
8821
|
+
// closed token grammar `triggerTypeToHookEvents` maps. None of those verdicts
|
|
8822
|
+
// can be changed by installing a package, so there is no reading under which
|
|
8823
|
+
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8824
|
+
// (the object may come from another installed package — a hedge this rule
|
|
8825
|
+
// cannot decide), as did `flow-draft-status-ambiguous` (draft flows DO fire;
|
|
8826
|
+
// that one is ambiguity of intent, not a dead flow).
|
|
7800
8827
|
{
|
|
7801
8828
|
name: "validateFlowTriggerReadiness",
|
|
7802
|
-
tier: "
|
|
8829
|
+
tier: "gating",
|
|
7803
8830
|
input: "normalized",
|
|
7804
8831
|
commands: ALL,
|
|
7805
8832
|
source: "packages/lint/src/validate-flow-trigger-readiness.ts",
|
|
7806
|
-
// Runtime publish gate (#4463): the FLOW family.
|
|
7807
|
-
//
|
|
7808
|
-
//
|
|
8833
|
+
// Runtime publish gate (#4463): the FLOW family. Its `error` findings now
|
|
8834
|
+
// REFUSE a `state: 'active'` write (P1 gates on `error` only); the rules that
|
|
8835
|
+
// stayed `warning` keep being logged as advisories. The gate judges a
|
|
8836
|
+
// snapshot whose `flows` holds only the written item and subtracts the
|
|
8837
|
+
// baseline's findings, so this refuses the dead flow's own publish — never
|
|
8838
|
+
// another flow's save on account of a stored one.
|
|
7809
8839
|
surfaces: CLI_AND_RUNTIME,
|
|
7810
8840
|
runtimeTypes: ["flow"],
|
|
7811
8841
|
run: (stack) => validateFlowTriggerReadiness(stack)
|
|
@@ -8016,9 +9046,31 @@ var AUTHORING_RULES = [
|
|
|
8016
9046
|
hint: f.hint
|
|
8017
9047
|
}))
|
|
8018
9048
|
},
|
|
8019
|
-
//
|
|
8020
|
-
//
|
|
8021
|
-
//
|
|
9049
|
+
// ADR-0120 D5a — a declared index with bare `unique: true` states no scope
|
|
9050
|
+
// at all (`unique/unscoped-declared-index` — the #4986 trap). Fires on the
|
|
9051
|
+
// spelling alone, no tenancy inference; 17.x warns, protocol 18 rejects the
|
|
9052
|
+
// spelling (#5082).
|
|
9053
|
+
{
|
|
9054
|
+
name: "lintUnscopedDeclaredIndexes",
|
|
9055
|
+
tier: "advisory",
|
|
9056
|
+
input: "parsed",
|
|
9057
|
+
commands: ["validate", "build"],
|
|
9058
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
9059
|
+
surfaces: CLI_ONLY,
|
|
9060
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9061
|
+
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly ahead of R10 in 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.",
|
|
9062
|
+
run: (stack) => lintUnscopedDeclaredIndexes(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9063
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9064
|
+
rule: f.rule,
|
|
9065
|
+
where: f.path,
|
|
9066
|
+
path: f.path,
|
|
9067
|
+
message: f.message,
|
|
9068
|
+
hint: f.fix ?? ""
|
|
9069
|
+
}))
|
|
9070
|
+
},
|
|
9071
|
+
// #3991 / ADR-0120 D5b — a column carrying BOTH a field-level `unique` and a
|
|
9072
|
+
// single-column declared unique index states two scopes of which at most one
|
|
9073
|
+
// takes effect (`unique/double-declaration`, the four-quadrant matrix).
|
|
8022
9074
|
{
|
|
8023
9075
|
name: "lintUniqueDeclarations",
|
|
8024
9076
|
tier: "advisory",
|
|
@@ -8037,6 +9089,28 @@ var AUTHORING_RULES = [
|
|
|
8037
9089
|
hint: f.fix ?? ""
|
|
8038
9090
|
}))
|
|
8039
9091
|
},
|
|
9092
|
+
// ADR-0120 D5c — a declared unique listing the organization column IS the
|
|
9093
|
+
// hand-written per-organization composite (S6). Advisory nudge toward the
|
|
9094
|
+
// `'organization'` respelling, which is also what closes its NULL hole
|
|
9095
|
+
// (#5030). Never auto-fixed: opting in is a real D4 tightening.
|
|
9096
|
+
{
|
|
9097
|
+
name: "lintLegacyOrganizationComposites",
|
|
9098
|
+
tier: "advisory",
|
|
9099
|
+
input: "parsed",
|
|
9100
|
+
commands: ["validate", "build"],
|
|
9101
|
+
source: "packages/lint/src/data-model-rules.ts",
|
|
9102
|
+
surfaces: CLI_ONLY,
|
|
9103
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9104
|
+
scopeReason: "`os lint` already reports this rule through `lintDataModel`, which calls it directly alongside R10/R11 in 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.",
|
|
9105
|
+
run: (stack) => lintLegacyOrganizationComposites(Array.isArray(stack.objects) ? stack.objects : []).map((f) => ({
|
|
9106
|
+
severity: f.severity === "suggestion" ? "info" : f.severity,
|
|
9107
|
+
rule: f.rule,
|
|
9108
|
+
where: f.path,
|
|
9109
|
+
path: f.path,
|
|
9110
|
+
message: f.message,
|
|
9111
|
+
hint: f.fix ?? ""
|
|
9112
|
+
}))
|
|
9113
|
+
},
|
|
8040
9114
|
// ADR-0090 D7 — the security-domain publish linter. Every `error` rule mirrors
|
|
8041
9115
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
8042
9116
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
@@ -8064,6 +9138,88 @@ var AUTHORING_RULES = [
|
|
|
8064
9138
|
surfaces: CLI_ONLY,
|
|
8065
9139
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8066
9140
|
run: (stack) => validateOrgAxisRedLines(stack)
|
|
9141
|
+
},
|
|
9142
|
+
// #4698 — the "declared but never read" gate, for the one surface where the
|
|
9143
|
+
// predicate is EXACT rather than inferred. A sharing rule's `condition` has a
|
|
9144
|
+
// single runtime consumer (`bootstrapDeclaredSharingRules`) whose only use of
|
|
9145
|
+
// the key is `compileCelToFilter(condition, { variables: {} })`; a condition
|
|
9146
|
+
// that does not lower means the rule is SKIPPED at boot, so the grant is
|
|
9147
|
+
// declared and does not exist. The lint calls that same compiler, from the
|
|
9148
|
+
// same package, with the same options — the verdict cannot drift from the
|
|
9149
|
+
// consumer's. Gating for the ADR-0078 reason `SharingRuleSchema`'s own
|
|
9150
|
+
// docblock states: the whole authorable surface is enforced, and this was the
|
|
9151
|
+
// one field where that sentence was not yet true.
|
|
9152
|
+
{
|
|
9153
|
+
name: "validateSharingRuleEnforceability",
|
|
9154
|
+
tier: "gating",
|
|
9155
|
+
input: "parsed",
|
|
9156
|
+
commands: ALL,
|
|
9157
|
+
source: "packages/lint/src/validate-sharing-rule-enforceability.ts",
|
|
9158
|
+
surfaces: CLI_ONLY,
|
|
9159
|
+
surfaceReason: "P2 (#4463): a sharing rule is not a `flow`, and P1 gates `flow` alone. The rule itself is snapshot-safe \u2014 it reads ONLY `stack.sharingRules[].condition` and needs no other collection \u2014 so widening it here is a `runtimeTypes: ['sharing_rule']` edit once the gate accepts that type, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
9160
|
+
run: (stack) => validateSharingRuleEnforceability(stack)
|
|
9161
|
+
},
|
|
9162
|
+
// #4983 — the sibling surface of the rule above, and ADR-0056 D4's gate,
|
|
9163
|
+
// which had never been wired to anything: `isSupportedRlsExpression` existed
|
|
9164
|
+
// solely so an authoring command could reject a predicate the runtime drops,
|
|
9165
|
+
// and no authoring command called it. An unlowerable
|
|
9166
|
+
// `rowLevelSecurity[].using` is DROPPED by `RLSCompiler` and — when it is the
|
|
9167
|
+
// only applicable policy — replaced by `RLS_DENY_FILTER`, so the policy reads
|
|
9168
|
+
// as an authorization and behaves as a blanket refusal. Same construction as
|
|
9169
|
+
// the sharing-rule entry: the verdict is the runtime's own function, reached
|
|
9170
|
+
// through `@objectstack/formula` (where #4983 hoisted it), never a model of it.
|
|
9171
|
+
{
|
|
9172
|
+
name: "validateRlsPredicateEnforceability",
|
|
9173
|
+
tier: "gating",
|
|
9174
|
+
input: "parsed",
|
|
9175
|
+
commands: ALL,
|
|
9176
|
+
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
9177
|
+
surfaces: CLI_ONLY,
|
|
9178
|
+
surfaceReason: "P2 (#4463): the rule reads `stack.permissions[]`, a stack-wide collection the per-write snapshot does not carry, and P1 gates `flow` alone. It is otherwise snapshot-ready \u2014 it needs no other collection \u2014 so widening it is a `runtimeTypes: ['permission_set']` edit once the gate builds that snapshot, not new wiring. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
9179
|
+
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
9180
|
+
},
|
|
9181
|
+
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
9182
|
+
// STATIC artifacts an object validation rule carries. A `format` rule's
|
|
9183
|
+
// `regex` that `new RegExp(...)` throws on, and a `json_schema` rule's schema
|
|
9184
|
+
// ajv cannot compile, are both logged and SKIPPED on the write path
|
|
9185
|
+
// (`rule-validator.ts`), so the rule ships, lists, and protects nothing.
|
|
9186
|
+
// Neither needs a record to judge, so the authoring door is the right one:
|
|
9187
|
+
// rejecting a broken regex at RUNTIME instead would reject every write
|
|
9188
|
+
// touching that field for as long as the metadata is deployed (#4762's own
|
|
9189
|
+
// analysis — the runtime-backstop question stays open for the maintainer).
|
|
9190
|
+
// Gating for the `lint-flow-patterns.ts` bar: no reading of the metadata
|
|
9191
|
+
// behaves as written, because the rule does not run at all.
|
|
9192
|
+
{
|
|
9193
|
+
name: "validateRuleCompilability",
|
|
9194
|
+
tier: "gating",
|
|
9195
|
+
input: "parsed",
|
|
9196
|
+
commands: ALL,
|
|
9197
|
+
source: "packages/lint/src/validate-rule-compilability.ts",
|
|
9198
|
+
surfaces: CLI_ONLY,
|
|
9199
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9200
|
+
run: (stack) => validateRuleCompilability(stack)
|
|
9201
|
+
},
|
|
9202
|
+
// #5178 — the residual half of #5029, which registering `ajv-formats` does
|
|
9203
|
+
// NOT close: under `strict: false` a MISSPELLED format name (`emial`) is
|
|
9204
|
+
// logged once and DROPPED, so the rule compiles, ships, runs on every write
|
|
9205
|
+
// and enforces nothing for the keyword its author wrote — and the record is
|
|
9206
|
+
// accepted, which is the silent direction. Deliberately its own entry rather
|
|
9207
|
+
// than a third finding inside the rule above: that one's whole contract is
|
|
9208
|
+
// compiling in the runtime's exact environment, and a typo'd format compiles
|
|
9209
|
+
// there. This judges the format NAME against the registered set (enumerated
|
|
9210
|
+
// from the same ajv instance, never a hardcoded list) and compiles nothing,
|
|
9211
|
+
// so the #4762/#5029 compile parity is untouched — a judgement beside the
|
|
9212
|
+
// compile, not a divergent compile. Gating for the `lint-flow-patterns.ts`
|
|
9213
|
+
// bar: no reading of the metadata behaves as written.
|
|
9214
|
+
{
|
|
9215
|
+
name: "validateRuleSchemaFormats",
|
|
9216
|
+
tier: "gating",
|
|
9217
|
+
input: "parsed",
|
|
9218
|
+
commands: ALL,
|
|
9219
|
+
source: "packages/lint/src/validate-rule-schema-formats.ts",
|
|
9220
|
+
surfaces: CLI_ONLY,
|
|
9221
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9222
|
+
run: (stack) => validateRuleSchemaFormats(stack)
|
|
8067
9223
|
}
|
|
8068
9224
|
];
|
|
8069
9225
|
function authoringRulesFor(command) {
|
|
@@ -8174,6 +9330,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8174
9330
|
APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER,
|
|
8175
9331
|
APPROVAL_APPROVER_TYPE_DEPRECATED,
|
|
8176
9332
|
APPROVAL_APPROVER_TYPE_UNKNOWN,
|
|
9333
|
+
APPROVAL_APPROVER_TYPE_UNSUPPORTED,
|
|
8177
9334
|
APPROVAL_DECISION_OUTPUTS_RESERVED,
|
|
8178
9335
|
APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
8179
9336
|
APPROVAL_EXPRESSION_INVALID,
|
|
@@ -8192,15 +9349,19 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8192
9349
|
CHART_DIMENSION_UNKNOWN,
|
|
8193
9350
|
CHART_FIELD_UNKNOWN,
|
|
8194
9351
|
CHART_MEASURE_UNKNOWN,
|
|
9352
|
+
COMPONENT_PROPS_INVALID,
|
|
9353
|
+
COMPONENT_PROPS_UNKNOWN_KEY,
|
|
8195
9354
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
8196
9355
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
8197
9356
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
8198
9357
|
EXPRESSION_INVALID,
|
|
8199
9358
|
FIELD_GROUP_EMPTY,
|
|
9359
|
+
FIELD_GROUP_SHADOWED,
|
|
8200
9360
|
FIELD_GROUP_UNDECLARED,
|
|
8201
9361
|
FILTER_TOKEN_UNKNOWN,
|
|
8202
9362
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
8203
9363
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
9364
|
+
FLOW_APPROVAL_REVISE_TARGET_NOT_SERVICE_OWNED,
|
|
8204
9365
|
FLOW_APPROVAL_REVISE_UNMARKED_BACKEDGE,
|
|
8205
9366
|
FLOW_BARE_DOLLAR_REF,
|
|
8206
9367
|
FLOW_BRANCH_LABEL_UNMATCHED,
|
|
@@ -8212,12 +9373,16 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8212
9373
|
FLOW_ERROR_LABEL_NOT_FAULT,
|
|
8213
9374
|
FLOW_INERT_NODE_CONDITION,
|
|
8214
9375
|
FLOW_MULTIPLE_DEFAULT_EDGES,
|
|
9376
|
+
FLOW_MULTI_WRITE_UNFILTERED,
|
|
8215
9377
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
8216
9378
|
FLOW_PHANTOM_AGGREGATION,
|
|
8217
9379
|
FLOW_RUNAS_UNSCOPED,
|
|
8218
9380
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
8219
9381
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
8220
9382
|
FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
9383
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID,
|
|
9384
|
+
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
9385
|
+
FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
8221
9386
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
8222
9387
|
FLOW_UPDATE_READONLY_FIELD,
|
|
8223
9388
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
@@ -8232,6 +9397,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8232
9397
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
8233
9398
|
LIVENESS_DEAD_PROPERTY,
|
|
8234
9399
|
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
9400
|
+
MAX_SCHEMA_WALK_DEPTH,
|
|
8235
9401
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
8236
9402
|
NAV_OBJECT_UNGRANTED,
|
|
8237
9403
|
NAV_TARGET_UNRESOLVED,
|
|
@@ -8245,14 +9411,19 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8245
9411
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
8246
9412
|
REACT_CHART_AGGREGATE_INVALID,
|
|
8247
9413
|
REACT_CHART_AXIS_UNKNOWN,
|
|
9414
|
+
REACT_CHART_DRILLDOWN_INVALID,
|
|
8248
9415
|
REACT_CHART_FIELD_UNKNOWN,
|
|
8249
9416
|
REFERENCE_INTEGRITY_RULES,
|
|
9417
|
+
RLS_PREDICATE_UNENFORCEABLE,
|
|
9418
|
+
RLS_PREDICATE_UNPARSEABLE,
|
|
9419
|
+
RUNTIME_AJV_OPTIONS,
|
|
8250
9420
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
8251
9421
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
8252
9422
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
8253
9423
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
8254
9424
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
8255
9425
|
SECURITY_EXTERNAL_WIDER,
|
|
9426
|
+
SECURITY_FLS_UNQUALIFIED_KEY,
|
|
8256
9427
|
SECURITY_GRANT_EXPIRED_AT_AUTHORING,
|
|
8257
9428
|
SECURITY_MASTER_DETAIL_UNGRANTED,
|
|
8258
9429
|
SECURITY_OWD_ALIAS,
|
|
@@ -8263,6 +9434,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8263
9434
|
SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
|
|
8264
9435
|
SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
8265
9436
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
9437
|
+
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
9438
|
+
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
8266
9439
|
STYLE_CLASSNAME_TAILWIND,
|
|
8267
9440
|
STYLE_NODE_MISSING_ID,
|
|
8268
9441
|
STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -8272,8 +9445,14 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8272
9445
|
TITLE_FORMAT_RETIRED,
|
|
8273
9446
|
TITLE_UNRESOLVABLE,
|
|
8274
9447
|
TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
9448
|
+
TRANSLATION_SECTION_NAME_MISSING,
|
|
8275
9449
|
TRANSLATION_TARGET_UNKNOWN,
|
|
8276
9450
|
UNIQUE_DOUBLE_DECLARATION,
|
|
9451
|
+
UNIQUE_LEGACY_ORGANIZATION_COMPOSITE,
|
|
9452
|
+
UNIQUE_UNSCOPED_DECLARED_INDEX,
|
|
9453
|
+
VALIDATION_RULE_REGEX_UNCOMPILABLE,
|
|
9454
|
+
VALIDATION_RULE_SCHEMA_UNCOMPILABLE,
|
|
9455
|
+
VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT,
|
|
8277
9456
|
VIEW_CONTAINER_SHAPE,
|
|
8278
9457
|
VIEW_KEY_COLLISION,
|
|
8279
9458
|
VIEW_REF_FORM_TARGET_KIND,
|
|
@@ -8282,6 +9461,8 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8282
9461
|
VISIBILITY_ROOT_MISLAYERED,
|
|
8283
9462
|
WIDGET_DATASET_UNKNOWN,
|
|
8284
9463
|
WIDGET_DIMENSION_UNKNOWN,
|
|
9464
|
+
WIDGET_LEGACY_ANALYTICS_SHAPE,
|
|
9465
|
+
WIDGET_LEGACY_ANALYTICS_UNRENDERABLE,
|
|
8285
9466
|
WIDGET_MEASURE_UNKNOWN,
|
|
8286
9467
|
authoringRulesFor,
|
|
8287
9468
|
buildAccessMatrix,
|
|
@@ -8289,12 +9470,16 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8289
9470
|
extractHookBodyWriteSet,
|
|
8290
9471
|
extractHookBodyWrites,
|
|
8291
9472
|
findUnguardedNullableOperands,
|
|
9473
|
+
isSourceAuthoredPage,
|
|
8292
9474
|
lintAutonumberFormats,
|
|
8293
9475
|
lintDataModel,
|
|
8294
9476
|
lintFlowPatterns,
|
|
9477
|
+
lintLegacyOrganizationComposites,
|
|
8295
9478
|
lintLivenessProperties,
|
|
8296
9479
|
lintUniqueDeclarations,
|
|
9480
|
+
lintUnscopedDeclaredIndexes,
|
|
8297
9481
|
lintViewRefs,
|
|
9482
|
+
nearestRegisteredFormat,
|
|
8298
9483
|
nullGuardMessage,
|
|
8299
9484
|
runAuthoringRules,
|
|
8300
9485
|
runRuntimeAuthoringRules,
|
|
@@ -8311,6 +9496,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8311
9496
|
validateApprovalApprovers,
|
|
8312
9497
|
validateCapabilityReferences,
|
|
8313
9498
|
validateChartBindings,
|
|
9499
|
+
validateComponentProps,
|
|
8314
9500
|
validateDashboardActionRefs,
|
|
8315
9501
|
validateFilterTokens,
|
|
8316
9502
|
validateFlowNodeWrites,
|
|
@@ -8333,15 +9519,21 @@ function runRuntimeAuthoringRules(args) {
|
|
|
8333
9519
|
validateRecordTitle,
|
|
8334
9520
|
validateReferenceIntegrity,
|
|
8335
9521
|
validateResponsiveStyles,
|
|
9522
|
+
validateRlsPredicateEnforceability,
|
|
9523
|
+
validateRuleCompilability,
|
|
9524
|
+
validateRuleSchemaFormats,
|
|
8336
9525
|
validateSearchableFields,
|
|
8337
9526
|
validateSecurityPosture,
|
|
8338
9527
|
validateSeedReplaySafety,
|
|
8339
9528
|
validateSeedStateMachine,
|
|
8340
9529
|
validateSemanticRoles,
|
|
9530
|
+
validateSharingRuleEnforceability,
|
|
8341
9531
|
validateStackExpressions,
|
|
9532
|
+
validateTranslatableSections,
|
|
8342
9533
|
validateTranslationReferences,
|
|
8343
9534
|
validateViewContainers,
|
|
8344
9535
|
validateVisibilityPredicates,
|
|
8345
|
-
validateWidgetBindings
|
|
9536
|
+
validateWidgetBindings,
|
|
9537
|
+
walkPageComponents
|
|
8346
9538
|
});
|
|
8347
9539
|
//# sourceMappingURL=index.cjs.map
|