@objectstack/lint 17.0.0-rc.6 → 17.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6996 -1
- package/dist/index.cjs +1164 -482
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +138 -15
- package/dist/index.d.ts +138 -15
- package/dist/index.js +1141 -465
- package/dist/index.js.map +1 -1
- package/dist/{runtime-B50yywI_.d.cts → runtime-H-nDodRy.d.cts} +86 -6
- package/dist/{runtime-B50yywI_.d.ts → runtime-H-nDodRy.d.ts} +86 -6
- package/dist/runtime.cjs +1140 -482
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1127 -463
- package/dist/runtime.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -61,6 +61,7 @@ __export(index_exports, {
|
|
|
61
61
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
62
62
|
DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
63
63
|
DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
64
|
+
DASHBOARD_FILTER_FIELD_UNPROVISIONED: () => DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
64
65
|
DEFAULT_AGENT_OUTSIDE_ROSTER: () => DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
65
66
|
EXPRESSION_INVALID: () => EXPRESSION_INVALID,
|
|
66
67
|
FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
|
|
@@ -87,6 +88,7 @@ __export(index_exports, {
|
|
|
87
88
|
FLOW_NODE_WRITE_UNKNOWN_FIELD: () => FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
88
89
|
FLOW_PHANTOM_AGGREGATION: () => FLOW_PHANTOM_AGGREGATION,
|
|
89
90
|
FLOW_RUNAS_UNSCOPED: () => FLOW_RUNAS_UNSCOPED,
|
|
91
|
+
FLOW_TEMPLATE_FIELD_UNPROVISIONED: () => FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
90
92
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL: () => FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
91
93
|
FLOW_TEMPLATE_UNKNOWN_FIELD: () => FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
92
94
|
FLOW_TIME_RELATIVE_ANTIPATTERN: () => FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
@@ -108,9 +110,11 @@ __export(index_exports, {
|
|
|
108
110
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE: () => LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
109
111
|
LIVENESS_DEAD_PROPERTY: () => LIVENESS_DEAD_PROPERTY,
|
|
110
112
|
LIVENESS_EXPERIMENTAL_PROPERTY: () => LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
113
|
+
MANAGED_API_METHOD_UNAFFORDABLE: () => MANAGED_API_METHOD_UNAFFORDABLE,
|
|
111
114
|
MAX_SCHEMA_WALK_DEPTH: () => MAX_SCHEMA_WALK_DEPTH,
|
|
112
115
|
MEASURE_AGGREGATE_INCOHERENT: () => MEASURE_AGGREGATE_INCOHERENT,
|
|
113
116
|
NAV_OBJECT_UNGRANTED: () => NAV_OBJECT_UNGRANTED,
|
|
117
|
+
NAV_OBJECT_UNSERVABLE: () => NAV_OBJECT_UNSERVABLE,
|
|
114
118
|
NAV_TARGET_UNRESOLVED: () => NAV_TARGET_UNRESOLVED,
|
|
115
119
|
NULL_GUARD_HINT: () => NULL_GUARD_HINT,
|
|
116
120
|
OBJECT_REFERENCE_UNKNOWN: () => OBJECT_REFERENCE_UNKNOWN,
|
|
@@ -119,15 +123,18 @@ __export(index_exports, {
|
|
|
119
123
|
ORG_AXIS_CROSS_ORG_BU_GRANT: () => ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
120
124
|
ORG_AXIS_PERMISSION_INHERITANCE: () => ORG_AXIS_PERMISSION_INHERITANCE,
|
|
121
125
|
PAGE_FIELD_UNKNOWN: () => PAGE_FIELD_UNKNOWN,
|
|
126
|
+
PAGE_FIELD_UNPROVISIONED: () => PAGE_FIELD_UNPROVISIONED,
|
|
122
127
|
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
123
128
|
PREDICATE_PATH_UNRESOLVED: () => PREDICATE_PATH_UNRESOLVED,
|
|
124
129
|
PREDICATE_PATH_UNROOTED: () => PREDICATE_PATH_UNROOTED,
|
|
130
|
+
PREDICATE_RHS_PATH_SHAPED: () => PREDICATE_RHS_PATH_SHAPED,
|
|
125
131
|
PRE_SEAL_PHASES: () => PRE_SEAL_PHASES,
|
|
126
132
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
127
133
|
REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
|
|
128
134
|
REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
|
|
129
135
|
REACT_CHART_DRILLDOWN_INVALID: () => REACT_CHART_DRILLDOWN_INVALID,
|
|
130
136
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
137
|
+
REACT_CHART_FIELD_UNPROVISIONED: () => REACT_CHART_FIELD_UNPROVISIONED,
|
|
131
138
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
132
139
|
RLS_PREDICATE_OVER_BUDGET: () => RLS_PREDICATE_OVER_BUDGET,
|
|
133
140
|
RLS_PREDICATE_UNENFORCEABLE: () => RLS_PREDICATE_UNENFORCEABLE,
|
|
@@ -138,6 +145,7 @@ __export(index_exports, {
|
|
|
138
145
|
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
139
146
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
140
147
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET: () => SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
148
|
+
SECURITY_CBP_NO_RELATION: () => SECURITY_CBP_NO_RELATION,
|
|
141
149
|
SECURITY_DELEGATION_MISSING_REASON: () => SECURITY_DELEGATION_MISSING_REASON,
|
|
142
150
|
SECURITY_EXTERNAL_WIDER: () => SECURITY_EXTERNAL_WIDER,
|
|
143
151
|
SECURITY_FLS_UNQUALIFIED_KEY: () => SECURITY_FLS_UNQUALIFIED_KEY,
|
|
@@ -151,6 +159,7 @@ __export(index_exports, {
|
|
|
151
159
|
SEED_INSERT_MODE_DUPLICATES_ON_REPLAY: () => SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
|
|
152
160
|
SEED_VALUE_OUTSIDE_STATE_MACHINE: () => SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
153
161
|
SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
162
|
+
SEMANTIC_ROLE_FIELD_UNPROVISIONED: () => SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
154
163
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
155
164
|
SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
156
165
|
STARTUP_OPEN_VOCABULARY_VERDICT: () => STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
@@ -188,6 +197,7 @@ __export(index_exports, {
|
|
|
188
197
|
WIDGET_MEASURE_UNKNOWN: () => WIDGET_MEASURE_UNKNOWN,
|
|
189
198
|
authoringRulesFor: () => authoringRulesFor,
|
|
190
199
|
buildAccessMatrix: () => buildAccessMatrix,
|
|
200
|
+
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
191
201
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
192
202
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
193
203
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
@@ -231,7 +241,9 @@ __export(index_exports, {
|
|
|
231
241
|
validateHookBodyWrites: () => validateHookBodyWrites,
|
|
232
242
|
validateJsxPages: () => validateJsxPages,
|
|
233
243
|
validateListViewMode: () => validateListViewMode,
|
|
244
|
+
validateManagedApiMethods: () => validateManagedApiMethods,
|
|
234
245
|
validateNavAccess: () => validateNavAccess,
|
|
246
|
+
validateNavObjectServability: () => validateNavObjectServability,
|
|
235
247
|
validateNavTargetRefs: () => validateNavTargetRefs,
|
|
236
248
|
validateObjectReferences: () => validateObjectReferences,
|
|
237
249
|
validateOrgAxisRedLines: () => validateOrgAxisRedLines,
|
|
@@ -249,6 +261,7 @@ __export(index_exports, {
|
|
|
249
261
|
validateRuleSchemaFormats: () => validateRuleSchemaFormats,
|
|
250
262
|
validateSearchableFields: () => validateSearchableFields,
|
|
251
263
|
validateSecurityPosture: () => validateSecurityPosture,
|
|
264
|
+
validateSecurityRoleWord: () => validateSecurityRoleWord,
|
|
252
265
|
validateSeedReplaySafety: () => validateSeedReplaySafety,
|
|
253
266
|
validateSeedStateMachine: () => validateSeedStateMachine,
|
|
254
267
|
validateSemanticRoles: () => validateSemanticRoles,
|
|
@@ -277,6 +290,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
277
290
|
function injectedColumnsFor(objectDef) {
|
|
278
291
|
return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
|
|
279
292
|
}
|
|
293
|
+
function unprovisionedInjectedColumnsFor(objectDef) {
|
|
294
|
+
return new Set((0, import_data.unprovisionedInjectedColumns)(objectDef));
|
|
295
|
+
}
|
|
296
|
+
function objectDefsOf(stack) {
|
|
297
|
+
if (!stack || typeof stack !== "object") return [];
|
|
298
|
+
const objects = stack.objects;
|
|
299
|
+
if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
|
|
300
|
+
if (objects && typeof objects === "object") {
|
|
301
|
+
return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
|
|
302
|
+
}
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
function indexUnprovisionedAnchors(stack) {
|
|
306
|
+
const index = /* @__PURE__ */ new Map();
|
|
307
|
+
for (const obj of objectDefsOf(stack)) {
|
|
308
|
+
const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
|
|
309
|
+
if (!name) continue;
|
|
310
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
311
|
+
if (anchors.size > 0) index.set(name, anchors);
|
|
312
|
+
}
|
|
313
|
+
return index;
|
|
314
|
+
}
|
|
315
|
+
function unprovisionedAnchorCause(objectName, field) {
|
|
316
|
+
return `'${field}' is an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column`;
|
|
317
|
+
}
|
|
318
|
+
function unprovisionedAnchorHint(objectName, field) {
|
|
319
|
+
return `If the remote table really carries '${field}', declare it in ${objectName}'s own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`;
|
|
320
|
+
}
|
|
280
321
|
|
|
281
322
|
// src/validate-widget-bindings.ts
|
|
282
323
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
@@ -289,6 +330,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
|
289
330
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
290
331
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
291
332
|
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
333
|
+
var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
|
|
292
334
|
var LEGACY_ANALYTICS_KEYS = [
|
|
293
335
|
"categoryField",
|
|
294
336
|
"valueField",
|
|
@@ -408,6 +450,7 @@ function validateWidgetBindings(stack) {
|
|
|
408
450
|
}
|
|
409
451
|
objectFieldTypes.set(o.name, fm);
|
|
410
452
|
}
|
|
453
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
411
454
|
const datasetList = asArray(stack.datasets);
|
|
412
455
|
for (let i = 0; i < datasetList.length; i++) {
|
|
413
456
|
const ds = datasetList[i];
|
|
@@ -494,13 +537,24 @@ function validateWidgetBindings(stack) {
|
|
|
494
537
|
if (dashFilterDefs.length > 0) {
|
|
495
538
|
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
496
539
|
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
497
|
-
|
|
540
|
+
const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
|
|
541
|
+
if (objectFields && datasetObject) {
|
|
498
542
|
for (const def of dashFilterDefs) {
|
|
499
543
|
const eff = effectiveFilterField(w, def);
|
|
500
544
|
if (!eff) continue;
|
|
501
545
|
const field = eff.field;
|
|
502
546
|
if (field.includes(".")) continue;
|
|
503
|
-
if (objectFields.has(field) || SYSTEM_FIELDS.has(field))
|
|
547
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
|
|
548
|
+
if (anchors?.has(field)) {
|
|
549
|
+
push2({
|
|
550
|
+
severity: "warning",
|
|
551
|
+
rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
552
|
+
message: (eff.explicit ? `binds dashboard filter \`${def.name}\` to field \`${field}\` (via filterBindings), but ` : `inherits dashboard filter \`${def.name}(${field})\`, but `) + `${unprovisionedAnchorCause(datasetObject, field)}. The filter is ANDed into this widget's analytics query (#2501), so it can never match a real value \u2014 on SQLite it silently degrades to constant-false and the widget renders empty (HTTP 200, zero rows, no error).`,
|
|
553
|
+
hint: `${unprovisionedAnchorHint(datasetObject, field)} A widget can also opt out with filterBindings: { ${def.name}: false }. Suppress with suppressWarnings: ['${DASHBOARD_FILTER_FIELD_UNPROVISIONED}'] if the remote schema resolves it some other way.`
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
504
558
|
push2({
|
|
505
559
|
severity: "error",
|
|
506
560
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
@@ -833,6 +887,37 @@ function buildFieldIndex(objects) {
|
|
|
833
887
|
}
|
|
834
888
|
return idx;
|
|
835
889
|
}
|
|
890
|
+
var BOUND_RECORD_ROOTS = ["record", "previous"];
|
|
891
|
+
function isCelNode(v) {
|
|
892
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
893
|
+
}
|
|
894
|
+
function collectBoundRecordReads(source) {
|
|
895
|
+
const out = /* @__PURE__ */ new Map();
|
|
896
|
+
const ast = (0, import_formula2.parseCelToAst)(source);
|
|
897
|
+
if (!ast) return out;
|
|
898
|
+
const pending = [ast];
|
|
899
|
+
while (pending.length > 0) {
|
|
900
|
+
const celNode = pending.pop();
|
|
901
|
+
if (!isCelNode(celNode)) continue;
|
|
902
|
+
if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
|
|
903
|
+
const [celRecv, seg] = celNode.args;
|
|
904
|
+
if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
|
|
905
|
+
if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const celArgs = celNode.args;
|
|
909
|
+
if (isCelNode(celArgs)) pending.push(celArgs);
|
|
910
|
+
else if (Array.isArray(celArgs)) {
|
|
911
|
+
for (const a of celArgs) {
|
|
912
|
+
if (isCelNode(a)) pending.push(a);
|
|
913
|
+
else if (Array.isArray(a)) {
|
|
914
|
+
for (const b of a) if (isCelNode(b)) pending.push(b);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return out;
|
|
920
|
+
}
|
|
836
921
|
function buildFieldTypeIndex(objects) {
|
|
837
922
|
const idx = /* @__PURE__ */ new Map();
|
|
838
923
|
for (const obj of objects) {
|
|
@@ -932,6 +1017,29 @@ function validateStackExpressions(stack) {
|
|
|
932
1017
|
const fieldIndex = buildFieldIndex(objects);
|
|
933
1018
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
934
1019
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
1020
|
+
const unprovisionedIndex = /* @__PURE__ */ new Map();
|
|
1021
|
+
for (const obj of objects) {
|
|
1022
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
1023
|
+
if (!name) continue;
|
|
1024
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
1025
|
+
if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
|
|
1026
|
+
}
|
|
1027
|
+
const warnUnprovisionedAnchors = (where, raw, objectName) => {
|
|
1028
|
+
if (!objectName) return;
|
|
1029
|
+
const anchors = unprovisionedIndex.get(objectName);
|
|
1030
|
+
if (!anchors) return;
|
|
1031
|
+
const source = celSourceOf(raw);
|
|
1032
|
+
if (!source) return;
|
|
1033
|
+
for (const [field, operand] of collectBoundRecordReads(source)) {
|
|
1034
|
+
if (!anchors.has(field)) continue;
|
|
1035
|
+
issues.push({
|
|
1036
|
+
where,
|
|
1037
|
+
message: `\`${operand}\` reads '${field}', an injected system column with NO storage behind it: '${objectName}' is an external object (ADR-0015), so the remote database owns its schema and the platform registers this anchor without provisioning a column. The predicate can never match a real value \u2014 on SQLite it silently degrades to constant-false (HTTP 200, zero rows, no error). If the remote table really carries this column, declare '${field}' in the object's own fields (mapped through the external binding's columnMap) so the reference resolves to a column you vouch for; otherwise drop the reference, or opt the object out of the injection (\`ownership: 'none'\` for the ownership anchors, \`systemFields: { audit: false }\` for the audit family).`,
|
|
1038
|
+
source,
|
|
1039
|
+
severity: "warning"
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
935
1043
|
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
936
1044
|
if (!objectName) return;
|
|
937
1045
|
const nullableFields = nullableIndex.get(objectName);
|
|
@@ -958,6 +1066,7 @@ function validateStackExpressions(stack) {
|
|
|
958
1066
|
);
|
|
959
1067
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
960
1068
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
1069
|
+
warnUnprovisionedAnchors(where, raw, objectName);
|
|
961
1070
|
};
|
|
962
1071
|
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
963
1072
|
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
@@ -1104,6 +1213,7 @@ function validateStackExpressions(stack) {
|
|
|
1104
1213
|
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
1105
1214
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
1106
1215
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
1216
|
+
warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
|
|
1107
1217
|
}
|
|
1108
1218
|
}
|
|
1109
1219
|
}
|
|
@@ -1651,6 +1761,43 @@ function validateFunctionalCompleteness(stack) {
|
|
|
1651
1761
|
return out;
|
|
1652
1762
|
}
|
|
1653
1763
|
|
|
1764
|
+
// src/validate-managed-api-methods.ts
|
|
1765
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
1766
|
+
var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
|
|
1767
|
+
var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
1768
|
+
function entriesOf2(v) {
|
|
1769
|
+
if (Array.isArray(v)) {
|
|
1770
|
+
return v.flatMap(
|
|
1771
|
+
(def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
if (isRec2(v)) {
|
|
1775
|
+
return Object.entries(v).flatMap(
|
|
1776
|
+
([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
return [];
|
|
1780
|
+
}
|
|
1781
|
+
function validateManagedApiMethods(stack) {
|
|
1782
|
+
const out = [];
|
|
1783
|
+
if (!isRec2(stack)) return out;
|
|
1784
|
+
for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
|
|
1785
|
+
const conflicts = (0, import_data3.checkManagedApiMethodAffordances)(obj.def);
|
|
1786
|
+
if (conflicts.length === 0) continue;
|
|
1787
|
+
const verbs = conflicts.map((c) => c.verb).join(", ");
|
|
1788
|
+
const flags = [...new Set(conflicts.map((c) => c.needs))];
|
|
1789
|
+
out.push({
|
|
1790
|
+
severity: "error",
|
|
1791
|
+
rule: MANAGED_API_METHOD_UNAFFORDABLE,
|
|
1792
|
+
where: `object "${obj.name}"`,
|
|
1793
|
+
path: `objects[${oi}].enable.apiMethods`,
|
|
1794
|
+
message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + (0, import_data3.describeManagedApiMethodConflicts)(conflicts) + ` The registry STRIPS [${verbs}] at registration, so this declaration and the API you actually get already disagree \u2014 today the only trace is a line in the boot log.`,
|
|
1795
|
+
hint: `Either add \`userActions: { ${flags.map((f) => `${f}: true`).join(", ")} }\` to the object \u2014 only if the write is genuinely one a user context may perform, and only once the guard enforcing it exists (ADR-0092 D4: affordance never ships ahead of the guard) \u2014 or remove [${verbs}] from \`enable.apiMethods\`, which is what the runtime does for you today.`
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1798
|
+
return out;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1654
1801
|
// src/validate-flow-trigger-readiness.ts
|
|
1655
1802
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
1656
1803
|
var FLOW_TRIGGER_UNKNOWN_OBJECT = "flow-trigger-unknown-object";
|
|
@@ -1835,7 +1982,7 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1835
1982
|
|
|
1836
1983
|
// src/flow-walk.ts
|
|
1837
1984
|
var import_automation3 = require("@objectstack/spec/automation");
|
|
1838
|
-
function
|
|
1985
|
+
function isRec3(v) {
|
|
1839
1986
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1840
1987
|
}
|
|
1841
1988
|
function strName(v) {
|
|
@@ -1850,7 +1997,7 @@ function flowNodeLabel(node, index) {
|
|
|
1850
1997
|
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
1851
1998
|
}
|
|
1852
1999
|
function stripRegions(config) {
|
|
1853
|
-
if (!
|
|
2000
|
+
if (!isRec3(config)) return void 0;
|
|
1854
2001
|
let out;
|
|
1855
2002
|
for (const key of Object.keys(config)) {
|
|
1856
2003
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -1861,11 +2008,11 @@ function stripRegions(config) {
|
|
|
1861
2008
|
}
|
|
1862
2009
|
function walkFlowNodes(flow, flowPath) {
|
|
1863
2010
|
const out = [];
|
|
1864
|
-
if (!
|
|
2011
|
+
if (!isRec3(flow)) return out;
|
|
1865
2012
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
1866
2013
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
1867
2014
|
nodes.forEach((raw, index) => {
|
|
1868
|
-
if (!
|
|
2015
|
+
if (!isRec3(raw)) return;
|
|
1869
2016
|
const path = `${basePath}[${index}]`;
|
|
1870
2017
|
out.push({
|
|
1871
2018
|
node: raw,
|
|
@@ -1876,7 +2023,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
1876
2023
|
});
|
|
1877
2024
|
const type = strName(raw.type);
|
|
1878
2025
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
1879
|
-
if (!slots || !
|
|
2026
|
+
if (!slots || !isRec3(raw.config)) return;
|
|
1880
2027
|
const config = raw.config;
|
|
1881
2028
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
1882
2029
|
for (const slot of slots) {
|
|
@@ -1884,7 +2031,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
1884
2031
|
if (slot === "branches") {
|
|
1885
2032
|
if (!Array.isArray(value)) continue;
|
|
1886
2033
|
value.forEach((branch, b) => {
|
|
1887
|
-
if (!
|
|
2034
|
+
if (!isRec3(branch)) return;
|
|
1888
2035
|
const branchName = strName(branch.name) ?? `#${b}`;
|
|
1889
2036
|
visitList(
|
|
1890
2037
|
branch.nodes,
|
|
@@ -1895,7 +2042,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
1895
2042
|
});
|
|
1896
2043
|
continue;
|
|
1897
2044
|
}
|
|
1898
|
-
if (!
|
|
2045
|
+
if (!isRec3(value)) continue;
|
|
1899
2046
|
visitList(
|
|
1900
2047
|
value.nodes,
|
|
1901
2048
|
`${path}.config.${slot}.nodes`,
|
|
@@ -1915,6 +2062,7 @@ function joinTrail(trail, segment) {
|
|
|
1915
2062
|
// src/validate-flow-template-paths.ts
|
|
1916
2063
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
1917
2064
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
2065
|
+
var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
|
|
1918
2066
|
function asArray5(v) {
|
|
1919
2067
|
if (Array.isArray(v)) return v;
|
|
1920
2068
|
if (v && typeof v === "object") {
|
|
@@ -2057,6 +2205,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
2057
2205
|
const obj = objectsByName.get(objectName);
|
|
2058
2206
|
if (!obj) return;
|
|
2059
2207
|
const fieldTypes = fieldTypesOf(obj);
|
|
2208
|
+
const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
|
|
2060
2209
|
const expandSet = declaredExpandOf(flow);
|
|
2061
2210
|
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
2062
2211
|
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
@@ -2068,6 +2217,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
2068
2217
|
if (leaves.length === 0) return;
|
|
2069
2218
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
2070
2219
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
2220
|
+
const seenUnprovisioned = /* @__PURE__ */ new Set();
|
|
2071
2221
|
for (const leaf of leaves) {
|
|
2072
2222
|
const inFilter = leaf.inFilter;
|
|
2073
2223
|
for (const rest of recordRefsIn(leaf.text)) {
|
|
@@ -2075,6 +2225,19 @@ function validateFlowTemplatePaths(stack) {
|
|
|
2075
2225
|
const hasSubPath = rest.length > 1;
|
|
2076
2226
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
2077
2227
|
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
2228
|
+
if (unprovisionedAnchors.has(head)) {
|
|
2229
|
+
if (!seenUnprovisioned.has(head)) {
|
|
2230
|
+
seenUnprovisioned.add(head);
|
|
2231
|
+
findings.push({
|
|
2232
|
+
severity: "warning",
|
|
2233
|
+
rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
2234
|
+
where,
|
|
2235
|
+
path: nodePath,
|
|
2236
|
+
message: (inFilter ? `${nodeType} filter references ` : "template references ") + `'{record.${rest.join(".")}}', and ${unprovisionedAnchorCause(objectName, head)} \u2014 ` + (inFilter ? `the token resolves to nothing on every run, which DROPS the condition from the query instead of narrowing it; the node then refuses to run at execution time (#3810).` : `the token resolves to an empty string on every run (silently).`),
|
|
2237
|
+
hint: unprovisionedAnchorHint(objectName, head)
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2078
2241
|
if (!isKnown) {
|
|
2079
2242
|
if (seenUnknown.has(head)) continue;
|
|
2080
2243
|
seenUnknown.add(head);
|
|
@@ -2219,10 +2382,32 @@ function containerViewCount(rec) {
|
|
|
2219
2382
|
function validateViewContainers(stack) {
|
|
2220
2383
|
const out = [];
|
|
2221
2384
|
if (!stack || typeof stack !== "object") return out;
|
|
2385
|
+
const viewItems = stack.viewItems;
|
|
2386
|
+
if (viewItems != null && asEntries(viewItems).length > 0) {
|
|
2387
|
+
out.push({
|
|
2388
|
+
severity: "error",
|
|
2389
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
2390
|
+
where: "viewItems",
|
|
2391
|
+
path: "viewItems",
|
|
2392
|
+
message: "`viewItems` is the machine-assembled channel for non-container view artifacts in runtime-assembled manifests (package export, environment artifacts) \u2014 it is not an authoring surface.",
|
|
2393
|
+
hint: "Author views as defineView containers in `views:`; author a standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`), not in stack source."
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2222
2396
|
for (const { key, value } of asEntries(stack.views)) {
|
|
2223
2397
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
2224
2398
|
const rec = value;
|
|
2225
|
-
if (rec.viewKind != null)
|
|
2399
|
+
if (rec.viewKind != null) {
|
|
2400
|
+
const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
2401
|
+
out.push({
|
|
2402
|
+
severity: "error",
|
|
2403
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
2404
|
+
where: `views${key}${label3}`,
|
|
2405
|
+
path: `views${key}`,
|
|
2406
|
+
message: "A ViewItem record is not a view container: the stack `views:` collection carries containers only \u2014 `viewKind` belongs to a single VIEW, not to the container. The registration loop refuses this entry (#5320).",
|
|
2407
|
+
hint: "Wrap it in a defineView container: defineView({ list: { type, data, columns, ... }, listViews: { ... } }) \u2014 or author the standalone view through the metadata door (Studio / `PUT /api/v1/meta/view`). Machine-assembled manifests carry it under `viewItems:`."
|
|
2408
|
+
});
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2226
2411
|
if (containerViewCount(rec) > 0) continue;
|
|
2227
2412
|
const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
2228
2413
|
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
@@ -2644,10 +2829,10 @@ function validateReactPages(stack) {
|
|
|
2644
2829
|
// src/validate-react-page-props.ts
|
|
2645
2830
|
var import_node_module3 = require("module");
|
|
2646
2831
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
2647
|
-
var
|
|
2832
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
2648
2833
|
|
|
2649
2834
|
// src/validate-searchable-fields.ts
|
|
2650
|
-
var
|
|
2835
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
2651
2836
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
2652
2837
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
2653
2838
|
function asArray10(v) {
|
|
@@ -2657,7 +2842,7 @@ function asArray10(v) {
|
|
|
2657
2842
|
}
|
|
2658
2843
|
return [];
|
|
2659
2844
|
}
|
|
2660
|
-
function
|
|
2845
|
+
function isRec4(v) {
|
|
2661
2846
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2662
2847
|
}
|
|
2663
2848
|
function strName2(v) {
|
|
@@ -2695,7 +2880,7 @@ function resolveAllowedSet(target) {
|
|
|
2695
2880
|
fields = { ...fields };
|
|
2696
2881
|
for (const f of systemDeclared) fields[f] = {};
|
|
2697
2882
|
}
|
|
2698
|
-
const { allowed, source } = (0,
|
|
2883
|
+
const { allowed, source } = (0, import_data4.resolveSearchFieldResolution)({
|
|
2699
2884
|
fields,
|
|
2700
2885
|
searchableFields: target.searchableFields,
|
|
2701
2886
|
displayField: target.displayField
|
|
@@ -2733,7 +2918,7 @@ function distance(a, b) {
|
|
|
2733
2918
|
}
|
|
2734
2919
|
function indexObjectSearchTargets(stack) {
|
|
2735
2920
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
2736
|
-
if (!
|
|
2921
|
+
if (!isRec4(stack)) return fieldsByObject;
|
|
2737
2922
|
for (const obj of asArray10(stack.objects)) {
|
|
2738
2923
|
const name = strName2(obj.name);
|
|
2739
2924
|
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
@@ -2765,7 +2950,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2765
2950
|
});
|
|
2766
2951
|
continue;
|
|
2767
2952
|
}
|
|
2768
|
-
if ((0,
|
|
2953
|
+
if ((0, import_data4.isVirtualSearchField)(target.fields[name])) {
|
|
2769
2954
|
const vtype = target.fields[name]?.type;
|
|
2770
2955
|
findings.push({
|
|
2771
2956
|
severity: "error",
|
|
@@ -2793,7 +2978,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2793
2978
|
}
|
|
2794
2979
|
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
2795
2980
|
let why;
|
|
2796
|
-
if (
|
|
2981
|
+
if (import_data4.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
2797
2982
|
why = "a system/audit column, which the auto-default set never includes";
|
|
2798
2983
|
} else if (meta?.hidden) {
|
|
2799
2984
|
why = "hidden";
|
|
@@ -2807,7 +2992,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2807
2992
|
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
2808
2993
|
where,
|
|
2809
2994
|
path: `${path}[${i}]`,
|
|
2810
|
-
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...
|
|
2995
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data4.SEARCHABLE_TEXTUAL_TYPES, ...import_data4.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
2811
2996
|
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
|
|
2812
2997
|
});
|
|
2813
2998
|
}
|
|
@@ -2815,7 +3000,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2815
3000
|
}
|
|
2816
3001
|
function validateSearchableFields(stack) {
|
|
2817
3002
|
const findings = [];
|
|
2818
|
-
if (!
|
|
3003
|
+
if (!isRec4(stack)) return findings;
|
|
2819
3004
|
const objects = asArray10(stack.objects);
|
|
2820
3005
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
2821
3006
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
@@ -2825,7 +3010,7 @@ function validateSearchableFields(stack) {
|
|
|
2825
3010
|
};
|
|
2826
3011
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2827
3012
|
const obj = objects[oi];
|
|
2828
|
-
if (!
|
|
3013
|
+
if (!isRec4(obj)) continue;
|
|
2829
3014
|
const objName = strName2(obj.name);
|
|
2830
3015
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
2831
3016
|
check(
|
|
@@ -2836,9 +3021,9 @@ function validateSearchableFields(stack) {
|
|
|
2836
3021
|
"searchableFields",
|
|
2837
3022
|
"canonical"
|
|
2838
3023
|
);
|
|
2839
|
-
if (
|
|
3024
|
+
if (isRec4(obj.listViews)) {
|
|
2840
3025
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2841
|
-
if (!
|
|
3026
|
+
if (!isRec4(lv)) continue;
|
|
2842
3027
|
check(
|
|
2843
3028
|
lv.searchableFields,
|
|
2844
3029
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -2855,10 +3040,10 @@ function validateSearchableFields(stack) {
|
|
|
2855
3040
|
const views = asArray10(stack.views);
|
|
2856
3041
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2857
3042
|
const view = views[vi];
|
|
2858
|
-
if (!
|
|
3043
|
+
if (!isRec4(view)) continue;
|
|
2859
3044
|
const viewLabel2 = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
2860
3045
|
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
2861
|
-
if (
|
|
3046
|
+
if (isRec4(view.list)) {
|
|
2862
3047
|
check(
|
|
2863
3048
|
view.list.searchableFields,
|
|
2864
3049
|
listViewObject(view.list) ?? viewObject,
|
|
@@ -2868,9 +3053,9 @@ function validateSearchableFields(stack) {
|
|
|
2868
3053
|
"narrowing"
|
|
2869
3054
|
);
|
|
2870
3055
|
}
|
|
2871
|
-
if (
|
|
3056
|
+
if (isRec4(view.listViews)) {
|
|
2872
3057
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2873
|
-
if (!
|
|
3058
|
+
if (!isRec4(lv)) continue;
|
|
2874
3059
|
check(
|
|
2875
3060
|
lv.searchableFields,
|
|
2876
3061
|
listViewObject(lv) ?? viewObject,
|
|
@@ -2886,11 +3071,11 @@ function validateSearchableFields(stack) {
|
|
|
2886
3071
|
}
|
|
2887
3072
|
function listViewObject(listView) {
|
|
2888
3073
|
const data = listView.data;
|
|
2889
|
-
return
|
|
3074
|
+
return isRec4(data) ? strName2(data.object) : void 0;
|
|
2890
3075
|
}
|
|
2891
3076
|
|
|
2892
3077
|
// src/page-walk.ts
|
|
2893
|
-
function
|
|
3078
|
+
function isRec5(v) {
|
|
2894
3079
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2895
3080
|
}
|
|
2896
3081
|
function strName3(v) {
|
|
@@ -2903,19 +3088,19 @@ function isSourceAuthoredPage(page) {
|
|
|
2903
3088
|
}
|
|
2904
3089
|
function walkPageComponents(page, pagePath) {
|
|
2905
3090
|
const out = [];
|
|
2906
|
-
if (!
|
|
3091
|
+
if (!isRec5(page) || isSourceAuthoredPage(page)) return out;
|
|
2907
3092
|
const pageObject = strName3(page.object);
|
|
2908
3093
|
const visit = (node, path, inheritedObject) => {
|
|
2909
|
-
if (!
|
|
2910
|
-
const props =
|
|
2911
|
-
const dataSource =
|
|
3094
|
+
if (!isRec5(node)) return;
|
|
3095
|
+
const props = isRec5(node.properties) ? node.properties : void 0;
|
|
3096
|
+
const dataSource = isRec5(node.dataSource) ? node.dataSource : void 0;
|
|
2912
3097
|
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
2913
3098
|
out.push({ component: node, path, objectName });
|
|
2914
3099
|
if (!props) return;
|
|
2915
3100
|
if (Array.isArray(props.items)) {
|
|
2916
3101
|
for (let i = 0; i < props.items.length; i++) {
|
|
2917
3102
|
const item = props.items[i];
|
|
2918
|
-
if (!
|
|
3103
|
+
if (!isRec5(item) || !Array.isArray(item.children)) continue;
|
|
2919
3104
|
for (let c = 0; c < item.children.length; c++) {
|
|
2920
3105
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
2921
3106
|
}
|
|
@@ -2937,12 +3122,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
2937
3122
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
2938
3123
|
for (let r = 0; r < regions.length; r++) {
|
|
2939
3124
|
const region = regions[r];
|
|
2940
|
-
if (!
|
|
3125
|
+
if (!isRec5(region) || !Array.isArray(region.components)) continue;
|
|
2941
3126
|
for (let c = 0; c < region.components.length; c++) {
|
|
2942
3127
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
2943
3128
|
}
|
|
2944
3129
|
}
|
|
2945
|
-
const slots =
|
|
3130
|
+
const slots = isRec5(page.slots) ? page.slots : void 0;
|
|
2946
3131
|
if (slots) {
|
|
2947
3132
|
for (const [slot, value] of Object.entries(slots)) {
|
|
2948
3133
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2957,6 +3142,7 @@ function walkPageComponents(page, pagePath) {
|
|
|
2957
3142
|
|
|
2958
3143
|
// src/validate-page-field-bindings.ts
|
|
2959
3144
|
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
3145
|
+
var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
|
|
2960
3146
|
function asArray11(v) {
|
|
2961
3147
|
if (Array.isArray(v)) return v;
|
|
2962
3148
|
if (v && typeof v === "object") {
|
|
@@ -2967,7 +3153,7 @@ function asArray11(v) {
|
|
|
2967
3153
|
function strName4(v) {
|
|
2968
3154
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2969
3155
|
}
|
|
2970
|
-
function
|
|
3156
|
+
function isRec6(v) {
|
|
2971
3157
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2972
3158
|
}
|
|
2973
3159
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -2978,7 +3164,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
2978
3164
|
out.push({ name: bare, path });
|
|
2979
3165
|
return;
|
|
2980
3166
|
}
|
|
2981
|
-
if (!
|
|
3167
|
+
if (!isRec6(v)) return;
|
|
2982
3168
|
const named = strName4(v.field) ?? strName4(v.name);
|
|
2983
3169
|
if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
|
|
2984
3170
|
};
|
|
@@ -3036,15 +3222,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
3036
3222
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
3037
3223
|
for (let si = 0; si < sections.length; si++) {
|
|
3038
3224
|
const section = sections[si];
|
|
3039
|
-
if (!
|
|
3225
|
+
if (!isRec6(section)) continue;
|
|
3040
3226
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
3041
3227
|
}
|
|
3042
3228
|
}
|
|
3043
3229
|
return refs;
|
|
3044
3230
|
}
|
|
3045
3231
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
3046
|
-
const add =
|
|
3047
|
-
const picker = add &&
|
|
3232
|
+
const add = isRec6(props.add) ? props.add : void 0;
|
|
3233
|
+
const picker = add && isRec6(add.picker) ? add.picker : void 0;
|
|
3048
3234
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
3049
3235
|
return {
|
|
3050
3236
|
relatedObject: strName4(props.objectName),
|
|
@@ -3065,7 +3251,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
3065
3251
|
}
|
|
3066
3252
|
function indexObjectFields(stack) {
|
|
3067
3253
|
const objectFields = /* @__PURE__ */ new Map();
|
|
3068
|
-
if (!
|
|
3254
|
+
if (!isRec6(stack)) return objectFields;
|
|
3069
3255
|
for (const obj of asArray11(stack.objects)) {
|
|
3070
3256
|
const name = strName4(obj.name);
|
|
3071
3257
|
if (!name) continue;
|
|
@@ -3078,14 +3264,27 @@ function indexObjectFields(stack) {
|
|
|
3078
3264
|
}
|
|
3079
3265
|
return objectFields;
|
|
3080
3266
|
}
|
|
3081
|
-
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
3267
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
|
|
3082
3268
|
const findings = [];
|
|
3083
3269
|
if (!objectName) return findings;
|
|
3084
3270
|
const known = objectFields.get(objectName);
|
|
3085
3271
|
if (!known) return findings;
|
|
3272
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
3086
3273
|
for (const ref of refs) {
|
|
3087
3274
|
if (ref.name.includes(".")) continue;
|
|
3088
|
-
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name))
|
|
3275
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
|
|
3276
|
+
if (anchors?.has(ref.name)) {
|
|
3277
|
+
findings.push({
|
|
3278
|
+
severity: "warning",
|
|
3279
|
+
rule: PAGE_FIELD_UNPROVISIONED,
|
|
3280
|
+
where,
|
|
3281
|
+
path: ref.path,
|
|
3282
|
+
message: `field "${ref.name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, ref.name)}` + (consequence2 === "queried" ? ' \u2014 it is used in a QUERY, so the predicate can never match a real value: on SQLite it silently degrades to constant-false and the surface renders an empty result that looks exactly like "there is no data".' : " \u2014 the component renders it, blank, on every record."),
|
|
3283
|
+
hint: unprovisionedAnchorHint(objectName, ref.name)
|
|
3284
|
+
});
|
|
3285
|
+
}
|
|
3286
|
+
continue;
|
|
3287
|
+
}
|
|
3089
3288
|
findings.push({
|
|
3090
3289
|
severity: consequence2 === "queried" ? "error" : "warning",
|
|
3091
3290
|
rule: PAGE_FIELD_UNKNOWN,
|
|
@@ -3101,17 +3300,20 @@ function validatePageFieldBindings(stack) {
|
|
|
3101
3300
|
const findings = [];
|
|
3102
3301
|
if (!stack || typeof stack !== "object") return findings;
|
|
3103
3302
|
const objectFields = indexObjectFields(stack);
|
|
3303
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
3104
3304
|
const pages = asArray11(stack.pages);
|
|
3105
3305
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3106
3306
|
const page = pages[pi];
|
|
3107
3307
|
if (!page || typeof page !== "object") continue;
|
|
3108
3308
|
const pageName = strName4(page.name) ?? `#${pi}`;
|
|
3109
3309
|
const checkRefs = (refs, objectName, where) => {
|
|
3110
|
-
findings.push(
|
|
3310
|
+
findings.push(
|
|
3311
|
+
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
3312
|
+
);
|
|
3111
3313
|
};
|
|
3112
3314
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3113
3315
|
const type = strName4(component.type);
|
|
3114
|
-
const props =
|
|
3316
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
3115
3317
|
if (!type || !props) continue;
|
|
3116
3318
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
3117
3319
|
const base = `${path}.properties`;
|
|
@@ -3126,7 +3328,7 @@ function validatePageFieldBindings(stack) {
|
|
|
3126
3328
|
if (!refs) continue;
|
|
3127
3329
|
checkRefs(refs, objectName, where);
|
|
3128
3330
|
}
|
|
3129
|
-
const cfg =
|
|
3331
|
+
const cfg = isRec6(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
3130
3332
|
if (cfg) {
|
|
3131
3333
|
const cfgObject = strName4(cfg.source) ?? strName4(page.object);
|
|
3132
3334
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -3135,7 +3337,7 @@ function validatePageFieldBindings(stack) {
|
|
|
3135
3337
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
3136
3338
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
3137
3339
|
];
|
|
3138
|
-
const userFilters =
|
|
3340
|
+
const userFilters = isRec6(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
3139
3341
|
if (userFilters) {
|
|
3140
3342
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
3141
3343
|
}
|
|
@@ -3146,11 +3348,11 @@ function validatePageFieldBindings(stack) {
|
|
|
3146
3348
|
}
|
|
3147
3349
|
|
|
3148
3350
|
// src/zod-issue-format.ts
|
|
3149
|
-
var
|
|
3351
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3150
3352
|
var valueAtPath = (root, path) => {
|
|
3151
3353
|
let cur = root;
|
|
3152
3354
|
for (const key of path) {
|
|
3153
|
-
if (!
|
|
3355
|
+
if (!isRec7(cur) && !Array.isArray(cur)) return void 0;
|
|
3154
3356
|
cur = cur[key];
|
|
3155
3357
|
}
|
|
3156
3358
|
return cur;
|
|
@@ -3284,12 +3486,13 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
3284
3486
|
return perPosition(init.expression);
|
|
3285
3487
|
}
|
|
3286
3488
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
3489
|
+
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
3287
3490
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
3288
3491
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
3289
3492
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
3290
3493
|
function checkChartDrillDown(raw, push2) {
|
|
3291
3494
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
3292
|
-
if (!
|
|
3495
|
+
if (!isRec8(raw)) {
|
|
3293
3496
|
push2(
|
|
3294
3497
|
"error",
|
|
3295
3498
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -3312,7 +3515,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
3312
3515
|
}
|
|
3313
3516
|
function checkChartAggregate(raw, push2) {
|
|
3314
3517
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
3315
|
-
if (!
|
|
3518
|
+
if (!isRec8(raw)) {
|
|
3316
3519
|
push2(
|
|
3317
3520
|
"error",
|
|
3318
3521
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -3343,9 +3546,9 @@ function checkChartAggregate(raw, push2) {
|
|
|
3343
3546
|
);
|
|
3344
3547
|
}
|
|
3345
3548
|
}
|
|
3346
|
-
var
|
|
3549
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
3347
3550
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3348
|
-
function checkObjectChart(attrs, objectFields, findings) {
|
|
3551
|
+
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
3349
3552
|
const { values, where, path } = attrs;
|
|
3350
3553
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
3351
3554
|
checkChartDrillDown(values.get("drillDown"), push2);
|
|
@@ -3353,18 +3556,29 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
3353
3556
|
const aggregate = values.get("aggregate");
|
|
3354
3557
|
checkChartAggregate(aggregate, push2);
|
|
3355
3558
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
3356
|
-
if (!
|
|
3559
|
+
if (!isRec8(aggregate)) return;
|
|
3357
3560
|
const fn = strOf(aggregate.function);
|
|
3358
3561
|
const field = strOf(aggregate.field);
|
|
3359
3562
|
const groupBy = aggregate.groupBy;
|
|
3360
|
-
const groupByField = strOf(groupBy) ?? (
|
|
3563
|
+
const groupByField = strOf(groupBy) ?? (isRec8(groupBy) ? strOf(groupBy.field) : void 0);
|
|
3361
3564
|
const objectName = strOf(values.get("objectName"));
|
|
3362
3565
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
3363
3566
|
if (objectName && known) {
|
|
3567
|
+
const anchors = unprovisionedAnchors.get(objectName);
|
|
3364
3568
|
const fieldRef = (name, prop) => {
|
|
3365
3569
|
if (!name) return;
|
|
3366
3570
|
if (name.includes(".")) return;
|
|
3367
|
-
if (known.has(name) || SYSTEM_FIELDS.has(name))
|
|
3571
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) {
|
|
3572
|
+
if (anchors?.has(name)) {
|
|
3573
|
+
push2(
|
|
3574
|
+
"warning",
|
|
3575
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
3576
|
+
`aggregate.${prop} "${name}" resolves on object "${objectName}", but ${unprovisionedAnchorCause(objectName, name)} \u2014 the aggregate query reads a column that is empty on every row, so the chart ${prop === "groupBy" ? "groups everything into one empty bucket" : "aggregates nothing"} instead of failing.`,
|
|
3577
|
+
unprovisionedAnchorHint(objectName, name)
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3368
3582
|
push2(
|
|
3369
3583
|
"error",
|
|
3370
3584
|
REACT_CHART_FIELD_UNKNOWN,
|
|
@@ -3390,18 +3604,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
3390
3604
|
);
|
|
3391
3605
|
};
|
|
3392
3606
|
const xAxisRaw = values.get("xAxis");
|
|
3393
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
3607
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec8(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
3394
3608
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
3395
3609
|
axisRef(categoryAxis, categoryProp);
|
|
3396
3610
|
const yAxisRaw = values.get("yAxis");
|
|
3397
3611
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
3398
3612
|
for (const a of yAxisList) {
|
|
3399
|
-
axisRef(strOf(a) ?? (
|
|
3613
|
+
axisRef(strOf(a) ?? (isRec8(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
3400
3614
|
}
|
|
3401
3615
|
const series = values.get("series");
|
|
3402
3616
|
if (Array.isArray(series)) {
|
|
3403
3617
|
for (const s of series) {
|
|
3404
|
-
if (!
|
|
3618
|
+
if (!isRec8(s)) continue;
|
|
3405
3619
|
const dataKey = strOf(s.dataKey);
|
|
3406
3620
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
3407
3621
|
}
|
|
@@ -3457,7 +3671,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
3457
3671
|
if (!Array.isArray(value)) return { child, parent };
|
|
3458
3672
|
for (let i = 0; i < value.length; i++) {
|
|
3459
3673
|
const sub = value[i];
|
|
3460
|
-
if (!
|
|
3674
|
+
if (!isRec8(sub)) continue;
|
|
3461
3675
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
3462
3676
|
child.push({
|
|
3463
3677
|
objectName: strOf(sub.childObject),
|
|
@@ -3482,7 +3696,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
3482
3696
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
3483
3697
|
return;
|
|
3484
3698
|
}
|
|
3485
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
3699
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data5.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
3486
3700
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
3487
3701
|
}
|
|
3488
3702
|
}
|
|
@@ -3502,20 +3716,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
3502
3716
|
}
|
|
3503
3717
|
for (const key of spec.nestedFields ?? []) {
|
|
3504
3718
|
const v = readable(key);
|
|
3505
|
-
if (
|
|
3719
|
+
if (isRec8(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
3506
3720
|
}
|
|
3507
3721
|
for (const key of spec.sections ?? []) {
|
|
3508
3722
|
const v = readable(key);
|
|
3509
3723
|
if (!Array.isArray(v)) continue;
|
|
3510
3724
|
for (let i = 0; i < v.length; i++) {
|
|
3511
3725
|
const section = v[i];
|
|
3512
|
-
if (!
|
|
3726
|
+
if (!isRec8(section)) continue;
|
|
3513
3727
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
3514
3728
|
}
|
|
3515
3729
|
}
|
|
3516
3730
|
for (const key of spec.keyedByField ?? []) {
|
|
3517
3731
|
const v = readable(key);
|
|
3518
|
-
if (!
|
|
3732
|
+
if (!isRec8(v)) continue;
|
|
3519
3733
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
3520
3734
|
}
|
|
3521
3735
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -3523,22 +3737,30 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
3523
3737
|
}
|
|
3524
3738
|
return { own, queried };
|
|
3525
3739
|
}
|
|
3526
|
-
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
3740
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
|
|
3527
3741
|
const objectName = strOf(values.get("objectName"));
|
|
3528
3742
|
const out = [];
|
|
3529
3743
|
const spec = REACT_FIELD_SPECS[tag];
|
|
3530
3744
|
if (spec) {
|
|
3531
3745
|
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
3532
|
-
out.push(
|
|
3533
|
-
|
|
3746
|
+
out.push(
|
|
3747
|
+
...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
3748
|
+
);
|
|
3749
|
+
out.push(
|
|
3750
|
+
...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
|
|
3751
|
+
);
|
|
3534
3752
|
}
|
|
3535
3753
|
if (tag === "ObjectForm") {
|
|
3536
3754
|
const raw = values.get("subforms");
|
|
3537
3755
|
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
3538
3756
|
for (const sub of subs.child) {
|
|
3539
|
-
out.push(
|
|
3757
|
+
out.push(
|
|
3758
|
+
...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
3759
|
+
);
|
|
3540
3760
|
}
|
|
3541
|
-
out.push(
|
|
3761
|
+
out.push(
|
|
3762
|
+
...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
3763
|
+
);
|
|
3542
3764
|
}
|
|
3543
3765
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
3544
3766
|
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
@@ -3547,7 +3769,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
3547
3769
|
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
3548
3770
|
objectName,
|
|
3549
3771
|
objectFields,
|
|
3550
|
-
where
|
|
3772
|
+
where,
|
|
3773
|
+
"skipped",
|
|
3774
|
+
unprovisionedAnchors
|
|
3551
3775
|
)
|
|
3552
3776
|
);
|
|
3553
3777
|
}
|
|
@@ -3579,6 +3803,7 @@ function localComponentNames(tsc, sf) {
|
|
|
3579
3803
|
function validateReactPageProps(stack) {
|
|
3580
3804
|
const findings = [];
|
|
3581
3805
|
const objectFields = indexObjectFields(stack);
|
|
3806
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
3582
3807
|
const searchTargets = indexObjectSearchTargets(stack);
|
|
3583
3808
|
const pages = asArray12(stack.pages);
|
|
3584
3809
|
for (let p = 0; p < pages.length; p++) {
|
|
@@ -3661,7 +3886,7 @@ function validateReactPageProps(stack) {
|
|
|
3661
3886
|
}
|
|
3662
3887
|
}
|
|
3663
3888
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
3664
|
-
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
3889
|
+
checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
|
|
3665
3890
|
}
|
|
3666
3891
|
if (tag === "ListView" && !hasSpread) {
|
|
3667
3892
|
findings.push(
|
|
@@ -3677,7 +3902,7 @@ function validateReactPageProps(stack) {
|
|
|
3677
3902
|
}
|
|
3678
3903
|
if (!hasSpread) {
|
|
3679
3904
|
findings.push(
|
|
3680
|
-
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
3905
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
|
|
3681
3906
|
);
|
|
3682
3907
|
}
|
|
3683
3908
|
}
|
|
@@ -3721,7 +3946,7 @@ function validatePageSourceStyling(stack) {
|
|
|
3721
3946
|
}
|
|
3722
3947
|
|
|
3723
3948
|
// src/validate-record-title.ts
|
|
3724
|
-
var
|
|
3949
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
3725
3950
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
3726
3951
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
3727
3952
|
function asArray14(v) {
|
|
@@ -3749,7 +3974,7 @@ function validateRecordTitle(stack) {
|
|
|
3749
3974
|
hint: `titleFormat is a render-only template the server cannot return or query, and an explicit nameField now takes precedence. For a single-field title set nameField: '<field>'. For a composite title, add a formula field (returnType: 'text') and designate it via nameField.`
|
|
3750
3975
|
});
|
|
3751
3976
|
}
|
|
3752
|
-
const completeness = (0,
|
|
3977
|
+
const completeness = (0, import_data6.objectTitleCompleteness)(obj);
|
|
3753
3978
|
if (completeness.status === "none") {
|
|
3754
3979
|
findings.push({
|
|
3755
3980
|
severity: "warning",
|
|
@@ -3769,6 +3994,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
3769
3994
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
3770
3995
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
3771
3996
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
3997
|
+
var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
|
|
3772
3998
|
function asArray15(v) {
|
|
3773
3999
|
if (Array.isArray(v)) return v;
|
|
3774
4000
|
if (v && typeof v === "object") {
|
|
@@ -3787,6 +4013,15 @@ function validateSemanticRoles(stack) {
|
|
|
3787
4013
|
const path = `objects[${i}]`;
|
|
3788
4014
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
3789
4015
|
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
4016
|
+
const unprovisioned = unprovisionedInjectedColumnsFor(obj);
|
|
4017
|
+
const unprovisionedPointer = (slot, entry) => ({
|
|
4018
|
+
severity: "warning",
|
|
4019
|
+
rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
4020
|
+
where,
|
|
4021
|
+
path: `${path}.${slot}`,
|
|
4022
|
+
message: `${objName}: ${slot} points at "${entry}", an injected system column with no storage behind it \u2014 this object is external (ADR-0015), so the platform registers the anchor but the remote schema owns the table and no column backs it. Every consumer renders it empty on every record.`,
|
|
4023
|
+
hint: `If the remote table really carries "${entry}", declare it in the object's own fields (mapped through the external binding's columnMap); otherwise point ${slot} at a real remote column.`
|
|
4024
|
+
});
|
|
3790
4025
|
const declaredGroups = new Set(
|
|
3791
4026
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
3792
4027
|
);
|
|
@@ -3828,10 +4063,16 @@ function validateSemanticRoles(stack) {
|
|
|
3828
4063
|
message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
|
|
3829
4064
|
hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
|
|
3830
4065
|
});
|
|
4066
|
+
} else if (typeof stage === "string" && unprovisioned.has(stage)) {
|
|
4067
|
+
findings.push(unprovisionedPointer("stageField", stage));
|
|
3831
4068
|
}
|
|
3832
4069
|
const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
|
|
3833
4070
|
for (const entry of highlights) {
|
|
3834
|
-
if (typeof entry !== "string" || entry.length === 0
|
|
4071
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
4072
|
+
if (fieldNames.has(entry)) {
|
|
4073
|
+
if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
3835
4076
|
findings.push({
|
|
3836
4077
|
severity: "warning",
|
|
3837
4078
|
rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -3871,45 +4112,45 @@ function validateSemanticRoles(stack) {
|
|
|
3871
4112
|
}
|
|
3872
4113
|
|
|
3873
4114
|
// src/collection-entries.ts
|
|
3874
|
-
function
|
|
4115
|
+
function isRec9(v) {
|
|
3875
4116
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3876
4117
|
}
|
|
3877
4118
|
function collectionEntries(v, base) {
|
|
3878
4119
|
if (Array.isArray(v)) {
|
|
3879
4120
|
const out = [];
|
|
3880
4121
|
for (let i = 0; i < v.length; i++) {
|
|
3881
|
-
if (
|
|
4122
|
+
if (isRec9(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3882
4123
|
}
|
|
3883
4124
|
return out;
|
|
3884
4125
|
}
|
|
3885
|
-
if (
|
|
3886
|
-
return Object.entries(v).filter(([, def]) =>
|
|
4126
|
+
if (isRec9(v)) {
|
|
4127
|
+
return Object.entries(v).filter(([, def]) => isRec9(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3887
4128
|
}
|
|
3888
4129
|
return [];
|
|
3889
4130
|
}
|
|
3890
4131
|
|
|
3891
4132
|
// src/view-walk.ts
|
|
3892
|
-
function
|
|
4133
|
+
function isRec10(v) {
|
|
3893
4134
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3894
4135
|
}
|
|
3895
4136
|
function strName5(v) {
|
|
3896
4137
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3897
4138
|
}
|
|
3898
4139
|
function viewObjectName(view) {
|
|
3899
|
-
return strName5(view.objectName) ?? strName5(view.object) ?? (
|
|
4140
|
+
return strName5(view.objectName) ?? strName5(view.object) ?? (isRec10(view.data) ? strName5(view.data.object) : void 0);
|
|
3900
4141
|
}
|
|
3901
4142
|
function viewContainerSites(view, basePath) {
|
|
3902
|
-
if (!
|
|
4143
|
+
if (!isRec10(view)) return [];
|
|
3903
4144
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3904
|
-
if (
|
|
4145
|
+
if (isRec10(view.form)) {
|
|
3905
4146
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3906
4147
|
}
|
|
3907
4148
|
for (const key of ["listViews", "formViews"]) {
|
|
3908
4149
|
const container = view[key];
|
|
3909
|
-
if (!
|
|
4150
|
+
if (!isRec10(container)) continue;
|
|
3910
4151
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
3911
4152
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
3912
|
-
if (!
|
|
4153
|
+
if (!isRec10(sub)) continue;
|
|
3913
4154
|
sites.push({
|
|
3914
4155
|
view: sub,
|
|
3915
4156
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -3934,7 +4175,7 @@ function asArray16(v) {
|
|
|
3934
4175
|
}
|
|
3935
4176
|
return [];
|
|
3936
4177
|
}
|
|
3937
|
-
function
|
|
4178
|
+
function isRec11(v) {
|
|
3938
4179
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3939
4180
|
}
|
|
3940
4181
|
function strName6(v) {
|
|
@@ -3968,7 +4209,7 @@ function validateFormLayout(stack) {
|
|
|
3968
4209
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
3969
4210
|
for (let s = 0; s < sections.length; s++) {
|
|
3970
4211
|
const sec = sections[s];
|
|
3971
|
-
const secFields =
|
|
4212
|
+
const secFields = isRec11(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
3972
4213
|
for (let f = 0; f < secFields.length; f++) {
|
|
3973
4214
|
const entry = secFields[f];
|
|
3974
4215
|
const fname = fieldNameOf(entry);
|
|
@@ -3983,7 +4224,7 @@ function validateFormLayout(stack) {
|
|
|
3983
4224
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
3984
4225
|
});
|
|
3985
4226
|
}
|
|
3986
|
-
const colSpan =
|
|
4227
|
+
const colSpan = isRec11(entry) ? entry.colSpan : void 0;
|
|
3987
4228
|
if (colSpan != null) {
|
|
3988
4229
|
findings.push({
|
|
3989
4230
|
severity: "warning",
|
|
@@ -4004,6 +4245,64 @@ function validateFormLayout(stack) {
|
|
|
4004
4245
|
|
|
4005
4246
|
// src/validate-visibility-predicates.ts
|
|
4006
4247
|
var import_formula3 = require("@objectstack/formula");
|
|
4248
|
+
|
|
4249
|
+
// src/predicate-rhs-position.ts
|
|
4250
|
+
function isNode2(v) {
|
|
4251
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4252
|
+
}
|
|
4253
|
+
var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
|
|
4254
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
4255
|
+
function bareId(node) {
|
|
4256
|
+
if (!isNode2(node)) return null;
|
|
4257
|
+
return node.op === "id" && typeof node.args === "string" ? node.args : null;
|
|
4258
|
+
}
|
|
4259
|
+
function bareRhsOnlyIdentifiers(ast) {
|
|
4260
|
+
const rhs = /* @__PURE__ */ new Set();
|
|
4261
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
4262
|
+
const walk = (node, suppressible) => {
|
|
4263
|
+
if (Array.isArray(node)) {
|
|
4264
|
+
for (const child of node) walk(child, suppressible);
|
|
4265
|
+
return;
|
|
4266
|
+
}
|
|
4267
|
+
if (!isNode2(node)) return;
|
|
4268
|
+
const args = node.args;
|
|
4269
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
4270
|
+
walk(args[1], suppressible);
|
|
4271
|
+
walk(args[2], false);
|
|
4272
|
+
return;
|
|
4273
|
+
}
|
|
4274
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
4275
|
+
const right = suppressible ? bareId(args[1]) : null;
|
|
4276
|
+
walk(args[0], suppressible);
|
|
4277
|
+
if (right !== null) {
|
|
4278
|
+
rhs.add(right);
|
|
4279
|
+
return;
|
|
4280
|
+
}
|
|
4281
|
+
walk(args[1], suppressible);
|
|
4282
|
+
return;
|
|
4283
|
+
}
|
|
4284
|
+
const name = bareId(node);
|
|
4285
|
+
if (name !== null) {
|
|
4286
|
+
elsewhere.add(name);
|
|
4287
|
+
return;
|
|
4288
|
+
}
|
|
4289
|
+
walk(args, suppressible);
|
|
4290
|
+
};
|
|
4291
|
+
walk(ast, true);
|
|
4292
|
+
for (const name of elsewhere) rhs.delete(name);
|
|
4293
|
+
return rhs;
|
|
4294
|
+
}
|
|
4295
|
+
function isRec12(v) {
|
|
4296
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4297
|
+
}
|
|
4298
|
+
function schemaIdOf(view) {
|
|
4299
|
+
const data = view.data;
|
|
4300
|
+
if (!isRec12(data)) return void 0;
|
|
4301
|
+
if (data.provider !== "schema") return void 0;
|
|
4302
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
// src/validate-visibility-predicates.ts
|
|
4007
4306
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
4008
4307
|
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
4009
4308
|
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
@@ -4051,7 +4350,7 @@ function boundName(overrun) {
|
|
|
4051
4350
|
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
4052
4351
|
}
|
|
4053
4352
|
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
4054
|
-
function
|
|
4353
|
+
function isNode3(v) {
|
|
4055
4354
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4056
4355
|
}
|
|
4057
4356
|
function namespaceRoots(node, out) {
|
|
@@ -4059,22 +4358,27 @@ function namespaceRoots(node, out) {
|
|
|
4059
4358
|
for (const child of node) namespaceRoots(child, out);
|
|
4060
4359
|
return;
|
|
4061
4360
|
}
|
|
4062
|
-
if (!
|
|
4361
|
+
if (!isNode3(node)) return;
|
|
4063
4362
|
const args = node.args;
|
|
4064
4363
|
if (Array.isArray(args)) {
|
|
4065
4364
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
4066
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
4365
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
4067
4366
|
out.add(receiver.args);
|
|
4068
4367
|
}
|
|
4069
4368
|
}
|
|
4070
4369
|
namespaceRoots(args, out);
|
|
4071
4370
|
}
|
|
4072
|
-
function firstBareIdentifier(source) {
|
|
4371
|
+
function firstBareIdentifier(source, literalRhs) {
|
|
4073
4372
|
const ast = (0, import_formula3.parseCelToAst)(source);
|
|
4074
4373
|
if (!ast) return null;
|
|
4075
4374
|
const rooted = /* @__PURE__ */ new Set();
|
|
4076
4375
|
namespaceRoots(ast, rooted);
|
|
4077
|
-
|
|
4376
|
+
const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
|
|
4377
|
+
return (0, import_formula3.firstUndeclaredReference)(source, [
|
|
4378
|
+
...VIEW_PAGE_EXTRA_ROOTS,
|
|
4379
|
+
...rooted,
|
|
4380
|
+
...literalSlot
|
|
4381
|
+
]);
|
|
4078
4382
|
}
|
|
4079
4383
|
var CANONICAL_ROOT_BY_LAYER = {
|
|
4080
4384
|
runtime: "record",
|
|
@@ -4083,16 +4387,16 @@ var CANONICAL_ROOT_BY_LAYER = {
|
|
|
4083
4387
|
var MISLAYER_BY_LAYER = {
|
|
4084
4388
|
runtime: {
|
|
4085
4389
|
forbiddenRoot: "data",
|
|
4086
|
-
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing
|
|
4390
|
+
message: "visibility predicate is rooted at `data.` \u2014 that is the root a metadata-editing form binds (the row under edit), not a runtime surface. A runtime view/page predicate that binds `data.` never matches and the element renders unconditionally (ADR-0089).",
|
|
4087
4391
|
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
4088
4392
|
},
|
|
4089
4393
|
metadata: {
|
|
4090
4394
|
forbiddenRoot: "record",
|
|
4091
|
-
message: "visibility predicate is rooted at `record.` \u2014 that is the
|
|
4395
|
+
message: "visibility predicate is rooted at `record.` \u2014 that is the root a runtime view/page surface binds (the live record), not the root a metadata-editing form binds. On a metadata-editing form \u2014 the row under edit \u2014 a `record.`-rooted predicate never matches and the element renders unconditionally (ADR-0089).",
|
|
4092
4396
|
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
4093
4397
|
}
|
|
4094
4398
|
};
|
|
4095
|
-
function checkElement(el, where, path, layer, findings) {
|
|
4399
|
+
function checkElement(el, where, path, layer, findings, literalRhs = false) {
|
|
4096
4400
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
4097
4401
|
const source = predicateSource(raw);
|
|
4098
4402
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -4130,7 +4434,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
4130
4434
|
});
|
|
4131
4435
|
}
|
|
4132
4436
|
if (source && !refusal) {
|
|
4133
|
-
const bare = firstBareIdentifier(source);
|
|
4437
|
+
const bare = firstBareIdentifier(source, literalRhs);
|
|
4134
4438
|
if (bare) {
|
|
4135
4439
|
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
4136
4440
|
findings.push({
|
|
@@ -4139,7 +4443,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
4139
4443
|
where,
|
|
4140
4444
|
path,
|
|
4141
4445
|
message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
4142
|
-
hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a
|
|
4446
|
+
hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a metadata-editing form binds the row under edit as `data`).")
|
|
4143
4447
|
});
|
|
4144
4448
|
}
|
|
4145
4449
|
}
|
|
@@ -4148,24 +4452,27 @@ function isFieldObject(entry) {
|
|
|
4148
4452
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
4149
4453
|
}
|
|
4150
4454
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
4151
|
-
const
|
|
4455
|
+
const declaredLayer = opts.layer ?? "runtime";
|
|
4152
4456
|
const findings = [];
|
|
4153
4457
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
4154
4458
|
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
4155
4459
|
for (const site of formViewSites(view, viewPath)) {
|
|
4156
4460
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
4461
|
+
const schemaBound = schemaIdOf(site.view) !== void 0;
|
|
4462
|
+
const literalRhs = schemaBound;
|
|
4463
|
+
const layer = schemaBound ? "metadata" : declaredLayer;
|
|
4157
4464
|
for (const bucket of ["sections", "groups"]) {
|
|
4158
4465
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
4159
4466
|
for (let s = 0; s < sections.length; s++) {
|
|
4160
4467
|
const sec = sections[s];
|
|
4161
4468
|
if (!sec || typeof sec !== "object") continue;
|
|
4162
4469
|
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
4163
|
-
checkElement(sec, where, secPath, layer, findings);
|
|
4470
|
+
checkElement(sec, where, secPath, layer, findings, literalRhs);
|
|
4164
4471
|
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
4165
4472
|
for (let f = 0; f < secFields.length; f++) {
|
|
4166
4473
|
const entry = secFields[f];
|
|
4167
4474
|
if (isFieldObject(entry)) {
|
|
4168
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
4475
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
|
|
4169
4476
|
}
|
|
4170
4477
|
}
|
|
4171
4478
|
}
|
|
@@ -4176,7 +4483,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
4176
4483
|
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
4177
4484
|
const where = `page "${pageName ?? pagePath}"`;
|
|
4178
4485
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
4179
|
-
checkElement(walked.component, where, walked.path,
|
|
4486
|
+
checkElement(walked.component, where, walked.path, declaredLayer, findings);
|
|
4180
4487
|
}
|
|
4181
4488
|
}
|
|
4182
4489
|
return findings;
|
|
@@ -4188,9 +4495,9 @@ var import_kernel2 = require("@objectstack/spec/kernel");
|
|
|
4188
4495
|
var import_spec = require("@objectstack/spec");
|
|
4189
4496
|
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
4190
4497
|
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
4498
|
+
var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
|
|
4191
4499
|
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
4192
4500
|
var ROOT = "data";
|
|
4193
|
-
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
4194
4501
|
function defOf(schema) {
|
|
4195
4502
|
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
4196
4503
|
const s = schema;
|
|
@@ -4283,11 +4590,11 @@ function stepInto(scope, segment) {
|
|
|
4283
4590
|
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
4284
4591
|
return { kind: "declared", next: propertyOf(u, segment) };
|
|
4285
4592
|
}
|
|
4286
|
-
function
|
|
4593
|
+
function isNode4(v) {
|
|
4287
4594
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4288
4595
|
}
|
|
4289
4596
|
function memberChain(node) {
|
|
4290
|
-
if (!
|
|
4597
|
+
if (!isNode4(node)) return null;
|
|
4291
4598
|
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
4292
4599
|
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
4293
4600
|
const head = memberChain(node.args[0]);
|
|
@@ -4300,7 +4607,7 @@ function rootedPaths(node, out) {
|
|
|
4300
4607
|
for (const child of node) rootedPaths(child, out);
|
|
4301
4608
|
return;
|
|
4302
4609
|
}
|
|
4303
|
-
if (!
|
|
4610
|
+
if (!isNode4(node)) return;
|
|
4304
4611
|
if (node.op === ".") {
|
|
4305
4612
|
const chain = memberChain(node);
|
|
4306
4613
|
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
@@ -4310,23 +4617,40 @@ function rootedPaths(node, out) {
|
|
|
4310
4617
|
}
|
|
4311
4618
|
rootedPaths(node.args, out);
|
|
4312
4619
|
}
|
|
4620
|
+
var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
|
|
4621
|
+
function equalitySites(node, out) {
|
|
4622
|
+
if (Array.isArray(node)) {
|
|
4623
|
+
for (const child of node) equalitySites(child, out);
|
|
4624
|
+
return;
|
|
4625
|
+
}
|
|
4626
|
+
if (!isNode4(node)) return;
|
|
4627
|
+
const args = node.args;
|
|
4628
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
4629
|
+
equalitySites(args[1], out);
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
4633
|
+
out.push({ op: node.op, right: args[1] });
|
|
4634
|
+
}
|
|
4635
|
+
equalitySites(args, out);
|
|
4636
|
+
}
|
|
4313
4637
|
function classifyIdentifiers(node, values, excluded) {
|
|
4314
4638
|
if (Array.isArray(node)) {
|
|
4315
4639
|
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
4316
4640
|
return;
|
|
4317
4641
|
}
|
|
4318
|
-
if (!
|
|
4642
|
+
if (!isNode4(node)) return;
|
|
4319
4643
|
const args = node.args;
|
|
4320
4644
|
if (Array.isArray(args)) {
|
|
4321
4645
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
4322
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
4646
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
4323
4647
|
excluded.add(receiver.args);
|
|
4324
4648
|
}
|
|
4325
4649
|
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
4326
4650
|
const macroArgs = args[2];
|
|
4327
4651
|
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
4328
4652
|
const bound = macroArgs[0];
|
|
4329
|
-
if (
|
|
4653
|
+
if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
4330
4654
|
excluded.add(bound.args);
|
|
4331
4655
|
}
|
|
4332
4656
|
}
|
|
@@ -4345,15 +4669,9 @@ function predicateSource2(v) {
|
|
|
4345
4669
|
}
|
|
4346
4670
|
return void 0;
|
|
4347
4671
|
}
|
|
4348
|
-
function
|
|
4672
|
+
function isRec13(v) {
|
|
4349
4673
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4350
4674
|
}
|
|
4351
|
-
function schemaIdOf(view) {
|
|
4352
|
-
const data = view.data;
|
|
4353
|
-
if (!isRec11(data)) return void 0;
|
|
4354
|
-
if (data.provider !== "schema") return void 0;
|
|
4355
|
-
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
4356
|
-
}
|
|
4357
4675
|
function checkPredicate(source, scope, where, path, findings) {
|
|
4358
4676
|
const ast = (0, import_formula4.parseCelToAst)(source);
|
|
4359
4677
|
if (!ast) return;
|
|
@@ -4383,19 +4701,38 @@ function checkPredicate(source, scope, where, path, findings) {
|
|
|
4383
4701
|
}
|
|
4384
4702
|
}
|
|
4385
4703
|
const declaredHere = keysOf(scope);
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4704
|
+
const rhsOnly = bareRhsOnlyIdentifiers(ast);
|
|
4705
|
+
if (declaredHere) {
|
|
4706
|
+
const values = /* @__PURE__ */ new Set();
|
|
4707
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
4708
|
+
classifyIdentifiers(ast, values, excluded);
|
|
4709
|
+
for (const id of values) {
|
|
4710
|
+
if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
|
|
4711
|
+
findings.push({
|
|
4712
|
+
severity: "error",
|
|
4713
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
4714
|
+
where,
|
|
4715
|
+
path,
|
|
4716
|
+
message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
|
|
4717
|
+
hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
|
|
4718
|
+
});
|
|
4719
|
+
}
|
|
4720
|
+
}
|
|
4721
|
+
const sites = [];
|
|
4722
|
+
equalitySites(ast, sites);
|
|
4723
|
+
for (const { op, right } of sites) {
|
|
4724
|
+
const chain = memberChain(right);
|
|
4725
|
+
if (!chain) continue;
|
|
4726
|
+
const text = chain.join(".");
|
|
4727
|
+
if (!PATH_SHAPED_RHS.test(text)) continue;
|
|
4728
|
+
const dotted = chain.length > 1;
|
|
4392
4729
|
findings.push({
|
|
4393
|
-
severity: "error",
|
|
4394
|
-
rule:
|
|
4730
|
+
severity: dotted ? "error" : "warning",
|
|
4731
|
+
rule: PREDICATE_RHS_PATH_SHAPED,
|
|
4395
4732
|
where,
|
|
4396
4733
|
path,
|
|
4397
|
-
message: `predicate
|
|
4398
|
-
hint: `
|
|
4734
|
+
message: dotted ? `predicate compares against \`${text}\` on the RIGHT of \`${op}\`, which is a path but is not evaluated as one. A metadata-editing form resolves paths on the LEFT of \`${op}\` only; the right-hand side goes to the literal parser, so \`${text}\` is compared as the literal string "${text}". The verdict therefore does not depend on the right-hand path at all: \`a == ${text}\` is FALSE even when both sides hold the same value, and \`a != ${text}\` is correspondingly TRUE. An \`==\` written this way hides the element on every row, and nothing in the console says why (objectui#4049).` : `predicate compares against the unquoted word \`${text}\` on the RIGHT of \`${op}\`. The right-hand side of \`${op}\` is a literal, never a reference, so this is read as the literal string "${text}" \u2014 which is probably what you meant, and is why it appears to work. It is outside the declared subset all the same (\`path == 'literal'\`), and it stops working when this surface moves to the real CEL evaluator, where a bare \`${text}\` resolves to nothing (objectui#4049). The token also reads as a \`${ROOT}.\` root someone dropped, so this one finding carries BOTH readings: which one you meant is the thing no linter can know, and it changes the fix (#7696).`,
|
|
4735
|
+
hint: dotted ? `Two sanctioned spellings. (1) If you meant the TEXT, quote it: \`${op} '${text}'\`. (2) If you meant the PATH, restructure so the path is on the LEFT and a literal is on the right \u2014 comparing one path against another is outside the subset this surface renders, which is \`path == 'literal'\` / \`path != 'literal'\` and nothing wider. There is no third spelling that compares two paths here.` : `Two sanctioned spellings, and you must pick \u2014 they are not the same predicate. (1) If you meant the TEXT \`${text}\`, quote it: \`${op} '${text}'\`. That is what this renders as today, so it changes no behaviour and is the fix unless you know otherwise. (2) If you meant the FIELD \`${ROOT}.${text}\`, move it to the LEFT and put a literal on the right, e.g. \`${ROOT}.${text} == 'yes'\`. \u26D4 Do NOT simply add the root in place: \`${op} ${ROOT}.${text}\` is a path on the RIGHT, which this surface parses as the literal string "${ROOT}.${text}" \u2014 it is refused by this same rule at \`error\`, and it is FALSE on every row. The subset here is \`path == 'literal'\` and nothing wider.`
|
|
4399
4736
|
});
|
|
4400
4737
|
}
|
|
4401
4738
|
}
|
|
@@ -4403,7 +4740,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
4403
4740
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
4404
4741
|
for (let i = 0; i < entries.length; i++) {
|
|
4405
4742
|
const entry = entries[i];
|
|
4406
|
-
if (!
|
|
4743
|
+
if (!isRec13(entry)) continue;
|
|
4407
4744
|
const path = `${base}[${i}]`;
|
|
4408
4745
|
for (const key of PREDICATE_KEYS) {
|
|
4409
4746
|
const source = predicateSource2(entry[key]);
|
|
@@ -4430,15 +4767,14 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
4430
4767
|
try {
|
|
4431
4768
|
root = resolveSchema(schemaId);
|
|
4432
4769
|
} catch {
|
|
4433
|
-
|
|
4770
|
+
root = void 0;
|
|
4434
4771
|
}
|
|
4435
|
-
if (!root) continue;
|
|
4436
4772
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
4437
4773
|
for (const bucket of ["sections", "groups"]) {
|
|
4438
4774
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
4439
4775
|
for (let s = 0; s < sections.length; s++) {
|
|
4440
4776
|
const section = sections[s];
|
|
4441
|
-
if (!
|
|
4777
|
+
if (!isRec13(section)) continue;
|
|
4442
4778
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
4443
4779
|
for (const key of PREDICATE_KEYS) {
|
|
4444
4780
|
const source = predicateSource2(section[key]);
|
|
@@ -4879,6 +5215,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
|
4879
5215
|
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
4880
5216
|
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
4881
5217
|
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
5218
|
+
var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
|
|
4882
5219
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
4883
5220
|
var OWD_ALIAS_FIX = {
|
|
4884
5221
|
read: "public_read",
|
|
@@ -4924,6 +5261,13 @@ function firstMasterDetailField(obj) {
|
|
|
4924
5261
|
}
|
|
4925
5262
|
return void 0;
|
|
4926
5263
|
}
|
|
5264
|
+
function resolveCbpRelation(obj) {
|
|
5265
|
+
const entries = asArray19(obj.fields);
|
|
5266
|
+
const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
|
|
5267
|
+
const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
|
|
5268
|
+
if (!found) return void 0;
|
|
5269
|
+
return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
|
|
5270
|
+
}
|
|
4927
5271
|
function grantsObjectAccess(p) {
|
|
4928
5272
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
4929
5273
|
}
|
|
@@ -4969,6 +5313,16 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4969
5313
|
});
|
|
4970
5314
|
}
|
|
4971
5315
|
}
|
|
5316
|
+
if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
|
|
5317
|
+
findings.push({
|
|
5318
|
+
severity: "error",
|
|
5319
|
+
rule: SECURITY_CBP_NO_RELATION,
|
|
5320
|
+
where: `object "${objName}"`,
|
|
5321
|
+
path: `${objPath}.sharingModel`,
|
|
5322
|
+
message: `"${objName}" declares sharingModel 'controlled_by_parent' but has no relation the platform can derive access from. ADR-0055 resolves the master through a required master_detail, then any master_detail, then a required lookup \u2014 each of which must also name a reference target \u2014 and this object matches none of the three. At runtime every read is DENIED and every write is refused with 422 INVALID_METADATA (#7474), so the object is unusable rather than merely locked down.`,
|
|
5323
|
+
hint: `Add the master relation this object is derived from, e.g. fields.parent: { type: 'master_detail', reference: '<master_object>', required: true }. If the object has no master, its baseline is its own decision \u2014 use sharingModel: 'private' (owner + shares), 'public_read', or 'public_read_write'.`
|
|
5324
|
+
});
|
|
5325
|
+
}
|
|
4972
5326
|
if (typeof external === "string") {
|
|
4973
5327
|
if (OWD_ALIAS_FIX[external]) {
|
|
4974
5328
|
findings.push({
|
|
@@ -5034,53 +5388,6 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5034
5388
|
}
|
|
5035
5389
|
}
|
|
5036
5390
|
}
|
|
5037
|
-
const flagRole = (kind, name, label2, where, path) => {
|
|
5038
|
-
if (identifierHasRoleToken(name)) {
|
|
5039
|
-
findings.push({
|
|
5040
|
-
severity: "error",
|
|
5041
|
-
rule: SECURITY_ROLE_WORD,
|
|
5042
|
-
where,
|
|
5043
|
-
path,
|
|
5044
|
-
message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
|
|
5045
|
-
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
5046
|
-
});
|
|
5047
|
-
} else if (labelHasRoleWord(label2)) {
|
|
5048
|
-
findings.push({
|
|
5049
|
-
severity: "error",
|
|
5050
|
-
rule: SECURITY_ROLE_WORD,
|
|
5051
|
-
where,
|
|
5052
|
-
path: `${path.replace(/\.name$/, "")}.label`,
|
|
5053
|
-
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
5054
|
-
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
5055
|
-
});
|
|
5056
|
-
}
|
|
5057
|
-
};
|
|
5058
|
-
for (let i = 0; i < objects.length; i++) {
|
|
5059
|
-
const obj = objects[i];
|
|
5060
|
-
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
5061
|
-
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
5062
|
-
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
5063
|
-
for (const f of asArray19(obj.fields)) {
|
|
5064
|
-
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
5065
|
-
}
|
|
5066
|
-
for (const [ai, action] of asArray19(obj.actions).entries()) {
|
|
5067
|
-
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
5068
|
-
}
|
|
5069
|
-
}
|
|
5070
|
-
for (let i = 0; i < permissionSets.length; i++) {
|
|
5071
|
-
const ps = permissionSets[i];
|
|
5072
|
-
if (!ps || typeof ps !== "object") continue;
|
|
5073
|
-
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
5074
|
-
}
|
|
5075
|
-
for (const [i, pos] of asArray19(stack.positions).entries()) {
|
|
5076
|
-
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
5077
|
-
}
|
|
5078
|
-
for (const [i, app] of asArray19(stack.apps).entries()) {
|
|
5079
|
-
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
5080
|
-
}
|
|
5081
|
-
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
5082
|
-
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
5083
|
-
}
|
|
5084
5391
|
const stackSetNames = new Set(
|
|
5085
5392
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
5086
5393
|
);
|
|
@@ -5201,6 +5508,60 @@ function validateSecurityPosture(stack, opts) {
|
|
|
5201
5508
|
}
|
|
5202
5509
|
return findings;
|
|
5203
5510
|
}
|
|
5511
|
+
function validateSecurityRoleWord(stack) {
|
|
5512
|
+
const findings = [];
|
|
5513
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
5514
|
+
const objects = asArray19(stack.objects);
|
|
5515
|
+
const permissionSets = asArray19(stack.permissions);
|
|
5516
|
+
const flagRole = (kind, name, label2, where, path) => {
|
|
5517
|
+
if (identifierHasRoleToken(name)) {
|
|
5518
|
+
findings.push({
|
|
5519
|
+
severity: "error",
|
|
5520
|
+
rule: SECURITY_ROLE_WORD,
|
|
5521
|
+
where,
|
|
5522
|
+
path,
|
|
5523
|
+
message: `${kind} name "${String(name)}" uses the reserved word "role" \u2014 the platform vocabulary is permission_set (capability), position (distribution), business_unit (hierarchy) (ADR-0090 D3).`,
|
|
5524
|
+
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
5525
|
+
});
|
|
5526
|
+
} else if (labelHasRoleWord(label2)) {
|
|
5527
|
+
findings.push({
|
|
5528
|
+
severity: "error",
|
|
5529
|
+
rule: SECURITY_ROLE_WORD,
|
|
5530
|
+
where,
|
|
5531
|
+
path: `${path.replace(/\.name$/, "")}.label`,
|
|
5532
|
+
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
5533
|
+
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
5534
|
+
});
|
|
5535
|
+
}
|
|
5536
|
+
};
|
|
5537
|
+
for (let i = 0; i < objects.length; i++) {
|
|
5538
|
+
const obj = objects[i];
|
|
5539
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
5540
|
+
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
5541
|
+
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
5542
|
+
for (const f of asArray19(obj.fields)) {
|
|
5543
|
+
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
5544
|
+
}
|
|
5545
|
+
for (const [ai, action] of asArray19(obj.actions).entries()) {
|
|
5546
|
+
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5549
|
+
for (let i = 0; i < permissionSets.length; i++) {
|
|
5550
|
+
const ps = permissionSets[i];
|
|
5551
|
+
if (!ps || typeof ps !== "object") continue;
|
|
5552
|
+
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
5553
|
+
}
|
|
5554
|
+
for (const [i, pos] of asArray19(stack.positions).entries()) {
|
|
5555
|
+
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
5556
|
+
}
|
|
5557
|
+
for (const [i, app] of asArray19(stack.apps).entries()) {
|
|
5558
|
+
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
5559
|
+
}
|
|
5560
|
+
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
5561
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
5562
|
+
}
|
|
5563
|
+
return findings;
|
|
5564
|
+
}
|
|
5204
5565
|
|
|
5205
5566
|
// src/validate-org-axis-red-lines.ts
|
|
5206
5567
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
@@ -5583,7 +5944,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
5583
5944
|
}
|
|
5584
5945
|
|
|
5585
5946
|
// src/validate-filter-tokens.ts
|
|
5586
|
-
var
|
|
5947
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
5587
5948
|
|
|
5588
5949
|
// src/filter-walk.ts
|
|
5589
5950
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
@@ -5643,7 +6004,7 @@ function walkAuthoredFilters(stack, surfaces, visit) {
|
|
|
5643
6004
|
|
|
5644
6005
|
// src/validate-filter-tokens.ts
|
|
5645
6006
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
5646
|
-
var KNOWN_LIST =
|
|
6007
|
+
var KNOWN_LIST = import_data7.CONTEXT_TOKENS.join("}, {");
|
|
5647
6008
|
var TOKEN_FILTER_SURFACES = [
|
|
5648
6009
|
{ key: "dashboards", kind: "dashboard" },
|
|
5649
6010
|
{ key: "objects", kind: "object" },
|
|
@@ -5656,7 +6017,7 @@ var TOKEN_FILTER_SURFACES = [
|
|
|
5656
6017
|
function walkFilterValues(node, path, where, out, seen) {
|
|
5657
6018
|
if (node === null || node === void 0) return;
|
|
5658
6019
|
if (typeof node === "string") {
|
|
5659
|
-
const cls = (0,
|
|
6020
|
+
const cls = (0, import_data7.classifyFilterToken)(node);
|
|
5660
6021
|
if (cls?.kind === "unknown") {
|
|
5661
6022
|
const suggestion = cls.suggestion;
|
|
5662
6023
|
out.push({
|
|
@@ -5691,7 +6052,7 @@ function validateFilterTokens(stack) {
|
|
|
5691
6052
|
}
|
|
5692
6053
|
|
|
5693
6054
|
// src/validate-empty-combinators.ts
|
|
5694
|
-
var
|
|
6055
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
5695
6056
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
5696
6057
|
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
5697
6058
|
var EMPTY_COMBINATOR_SURFACES = [
|
|
@@ -5710,12 +6071,12 @@ function isFilterNode(value) {
|
|
|
5710
6071
|
return proto === Object.prototype || proto === null;
|
|
5711
6072
|
}
|
|
5712
6073
|
var VERDICT_OF = {
|
|
5713
|
-
$and: (0,
|
|
5714
|
-
$or: (0,
|
|
5715
|
-
$not: (0,
|
|
5716
|
-
node: (0,
|
|
6074
|
+
$and: (0, import_data8.reduceFilterVerdict)({ $and: [] }),
|
|
6075
|
+
$or: (0, import_data8.reduceFilterVerdict)({ $or: [] }),
|
|
6076
|
+
$not: (0, import_data8.reduceFilterVerdict)({ $not: {} }),
|
|
6077
|
+
node: (0, import_data8.reduceFilterVerdict)({}),
|
|
5717
6078
|
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
5718
|
-
orWithEmptyBranch: (0,
|
|
6079
|
+
orWithEmptyBranch: (0, import_data8.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
5719
6080
|
};
|
|
5720
6081
|
function rows(verdict) {
|
|
5721
6082
|
if (verdict === "true") return "matches EVERY row";
|
|
@@ -6001,10 +6362,10 @@ function validateObjectReferences(stack) {
|
|
|
6001
6362
|
|
|
6002
6363
|
// src/validate-nav-target-refs.ts
|
|
6003
6364
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
6004
|
-
var
|
|
6365
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6005
6366
|
function asArray26(v) {
|
|
6006
|
-
if (Array.isArray(v)) return v.filter(
|
|
6007
|
-
if (
|
|
6367
|
+
if (Array.isArray(v)) return v.filter(isRec14);
|
|
6368
|
+
if (isRec14(v)) return Object.entries(v).map(([name, def]) => isRec14(def) ? { name, ...def } : { name });
|
|
6008
6369
|
return [];
|
|
6009
6370
|
}
|
|
6010
6371
|
function strName9(v) {
|
|
@@ -6026,7 +6387,7 @@ function namesOf(collection) {
|
|
|
6026
6387
|
}
|
|
6027
6388
|
function validateNavTargetRefs(stack) {
|
|
6028
6389
|
const findings = [];
|
|
6029
|
-
if (!
|
|
6390
|
+
if (!isRec14(stack)) return findings;
|
|
6030
6391
|
const apps = asArray26(stack.apps);
|
|
6031
6392
|
if (apps.length === 0) return findings;
|
|
6032
6393
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -6038,7 +6399,7 @@ function validateNavTargetRefs(stack) {
|
|
|
6038
6399
|
const walk = (items, basePath) => {
|
|
6039
6400
|
if (!Array.isArray(items)) return;
|
|
6040
6401
|
for (const [ni, raw] of items.entries()) {
|
|
6041
|
-
if (!
|
|
6402
|
+
if (!isRec14(raw)) continue;
|
|
6042
6403
|
const nav = raw;
|
|
6043
6404
|
const navPath = `${basePath}[${ni}]`;
|
|
6044
6405
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -6069,16 +6430,84 @@ function validateNavTargetRefs(stack) {
|
|
|
6069
6430
|
return findings;
|
|
6070
6431
|
}
|
|
6071
6432
|
|
|
6433
|
+
// src/validate-nav-object-servability.ts
|
|
6434
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
6435
|
+
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
6436
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6437
|
+
function asArray27(v) {
|
|
6438
|
+
if (Array.isArray(v)) return v.filter(isRec15);
|
|
6439
|
+
if (isRec15(v)) return Object.entries(v).map(([name, def]) => isRec15(def) ? { name, ...def } : { name });
|
|
6440
|
+
return [];
|
|
6441
|
+
}
|
|
6442
|
+
function strName10(v) {
|
|
6443
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6444
|
+
}
|
|
6445
|
+
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
6446
|
+
function validateNavObjectServability(stack) {
|
|
6447
|
+
const findings = [];
|
|
6448
|
+
if (!isRec15(stack)) return findings;
|
|
6449
|
+
const apps = asArray27(stack.apps);
|
|
6450
|
+
if (apps.length === 0) return findings;
|
|
6451
|
+
const ownEnable = /* @__PURE__ */ new Map();
|
|
6452
|
+
const objects = asArray27(stack.objects);
|
|
6453
|
+
for (const [oi, obj] of objects.entries()) {
|
|
6454
|
+
const n = strName10(obj.name);
|
|
6455
|
+
if (!n) continue;
|
|
6456
|
+
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
6457
|
+
}
|
|
6458
|
+
if (ownEnable.size === 0) return findings;
|
|
6459
|
+
for (const [ai, app] of apps.entries()) {
|
|
6460
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
6461
|
+
const walk = (items, basePath) => {
|
|
6462
|
+
if (!Array.isArray(items)) return;
|
|
6463
|
+
for (const [ni, raw] of items.entries()) {
|
|
6464
|
+
if (!isRec15(raw)) continue;
|
|
6465
|
+
const nav = raw;
|
|
6466
|
+
const navPath = `${basePath}[${ni}]`;
|
|
6467
|
+
if (nav.type === "object") {
|
|
6468
|
+
const target = strName10(nav.objectName);
|
|
6469
|
+
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
6470
|
+
if (target && declared && !(0, import_data9.canServeApiOperation)(declared.enable, "list")) {
|
|
6471
|
+
const enable = isRec15(declared.enable) ? declared.enable : {};
|
|
6472
|
+
const apiDisabled = enable.apiEnabled === false;
|
|
6473
|
+
const condition = apiDisabled ? "`enable.apiEnabled: false`" : "`enable.apiMethods` does not grant `list`" + (Array.isArray(enable.apiMethods) ? ` (declared: ${enable.apiMethods.length === 0 ? "[] \u2014 deny-all" : enable.apiMethods.map((m) => `\`${String(m)}\``).join(", ")})` : "");
|
|
6474
|
+
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
6475
|
+
const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
|
|
6476
|
+
findings.push({
|
|
6477
|
+
severity: "error",
|
|
6478
|
+
rule: NAV_OBJECT_UNSERVABLE,
|
|
6479
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
6480
|
+
// The nav entry is where the dead row is authored; the `enable`
|
|
6481
|
+
// key that condemns it is named in the message, because the fix
|
|
6482
|
+
// may belong at either end.
|
|
6483
|
+
path: `${navPath}.objectName`,
|
|
6484
|
+
message: `Navigation targets object "${target}", which cannot serve a list: ${condition} (\`${offendingKey}\`), so the list request answers ${answer} for EVERY user \u2014 platform administrators included, since that gate reads only the object's \`enable\` block and never the caller. The entry cannot be rescued with \`requiredPermissions\`: they are independent conditions. The server prunes this entry from the served \`/meta\` payload (#7912), so publishing it ships a menu row that silently is not there.`,
|
|
6485
|
+
hint: `Remove the nav entry, or make "${target}" listable by setting \`enable.apiEnabled: true\` and granting \`list\` in \`enable.apiMethods\`. \u26D4 Do NOT open the API on an object that is disabled on purpose \u2014 several platform objects hold credential material and are API-disabled deliberately; for those the entry is the mistake, not the \`enable\` block.`
|
|
6486
|
+
});
|
|
6487
|
+
}
|
|
6488
|
+
}
|
|
6489
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
6490
|
+
}
|
|
6491
|
+
};
|
|
6492
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
6493
|
+
for (const [ari, area] of asArray27(app.areas).entries()) {
|
|
6494
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
6495
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
6496
|
+
}
|
|
6497
|
+
}
|
|
6498
|
+
return findings;
|
|
6499
|
+
}
|
|
6500
|
+
|
|
6072
6501
|
// src/validate-action-name-refs.ts
|
|
6073
6502
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
6074
|
-
function
|
|
6503
|
+
function asArray28(v) {
|
|
6075
6504
|
if (Array.isArray(v)) return v;
|
|
6076
6505
|
if (v && typeof v === "object") {
|
|
6077
6506
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6078
6507
|
}
|
|
6079
6508
|
return [];
|
|
6080
6509
|
}
|
|
6081
|
-
function
|
|
6510
|
+
function strName11(v) {
|
|
6082
6511
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6083
6512
|
}
|
|
6084
6513
|
function strList(v) {
|
|
@@ -6115,14 +6544,14 @@ function suggest4(target, known) {
|
|
|
6115
6544
|
}
|
|
6116
6545
|
function collectActionNames(stack) {
|
|
6117
6546
|
const names = /* @__PURE__ */ new Set();
|
|
6118
|
-
for (const action of
|
|
6119
|
-
const n =
|
|
6547
|
+
for (const action of asArray28(stack.actions)) {
|
|
6548
|
+
const n = strName11(action?.name);
|
|
6120
6549
|
if (n) names.add(n);
|
|
6121
6550
|
}
|
|
6122
|
-
for (const obj of
|
|
6551
|
+
for (const obj of asArray28(stack.objects)) {
|
|
6123
6552
|
if (!obj || typeof obj !== "object") continue;
|
|
6124
|
-
for (const action of
|
|
6125
|
-
const n =
|
|
6553
|
+
for (const action of asArray28(obj.actions)) {
|
|
6554
|
+
const n = strName11(action?.name);
|
|
6126
6555
|
if (n) names.add(n);
|
|
6127
6556
|
}
|
|
6128
6557
|
}
|
|
@@ -6165,7 +6594,7 @@ function validateActionNameRefs(stack) {
|
|
|
6165
6594
|
if (!def || typeof def !== "object") continue;
|
|
6166
6595
|
if (def.execution !== "aggregate") continue;
|
|
6167
6596
|
if (def.actionDef !== void 0) continue;
|
|
6168
|
-
const name =
|
|
6597
|
+
const name = strName11(def.name);
|
|
6169
6598
|
if (!name) continue;
|
|
6170
6599
|
check(
|
|
6171
6600
|
name,
|
|
@@ -6176,11 +6605,11 @@ function validateActionNameRefs(stack) {
|
|
|
6176
6605
|
);
|
|
6177
6606
|
}
|
|
6178
6607
|
};
|
|
6179
|
-
const views =
|
|
6608
|
+
const views = asArray28(stack.views);
|
|
6180
6609
|
for (let vi = 0; vi < views.length; vi++) {
|
|
6181
6610
|
const view = views[vi];
|
|
6182
6611
|
if (!view || typeof view !== "object") continue;
|
|
6183
|
-
const viewName =
|
|
6612
|
+
const viewName = strName11(view.name) ?? strName11(view.object) ?? `#${vi}`;
|
|
6184
6613
|
const owner = `view "${viewName}"`;
|
|
6185
6614
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
6186
6615
|
const listViews = view.listViews;
|
|
@@ -6190,22 +6619,22 @@ function validateActionNameRefs(stack) {
|
|
|
6190
6619
|
}
|
|
6191
6620
|
}
|
|
6192
6621
|
}
|
|
6193
|
-
const objects =
|
|
6622
|
+
const objects = asArray28(stack.objects);
|
|
6194
6623
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
6195
6624
|
const obj = objects[oi];
|
|
6196
6625
|
if (!obj || typeof obj !== "object") continue;
|
|
6197
6626
|
const objListViews = obj.listViews;
|
|
6198
6627
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
6199
|
-
const owner = `object "${
|
|
6628
|
+
const owner = `object "${strName11(obj.name) ?? `#${oi}`}"`;
|
|
6200
6629
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
6201
6630
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
6202
6631
|
}
|
|
6203
6632
|
}
|
|
6204
|
-
const pages =
|
|
6633
|
+
const pages = asArray28(stack.pages);
|
|
6205
6634
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6206
6635
|
const page = pages[pi];
|
|
6207
6636
|
if (!page || typeof page !== "object") continue;
|
|
6208
|
-
const pageName =
|
|
6637
|
+
const pageName = strName11(page.name) ?? `#${pi}`;
|
|
6209
6638
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6210
6639
|
const props = component.properties;
|
|
6211
6640
|
if (!props || typeof props !== "object") continue;
|
|
@@ -6213,39 +6642,39 @@ function validateActionNameRefs(stack) {
|
|
|
6213
6642
|
for (let ai = 0; ai < names.length; ai++) {
|
|
6214
6643
|
check(
|
|
6215
6644
|
names[ai],
|
|
6216
|
-
`page "${pageName}" \xB7 component "${
|
|
6645
|
+
`page "${pageName}" \xB7 component "${strName11(component.type) ?? "?"}"`,
|
|
6217
6646
|
`${path}.properties.actionNames[${ai}]`,
|
|
6218
6647
|
"Quick-actions bar"
|
|
6219
6648
|
);
|
|
6220
6649
|
}
|
|
6221
6650
|
}
|
|
6222
6651
|
}
|
|
6223
|
-
const apps =
|
|
6652
|
+
const apps = asArray28(stack.apps);
|
|
6224
6653
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
6225
6654
|
const app = apps[ai];
|
|
6226
6655
|
if (!app || typeof app !== "object") continue;
|
|
6227
|
-
const appName =
|
|
6656
|
+
const appName = strName11(app.name) ?? `#${ai}`;
|
|
6228
6657
|
const walkNav = (items, basePath) => {
|
|
6229
|
-
const navItems =
|
|
6658
|
+
const navItems = asArray28(items);
|
|
6230
6659
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
6231
6660
|
const nav = navItems[ni];
|
|
6232
6661
|
if (!nav || typeof nav !== "object") continue;
|
|
6233
6662
|
const navPath = `${basePath}[${ni}]`;
|
|
6234
6663
|
const actionDef = nav.actionDef;
|
|
6235
|
-
const actionName =
|
|
6664
|
+
const actionName = strName11(actionDef?.actionName);
|
|
6236
6665
|
if (nav.type === "action" && actionName) {
|
|
6237
6666
|
check(
|
|
6238
6667
|
actionName,
|
|
6239
|
-
`app "${appName}" \xB7 nav "${
|
|
6668
|
+
`app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
|
|
6240
6669
|
`${navPath}.actionDef.actionName`,
|
|
6241
6670
|
"Navigation action item"
|
|
6242
6671
|
);
|
|
6243
6672
|
}
|
|
6244
|
-
const runAction =
|
|
6673
|
+
const runAction = strName11(nav.runAction);
|
|
6245
6674
|
if (nav.type === "object" && runAction) {
|
|
6246
6675
|
check(
|
|
6247
6676
|
runAction,
|
|
6248
|
-
`app "${appName}" \xB7 nav "${
|
|
6677
|
+
`app "${appName}" \xB7 nav "${strName11(nav.id) ?? `#${ni}`}"`,
|
|
6249
6678
|
`${navPath}.runAction`,
|
|
6250
6679
|
"Navigation deep-link auto-run"
|
|
6251
6680
|
);
|
|
@@ -6254,7 +6683,7 @@ function validateActionNameRefs(stack) {
|
|
|
6254
6683
|
}
|
|
6255
6684
|
};
|
|
6256
6685
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
6257
|
-
const areas =
|
|
6686
|
+
const areas = asArray28(app.areas);
|
|
6258
6687
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
6259
6688
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
6260
6689
|
}
|
|
@@ -6264,14 +6693,14 @@ function validateActionNameRefs(stack) {
|
|
|
6264
6693
|
|
|
6265
6694
|
// src/validate-action-locations.ts
|
|
6266
6695
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
6267
|
-
function
|
|
6696
|
+
function asArray29(v) {
|
|
6268
6697
|
if (Array.isArray(v)) return v;
|
|
6269
6698
|
if (v && typeof v === "object") {
|
|
6270
6699
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6271
6700
|
}
|
|
6272
6701
|
return [];
|
|
6273
6702
|
}
|
|
6274
|
-
function
|
|
6703
|
+
function strName12(v) {
|
|
6275
6704
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6276
6705
|
}
|
|
6277
6706
|
function strList2(v) {
|
|
@@ -6285,8 +6714,8 @@ function collectNamePlacedActions(stack) {
|
|
|
6285
6714
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
6286
6715
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
6287
6716
|
}
|
|
6288
|
-
for (const def of
|
|
6289
|
-
const n =
|
|
6717
|
+
for (const def of asArray29(list3.bulkActionDefs)) {
|
|
6718
|
+
const n = strName12(def?.name);
|
|
6290
6719
|
if (n) placed.add(n);
|
|
6291
6720
|
}
|
|
6292
6721
|
};
|
|
@@ -6294,12 +6723,12 @@ function collectNamePlacedActions(stack) {
|
|
|
6294
6723
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
6295
6724
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
6296
6725
|
};
|
|
6297
|
-
for (const view of
|
|
6726
|
+
for (const view of asArray29(stack.views)) {
|
|
6298
6727
|
if (!view || typeof view !== "object") continue;
|
|
6299
6728
|
harvest(view.list);
|
|
6300
6729
|
harvestListViews(view.listViews);
|
|
6301
6730
|
}
|
|
6302
|
-
for (const obj of
|
|
6731
|
+
for (const obj of asArray29(stack.objects)) {
|
|
6303
6732
|
if (!obj || typeof obj !== "object") continue;
|
|
6304
6733
|
harvestListViews(obj.listViews);
|
|
6305
6734
|
}
|
|
@@ -6312,7 +6741,7 @@ function validateActionLocations(stack) {
|
|
|
6312
6741
|
const check = (action, path) => {
|
|
6313
6742
|
if (!action || typeof action !== "object") return;
|
|
6314
6743
|
if ("locations" in action) return;
|
|
6315
|
-
const name =
|
|
6744
|
+
const name = strName12(action.name);
|
|
6316
6745
|
if (!name) return;
|
|
6317
6746
|
if (namePlaced.has(name)) return;
|
|
6318
6747
|
findings.push({
|
|
@@ -6324,13 +6753,13 @@ function validateActionLocations(stack) {
|
|
|
6324
6753
|
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); 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."
|
|
6325
6754
|
});
|
|
6326
6755
|
};
|
|
6327
|
-
const actions =
|
|
6756
|
+
const actions = asArray29(stack.actions);
|
|
6328
6757
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
6329
|
-
const objects =
|
|
6758
|
+
const objects = asArray29(stack.objects);
|
|
6330
6759
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
6331
6760
|
const obj = objects[oi];
|
|
6332
6761
|
if (!obj || typeof obj !== "object") continue;
|
|
6333
|
-
const own =
|
|
6762
|
+
const own = asArray29(obj.actions);
|
|
6334
6763
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
6335
6764
|
}
|
|
6336
6765
|
return findings;
|
|
@@ -6341,13 +6770,13 @@ var import_ui3 = require("@objectstack/spec/ui");
|
|
|
6341
6770
|
var import_spec3 = require("@objectstack/spec");
|
|
6342
6771
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
6343
6772
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
6344
|
-
function
|
|
6773
|
+
function isRec16(v) {
|
|
6345
6774
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6346
6775
|
}
|
|
6347
|
-
function
|
|
6776
|
+
function strName13(v) {
|
|
6348
6777
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6349
6778
|
}
|
|
6350
|
-
function
|
|
6779
|
+
function asArray30(v) {
|
|
6351
6780
|
if (Array.isArray(v)) return v;
|
|
6352
6781
|
if (v && typeof v === "object") {
|
|
6353
6782
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6358,23 +6787,36 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
|
6358
6787
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
6359
6788
|
function suppliedByDataSource(issue, component) {
|
|
6360
6789
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
6361
|
-
const dataSource =
|
|
6362
|
-
return
|
|
6790
|
+
const dataSource = isRec16(component.dataSource) ? component.dataSource : void 0;
|
|
6791
|
+
return strName13(dataSource?.object) !== void 0;
|
|
6792
|
+
}
|
|
6793
|
+
function unrecognizedKeysFromUnionArm(issue) {
|
|
6794
|
+
if (issue.code !== "invalid_union") return void 0;
|
|
6795
|
+
const arms = issue.errors;
|
|
6796
|
+
if (!arms || arms.length === 0) return void 0;
|
|
6797
|
+
let found;
|
|
6798
|
+
for (const arm of arms) {
|
|
6799
|
+
const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
|
|
6800
|
+
if (keyIssues.length === 0) continue;
|
|
6801
|
+
if (keyIssues.length !== arm.length || found) return void 0;
|
|
6802
|
+
found = keyIssues[0];
|
|
6803
|
+
}
|
|
6804
|
+
return found;
|
|
6363
6805
|
}
|
|
6364
6806
|
function validateComponentProps(stack) {
|
|
6365
6807
|
const findings = [];
|
|
6366
|
-
if (!
|
|
6367
|
-
const pages =
|
|
6808
|
+
if (!isRec16(stack)) return findings;
|
|
6809
|
+
const pages = asArray30(stack.pages);
|
|
6368
6810
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6369
6811
|
const page = pages[pi];
|
|
6370
|
-
if (!
|
|
6371
|
-
const pageName =
|
|
6812
|
+
if (!isRec16(page)) continue;
|
|
6813
|
+
const pageName = strName13(page.name) ?? `#${pi}`;
|
|
6372
6814
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6373
|
-
const type =
|
|
6815
|
+
const type = strName13(component.type);
|
|
6374
6816
|
if (!type) continue;
|
|
6375
6817
|
const schema = PROPS_SCHEMAS[type];
|
|
6376
6818
|
if (!schema) continue;
|
|
6377
|
-
const props =
|
|
6819
|
+
const props = isRec16(component.properties) ? component.properties : void 0;
|
|
6378
6820
|
if (!props) continue;
|
|
6379
6821
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
6380
6822
|
const base = `${path}.properties`;
|
|
@@ -6393,6 +6835,20 @@ function validateComponentProps(stack) {
|
|
|
6393
6835
|
for (const issue of parsed.error?.issues ?? []) {
|
|
6394
6836
|
if (suppliedByDataSource(issue, component)) continue;
|
|
6395
6837
|
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
6838
|
+
const armIssue = unrecognizedKeysFromUnionArm(issue);
|
|
6839
|
+
if (armIssue) {
|
|
6840
|
+
for (const key of armIssue.keys ?? []) {
|
|
6841
|
+
findings.push({
|
|
6842
|
+
severity: "warning",
|
|
6843
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
6844
|
+
where,
|
|
6845
|
+
path: `${at}.${key}`,
|
|
6846
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
|
|
6847
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
6848
|
+
});
|
|
6849
|
+
}
|
|
6850
|
+
continue;
|
|
6851
|
+
}
|
|
6396
6852
|
if (issue.code === "unrecognized_keys") {
|
|
6397
6853
|
for (const key of issue.keys ?? []) {
|
|
6398
6854
|
findings.push({
|
|
@@ -6425,20 +6881,20 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
|
6425
6881
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
6426
6882
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
6427
6883
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
6428
|
-
function
|
|
6884
|
+
function asArray31(v) {
|
|
6429
6885
|
if (Array.isArray(v)) return v;
|
|
6430
6886
|
if (v && typeof v === "object") {
|
|
6431
6887
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6432
6888
|
}
|
|
6433
6889
|
return [];
|
|
6434
6890
|
}
|
|
6435
|
-
function
|
|
6891
|
+
function strName14(v) {
|
|
6436
6892
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6437
6893
|
}
|
|
6438
6894
|
function strList3(v) {
|
|
6439
6895
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
6440
6896
|
}
|
|
6441
|
-
function
|
|
6897
|
+
function isRec17(v) {
|
|
6442
6898
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6443
6899
|
}
|
|
6444
6900
|
function distance4(a, b) {
|
|
@@ -6476,17 +6932,17 @@ function list2(names) {
|
|
|
6476
6932
|
}
|
|
6477
6933
|
function indexDatasets(stack) {
|
|
6478
6934
|
const out = /* @__PURE__ */ new Map();
|
|
6479
|
-
for (const ds of
|
|
6480
|
-
const name =
|
|
6935
|
+
for (const ds of asArray31(stack.datasets)) {
|
|
6936
|
+
const name = strName14(ds.name);
|
|
6481
6937
|
if (!name) continue;
|
|
6482
6938
|
const dimensions = /* @__PURE__ */ new Set();
|
|
6483
|
-
for (const d of
|
|
6484
|
-
const n =
|
|
6939
|
+
for (const d of asArray31(ds.dimensions)) {
|
|
6940
|
+
const n = strName14(d.name);
|
|
6485
6941
|
if (n) dimensions.add(n);
|
|
6486
6942
|
}
|
|
6487
6943
|
const measures = /* @__PURE__ */ new Set();
|
|
6488
|
-
for (const m of
|
|
6489
|
-
const n =
|
|
6944
|
+
for (const m of asArray31(ds.measures)) {
|
|
6945
|
+
const n = strName14(m.name);
|
|
6490
6946
|
if (n) measures.add(n);
|
|
6491
6947
|
}
|
|
6492
6948
|
out.set(name, { dimensions, measures });
|
|
@@ -6564,29 +7020,29 @@ function validateChartBindings(stack) {
|
|
|
6564
7020
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
6565
7021
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
6566
7022
|
};
|
|
6567
|
-
const reports =
|
|
7023
|
+
const reports = asArray31(stack.reports);
|
|
6568
7024
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
6569
7025
|
const report = reports[ri];
|
|
6570
|
-
if (!
|
|
6571
|
-
const reportName =
|
|
7026
|
+
if (!isRec17(report)) continue;
|
|
7027
|
+
const reportName = strName14(report.name) ?? `#${ri}`;
|
|
6572
7028
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
6573
|
-
if (!
|
|
7029
|
+
if (!isRec17(chart)) return;
|
|
6574
7030
|
check({
|
|
6575
7031
|
dataset,
|
|
6576
7032
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
6577
7033
|
// it in lets the yAxis "declared but not selected" check work without
|
|
6578
7034
|
// reporting the selection itself twice.
|
|
6579
7035
|
values: { names: values, path: `${path}.values` },
|
|
6580
|
-
xAxis:
|
|
6581
|
-
yAxis:
|
|
6582
|
-
series:
|
|
7036
|
+
xAxis: strName14(chart.xAxis) ? { name: strName14(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
7037
|
+
yAxis: strName14(chart.yAxis) ? { name: strName14(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
7038
|
+
series: asArray31(chart.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
6583
7039
|
where,
|
|
6584
7040
|
path: `${path}.chart`
|
|
6585
7041
|
});
|
|
6586
7042
|
};
|
|
6587
7043
|
checkReportChart(
|
|
6588
7044
|
report.chart,
|
|
6589
|
-
|
|
7045
|
+
strName14(report.dataset),
|
|
6590
7046
|
strList3(report.values),
|
|
6591
7047
|
`report "${reportName}" \xB7 chart`,
|
|
6592
7048
|
`reports[${ri}]`
|
|
@@ -6594,45 +7050,45 @@ function validateChartBindings(stack) {
|
|
|
6594
7050
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
6595
7051
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
6596
7052
|
const block = blocks[bi];
|
|
6597
|
-
if (!
|
|
7053
|
+
if (!isRec17(block)) continue;
|
|
6598
7054
|
checkReportChart(
|
|
6599
7055
|
block.chart,
|
|
6600
|
-
|
|
7056
|
+
strName14(block.dataset),
|
|
6601
7057
|
strList3(block.values),
|
|
6602
|
-
`report "${reportName}" \xB7 block "${
|
|
7058
|
+
`report "${reportName}" \xB7 block "${strName14(block.name) ?? `#${bi}`}" chart`,
|
|
6603
7059
|
`reports[${ri}].blocks[${bi}]`
|
|
6604
7060
|
);
|
|
6605
7061
|
}
|
|
6606
7062
|
}
|
|
6607
7063
|
const checkListChart = (container, where, path) => {
|
|
6608
|
-
if (!
|
|
7064
|
+
if (!isRec17(container)) return;
|
|
6609
7065
|
const chart = container.chart;
|
|
6610
|
-
if (!
|
|
7066
|
+
if (!isRec17(chart)) return;
|
|
6611
7067
|
check({
|
|
6612
|
-
dataset:
|
|
7068
|
+
dataset: strName14(chart.dataset),
|
|
6613
7069
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
6614
7070
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
6615
7071
|
where,
|
|
6616
7072
|
path: `${path}.chart`
|
|
6617
7073
|
});
|
|
6618
7074
|
};
|
|
6619
|
-
const views =
|
|
7075
|
+
const views = asArray31(stack.views);
|
|
6620
7076
|
for (let vi = 0; vi < views.length; vi++) {
|
|
6621
7077
|
const view = views[vi];
|
|
6622
|
-
if (!
|
|
6623
|
-
const viewName =
|
|
7078
|
+
if (!isRec17(view)) continue;
|
|
7079
|
+
const viewName = strName14(view.name) ?? strName14(view.objectName) ?? `#${vi}`;
|
|
6624
7080
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
6625
|
-
if (
|
|
7081
|
+
if (isRec17(view.listViews)) {
|
|
6626
7082
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
6627
7083
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
6628
7084
|
}
|
|
6629
7085
|
}
|
|
6630
7086
|
}
|
|
6631
|
-
const objects =
|
|
7087
|
+
const objects = asArray31(stack.objects);
|
|
6632
7088
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
6633
7089
|
const obj = objects[oi];
|
|
6634
|
-
if (!
|
|
6635
|
-
const objName =
|
|
7090
|
+
if (!isRec17(obj) || !isRec17(obj.listViews)) continue;
|
|
7091
|
+
const objName = strName14(obj.name) ?? `#${oi}`;
|
|
6636
7092
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
6637
7093
|
checkListChart(
|
|
6638
7094
|
lv,
|
|
@@ -6641,22 +7097,22 @@ function validateChartBindings(stack) {
|
|
|
6641
7097
|
);
|
|
6642
7098
|
}
|
|
6643
7099
|
}
|
|
6644
|
-
const pages =
|
|
7100
|
+
const pages = asArray31(stack.pages);
|
|
6645
7101
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6646
7102
|
const page = pages[pi];
|
|
6647
|
-
if (!
|
|
6648
|
-
const pageName =
|
|
7103
|
+
if (!isRec17(page)) continue;
|
|
7104
|
+
const pageName = strName14(page.name) ?? `#${pi}`;
|
|
6649
7105
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
6650
|
-
const props =
|
|
6651
|
-
if (!props || !
|
|
6652
|
-
const axisRefs =
|
|
6653
|
-
const seriesRefs =
|
|
7106
|
+
const props = isRec17(component.properties) ? component.properties : void 0;
|
|
7107
|
+
if (!props || !strName14(props.dataset)) continue;
|
|
7108
|
+
const axisRefs = asArray31(props.yAxis).map((a, ai) => ({ name: strName14(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
7109
|
+
const seriesRefs = asArray31(props.series).map((s, si) => ({ name: strName14(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
6654
7110
|
check({
|
|
6655
|
-
dataset:
|
|
7111
|
+
dataset: strName14(props.dataset),
|
|
6656
7112
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
6657
7113
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
6658
7114
|
series: [...axisRefs, ...seriesRefs],
|
|
6659
|
-
where: `page "${pageName}" \xB7 ${
|
|
7115
|
+
where: `page "${pageName}" \xB7 ${strName14(component.type) ?? "chart"}`,
|
|
6660
7116
|
path: `${path}.properties`
|
|
6661
7117
|
});
|
|
6662
7118
|
}
|
|
@@ -6670,11 +7126,11 @@ var import_meta4 = {};
|
|
|
6670
7126
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
6671
7127
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
6672
7128
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
6673
|
-
var
|
|
6674
|
-
function
|
|
6675
|
-
if (Array.isArray(v)) return v.filter(
|
|
6676
|
-
if (
|
|
6677
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7129
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7130
|
+
function asArray32(v) {
|
|
7131
|
+
if (Array.isArray(v)) return v.filter(isRec18);
|
|
7132
|
+
if (isRec18(v)) {
|
|
7133
|
+
return Object.entries(v).filter(([, def]) => isRec18(def)).map(([name, def]) => ({ name, ...def }));
|
|
6678
7134
|
}
|
|
6679
7135
|
return [];
|
|
6680
7136
|
}
|
|
@@ -6691,7 +7147,7 @@ function loadAjv() {
|
|
|
6691
7147
|
`@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.`
|
|
6692
7148
|
);
|
|
6693
7149
|
}
|
|
6694
|
-
const ctor =
|
|
7150
|
+
const ctor = isRec18(mod) && "default" in mod ? mod.default : mod;
|
|
6695
7151
|
cachedAjv = ctor;
|
|
6696
7152
|
return ctor;
|
|
6697
7153
|
}
|
|
@@ -6706,7 +7162,7 @@ function loadAddFormats() {
|
|
|
6706
7162
|
`@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.`
|
|
6707
7163
|
);
|
|
6708
7164
|
}
|
|
6709
|
-
const plugin =
|
|
7165
|
+
const plugin = isRec18(mod) && "default" in mod ? mod.default : mod;
|
|
6710
7166
|
cachedAddFormats = plugin;
|
|
6711
7167
|
return plugin;
|
|
6712
7168
|
}
|
|
@@ -6736,17 +7192,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
6736
7192
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
6737
7193
|
for (const branch of ["then", "otherwise"]) {
|
|
6738
7194
|
const nested = rule[branch];
|
|
6739
|
-
if (
|
|
7195
|
+
if (isRec18(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
6740
7196
|
}
|
|
6741
7197
|
return out;
|
|
6742
7198
|
}
|
|
6743
7199
|
function walkObjectValidationRules(stack) {
|
|
6744
7200
|
const walked = [];
|
|
6745
|
-
if (!
|
|
6746
|
-
for (const obj of
|
|
7201
|
+
if (!isRec18(stack)) return walked;
|
|
7202
|
+
for (const obj of asArray32(stack.objects)) {
|
|
6747
7203
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
6748
7204
|
const validations = obj.validations;
|
|
6749
|
-
for (const authored of
|
|
7205
|
+
for (const authored of asArray32(validations)) {
|
|
6750
7206
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
6751
7207
|
walked.push({
|
|
6752
7208
|
rule,
|
|
@@ -6777,7 +7233,7 @@ function validateRuleCompilability(stack) {
|
|
|
6777
7233
|
});
|
|
6778
7234
|
}
|
|
6779
7235
|
}
|
|
6780
|
-
if (rule.type === "json_schema" &&
|
|
7236
|
+
if (rule.type === "json_schema" && isRec18(rule.schema)) {
|
|
6781
7237
|
try {
|
|
6782
7238
|
createRuntimeAjv().compile(rule.schema);
|
|
6783
7239
|
} catch (err) {
|
|
@@ -6797,7 +7253,7 @@ function validateRuleCompilability(stack) {
|
|
|
6797
7253
|
|
|
6798
7254
|
// src/validate-rule-schema-formats.ts
|
|
6799
7255
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
6800
|
-
var
|
|
7256
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6801
7257
|
var SUBSCHEMA_KEYS = [
|
|
6802
7258
|
"additionalItems",
|
|
6803
7259
|
"additionalProperties",
|
|
@@ -6821,7 +7277,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
6821
7277
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
6822
7278
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
6823
7279
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
6824
|
-
if (!
|
|
7280
|
+
if (!isRec19(schema)) return;
|
|
6825
7281
|
if (typeof schema.format === "string") {
|
|
6826
7282
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
6827
7283
|
}
|
|
@@ -6840,7 +7296,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
6840
7296
|
}
|
|
6841
7297
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
6842
7298
|
const value = schema[key];
|
|
6843
|
-
if (!
|
|
7299
|
+
if (!isRec19(value)) continue;
|
|
6844
7300
|
for (const [name, entry] of Object.entries(value)) {
|
|
6845
7301
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
6846
7302
|
}
|
|
@@ -6848,13 +7304,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
6848
7304
|
const items = schema.items;
|
|
6849
7305
|
if (Array.isArray(items)) {
|
|
6850
7306
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
6851
|
-
} else if (
|
|
7307
|
+
} else if (isRec19(items)) {
|
|
6852
7308
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
6853
7309
|
}
|
|
6854
7310
|
const dependencies = schema.dependencies;
|
|
6855
|
-
if (
|
|
7311
|
+
if (isRec19(dependencies)) {
|
|
6856
7312
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
6857
|
-
if (!
|
|
7313
|
+
if (!isRec19(entry)) continue;
|
|
6858
7314
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
6859
7315
|
}
|
|
6860
7316
|
}
|
|
@@ -6893,7 +7349,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
6893
7349
|
const findings = [];
|
|
6894
7350
|
const pending = [];
|
|
6895
7351
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
6896
|
-
if (rule.type !== "json_schema" || !
|
|
7352
|
+
if (rule.type !== "json_schema" || !isRec19(rule.schema)) continue;
|
|
6897
7353
|
const uses = [];
|
|
6898
7354
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
6899
7355
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -6921,7 +7377,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
6921
7377
|
var import_system3 = require("@objectstack/spec/system");
|
|
6922
7378
|
|
|
6923
7379
|
// src/build-access-matrix.ts
|
|
6924
|
-
function
|
|
7380
|
+
function asArray33(v) {
|
|
6925
7381
|
if (Array.isArray(v)) return v;
|
|
6926
7382
|
if (v && typeof v === "object") {
|
|
6927
7383
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6932,13 +7388,13 @@ function buildAccessMatrix(stack) {
|
|
|
6932
7388
|
const entries = [];
|
|
6933
7389
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
6934
7390
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
6935
|
-
for (const obj of
|
|
7391
|
+
for (const obj of asArray33(stack.objects)) {
|
|
6936
7392
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
6937
7393
|
if (!name) continue;
|
|
6938
7394
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
6939
7395
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
6940
7396
|
}
|
|
6941
|
-
for (const ps of
|
|
7397
|
+
for (const ps of asArray33(stack.permissions)) {
|
|
6942
7398
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
6943
7399
|
if (!psName) continue;
|
|
6944
7400
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -7011,34 +7467,34 @@ function diffAccessMatrix(before, after) {
|
|
|
7011
7467
|
|
|
7012
7468
|
// src/validate-nav-access.ts
|
|
7013
7469
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
7014
|
-
function
|
|
7470
|
+
function asArray34(v) {
|
|
7015
7471
|
if (Array.isArray(v)) return v;
|
|
7016
7472
|
if (v && typeof v === "object") {
|
|
7017
7473
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7018
7474
|
}
|
|
7019
7475
|
return [];
|
|
7020
7476
|
}
|
|
7021
|
-
function
|
|
7477
|
+
function strName15(v) {
|
|
7022
7478
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7023
7479
|
}
|
|
7024
7480
|
function collectNavExposures(stack) {
|
|
7025
7481
|
const out = [];
|
|
7026
|
-
const apps =
|
|
7482
|
+
const apps = asArray34(stack.apps);
|
|
7027
7483
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
7028
7484
|
const app = apps[ai];
|
|
7029
7485
|
if (!app || typeof app !== "object") continue;
|
|
7030
|
-
const appName =
|
|
7486
|
+
const appName = strName15(app.name) ?? `#${ai}`;
|
|
7031
7487
|
const walk = (items, basePath) => {
|
|
7032
|
-
const navItems =
|
|
7488
|
+
const navItems = asArray34(items);
|
|
7033
7489
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
7034
7490
|
const nav = navItems[ni];
|
|
7035
7491
|
if (!nav || typeof nav !== "object") continue;
|
|
7036
7492
|
const navPath = `${basePath}[${ni}]`;
|
|
7037
|
-
const objectName =
|
|
7493
|
+
const objectName = strName15(nav.objectName);
|
|
7038
7494
|
if (nav.type === "object" && objectName) {
|
|
7039
7495
|
out.push({
|
|
7040
7496
|
objectName,
|
|
7041
|
-
where: `app "${appName}" \xB7 nav "${
|
|
7497
|
+
where: `app "${appName}" \xB7 nav "${strName15(nav.id) ?? `#${ni}`}"`,
|
|
7042
7498
|
path: `${navPath}.objectName`
|
|
7043
7499
|
});
|
|
7044
7500
|
}
|
|
@@ -7046,7 +7502,7 @@ function collectNavExposures(stack) {
|
|
|
7046
7502
|
}
|
|
7047
7503
|
};
|
|
7048
7504
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
7049
|
-
const areas =
|
|
7505
|
+
const areas = asArray34(app.areas);
|
|
7050
7506
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
7051
7507
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
7052
7508
|
}
|
|
@@ -7056,13 +7512,13 @@ function collectNavExposures(stack) {
|
|
|
7056
7512
|
function validateNavAccess(stack) {
|
|
7057
7513
|
const findings = [];
|
|
7058
7514
|
if (!stack || typeof stack !== "object") return findings;
|
|
7059
|
-
const permissionSets =
|
|
7515
|
+
const permissionSets = asArray34(stack.permissions);
|
|
7060
7516
|
if (permissionSets.length === 0) return findings;
|
|
7061
7517
|
const exposures = collectNavExposures(stack);
|
|
7062
7518
|
if (exposures.length === 0) return findings;
|
|
7063
7519
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
7064
|
-
for (const obj of
|
|
7065
|
-
const n =
|
|
7520
|
+
for (const obj of asArray34(stack.objects)) {
|
|
7521
|
+
const n = strName15(obj.name);
|
|
7066
7522
|
if (n) ownObjects.add(n);
|
|
7067
7523
|
}
|
|
7068
7524
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -7095,15 +7551,15 @@ var import_spec4 = require("@objectstack/spec");
|
|
|
7095
7551
|
var import_system4 = require("@objectstack/spec/system");
|
|
7096
7552
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
7097
7553
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
7098
|
-
function
|
|
7554
|
+
function isRec20(v) {
|
|
7099
7555
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7100
7556
|
}
|
|
7101
|
-
function
|
|
7557
|
+
function asArray35(v) {
|
|
7102
7558
|
if (Array.isArray(v)) return v;
|
|
7103
|
-
if (
|
|
7559
|
+
if (isRec20(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec20(def) ? def : {} }));
|
|
7104
7560
|
return [];
|
|
7105
7561
|
}
|
|
7106
|
-
function
|
|
7562
|
+
function strName16(v) {
|
|
7107
7563
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7108
7564
|
}
|
|
7109
7565
|
function distance5(a, b) {
|
|
@@ -7163,34 +7619,34 @@ function collectViewRecord(view, factsFor) {
|
|
|
7163
7619
|
};
|
|
7164
7620
|
const addSections = (container, binding) => {
|
|
7165
7621
|
if (!binding) return;
|
|
7166
|
-
for (const section of
|
|
7167
|
-
const sectionName =
|
|
7622
|
+
for (const section of asArray35(container.sections)) {
|
|
7623
|
+
const sectionName = strName16(section.name);
|
|
7168
7624
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
7169
7625
|
}
|
|
7170
7626
|
};
|
|
7171
|
-
const listBinding =
|
|
7172
|
-
if (
|
|
7173
|
-
addView(recordObject ?? listBinding,
|
|
7627
|
+
const listBinding = isRec20(view.list) ? bindingOf(view.list) : void 0;
|
|
7628
|
+
if (isRec20(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
7629
|
+
addView(recordObject ?? listBinding, strName16(view.name));
|
|
7174
7630
|
const named = namedViewKeys(view);
|
|
7175
7631
|
for (const family of ["listViews", "formViews"]) {
|
|
7176
7632
|
const container = view[family];
|
|
7177
|
-
if (!
|
|
7633
|
+
if (!isRec20(container)) continue;
|
|
7178
7634
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
7179
7635
|
let at = 0;
|
|
7180
7636
|
for (const sub of Object.values(container)) {
|
|
7181
7637
|
if (!sub || typeof sub !== "object") continue;
|
|
7182
7638
|
const registryKey = registryKeys[at++];
|
|
7183
|
-
if (!
|
|
7639
|
+
if (!isRec20(sub)) continue;
|
|
7184
7640
|
const binding = bindingOf(sub) ?? listBinding;
|
|
7185
7641
|
addView(binding, registryKey);
|
|
7186
7642
|
addSections(sub, binding);
|
|
7187
7643
|
}
|
|
7188
7644
|
}
|
|
7189
|
-
if (
|
|
7645
|
+
if (isRec20(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
7190
7646
|
addSections(view, recordObject ?? listBinding);
|
|
7191
7647
|
}
|
|
7192
7648
|
function defaultListViewKey(object, container) {
|
|
7193
|
-
if (!object || !
|
|
7649
|
+
if (!object || !isRec20(container.list)) return void 0;
|
|
7194
7650
|
const item = (0, import_spec4.expandViewContainer)(object, container).find(
|
|
7195
7651
|
(i) => i.viewKind === "list" && i.isDefault
|
|
7196
7652
|
);
|
|
@@ -7202,7 +7658,7 @@ function namedViewKeys(container) {
|
|
|
7202
7658
|
const object = "probe";
|
|
7203
7659
|
const prefix = `${object}.`;
|
|
7204
7660
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
7205
|
-
const countEntries = (v) =>
|
|
7661
|
+
const countEntries = (v) => isRec20(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
7206
7662
|
const listCount = countEntries(container.listViews);
|
|
7207
7663
|
const formCount = countEntries(container.formViews);
|
|
7208
7664
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -7220,14 +7676,14 @@ function readOptions(field) {
|
|
|
7220
7676
|
values.add(opt);
|
|
7221
7677
|
continue;
|
|
7222
7678
|
}
|
|
7223
|
-
if (!
|
|
7224
|
-
const value =
|
|
7679
|
+
if (!isRec20(opt)) continue;
|
|
7680
|
+
const value = strName16(opt.value);
|
|
7225
7681
|
if (!value) continue;
|
|
7226
7682
|
values.add(value);
|
|
7227
|
-
const label2 =
|
|
7683
|
+
const label2 = strName16(opt.label);
|
|
7228
7684
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
7229
7685
|
}
|
|
7230
|
-
} else if (
|
|
7686
|
+
} else if (isRec20(raw)) {
|
|
7231
7687
|
for (const [value, label2] of Object.entries(raw)) {
|
|
7232
7688
|
values.add(value);
|
|
7233
7689
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -7247,48 +7703,48 @@ function buildUniverse(stack) {
|
|
|
7247
7703
|
}
|
|
7248
7704
|
return facts;
|
|
7249
7705
|
};
|
|
7250
|
-
for (const obj of
|
|
7251
|
-
const objectName =
|
|
7706
|
+
for (const obj of asArray35(stack.objects)) {
|
|
7707
|
+
const objectName = strName16(obj.name);
|
|
7252
7708
|
if (!objectName) continue;
|
|
7253
7709
|
const facts = factsFor(objectName);
|
|
7254
|
-
for (const field of
|
|
7255
|
-
const fieldName =
|
|
7710
|
+
for (const field of asArray35(obj.fields)) {
|
|
7711
|
+
const fieldName = strName16(field.name);
|
|
7256
7712
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
7257
7713
|
}
|
|
7258
|
-
for (const action of
|
|
7259
|
-
const actionName =
|
|
7714
|
+
for (const action of asArray35(obj.actions)) {
|
|
7715
|
+
const actionName = strName16(action.name);
|
|
7260
7716
|
if (actionName) facts.actions.set(actionName, action);
|
|
7261
7717
|
}
|
|
7262
|
-
for (const view of
|
|
7263
|
-
collectViewRecord({ ...view, object:
|
|
7718
|
+
for (const view of asArray35(obj.views)) {
|
|
7719
|
+
collectViewRecord({ ...view, object: strName16(view.object) ?? objectName }, factsFor);
|
|
7264
7720
|
}
|
|
7265
7721
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
7266
|
-
for (const group of
|
|
7267
|
-
const key =
|
|
7722
|
+
for (const group of asArray35(obj.fieldGroups)) {
|
|
7723
|
+
const key = strName16(group.key) ?? strName16(group.name);
|
|
7268
7724
|
if (key) facts.sections.add(key);
|
|
7269
7725
|
}
|
|
7270
7726
|
}
|
|
7271
|
-
for (const view of
|
|
7727
|
+
for (const view of asArray35(stack.views)) {
|
|
7272
7728
|
collectViewRecord(view, factsFor);
|
|
7273
7729
|
}
|
|
7274
|
-
const pages =
|
|
7730
|
+
const pages = asArray35(stack.pages);
|
|
7275
7731
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
7276
7732
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
7277
7733
|
if (!walked.objectName) continue;
|
|
7278
|
-
const props =
|
|
7734
|
+
const props = isRec20(walked.component.properties) ? walked.component.properties : void 0;
|
|
7279
7735
|
if (!props) continue;
|
|
7280
|
-
for (const section of
|
|
7281
|
-
const sectionName =
|
|
7736
|
+
for (const section of asArray35(props.sections)) {
|
|
7737
|
+
const sectionName = strName16(section.name);
|
|
7282
7738
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
7283
7739
|
}
|
|
7284
7740
|
}
|
|
7285
7741
|
}
|
|
7286
7742
|
const globalActions = /* @__PURE__ */ new Map();
|
|
7287
7743
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
7288
|
-
for (const action of
|
|
7289
|
-
const actionName =
|
|
7744
|
+
for (const action of asArray35(stack.actions)) {
|
|
7745
|
+
const actionName = strName16(action.name);
|
|
7290
7746
|
if (!actionName) continue;
|
|
7291
|
-
const owner =
|
|
7747
|
+
const owner = strName16(action.objectName) ?? strName16(action.object);
|
|
7292
7748
|
if (owner) {
|
|
7293
7749
|
factsFor(owner).actions.set(actionName, action);
|
|
7294
7750
|
actionOwners.set(actionName, owner);
|
|
@@ -7302,41 +7758,41 @@ function buildUniverse(stack) {
|
|
|
7302
7758
|
}
|
|
7303
7759
|
}
|
|
7304
7760
|
const apps = /* @__PURE__ */ new Map();
|
|
7305
|
-
for (const app of
|
|
7306
|
-
const appName =
|
|
7761
|
+
for (const app of asArray35(stack.apps)) {
|
|
7762
|
+
const appName = strName16(app.name);
|
|
7307
7763
|
if (!appName) continue;
|
|
7308
7764
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
7309
7765
|
const walkNav = (items) => {
|
|
7310
|
-
for (const item of
|
|
7311
|
-
const id =
|
|
7766
|
+
for (const item of asArray35(items)) {
|
|
7767
|
+
const id = strName16(item.id);
|
|
7312
7768
|
if (id) navIds.add(id);
|
|
7313
7769
|
if (item.children) walkNav(item.children);
|
|
7314
7770
|
}
|
|
7315
7771
|
};
|
|
7316
7772
|
walkNav(app.navigation);
|
|
7317
|
-
for (const area of
|
|
7318
|
-
const areaId =
|
|
7773
|
+
for (const area of asArray35(app.areas)) {
|
|
7774
|
+
const areaId = strName16(area.id);
|
|
7319
7775
|
if (areaId) navIds.add(areaId);
|
|
7320
7776
|
walkNav(area.navigation);
|
|
7321
7777
|
}
|
|
7322
7778
|
apps.set(appName, navIds);
|
|
7323
7779
|
}
|
|
7324
7780
|
const dashboards = /* @__PURE__ */ new Map();
|
|
7325
|
-
for (const dash of
|
|
7326
|
-
const dashName =
|
|
7781
|
+
for (const dash of asArray35(stack.dashboards)) {
|
|
7782
|
+
const dashName = strName16(dash.name);
|
|
7327
7783
|
if (!dashName) continue;
|
|
7328
7784
|
const widgets = /* @__PURE__ */ new Set();
|
|
7329
|
-
for (const widget of
|
|
7330
|
-
const id =
|
|
7785
|
+
for (const widget of asArray35(dash.widgets)) {
|
|
7786
|
+
const id = strName16(widget.id) ?? strName16(widget.name);
|
|
7331
7787
|
if (id) widgets.add(id);
|
|
7332
7788
|
}
|
|
7333
7789
|
const actions = /* @__PURE__ */ new Set();
|
|
7334
7790
|
const headerActions = [
|
|
7335
|
-
...
|
|
7336
|
-
...
|
|
7791
|
+
...asArray35(isRec20(dash.header) ? dash.header.actions : void 0),
|
|
7792
|
+
...asArray35(dash.actions)
|
|
7337
7793
|
];
|
|
7338
7794
|
for (const action of headerActions) {
|
|
7339
|
-
const key =
|
|
7795
|
+
const key = strName16(action.actionUrl) ?? strName16(action.url) ?? strName16(action.name);
|
|
7340
7796
|
if (key) actions.add(key);
|
|
7341
7797
|
}
|
|
7342
7798
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -7348,7 +7804,7 @@ function localePath(bundleIndex, locale) {
|
|
|
7348
7804
|
}
|
|
7349
7805
|
function validateTranslationReferences(stack) {
|
|
7350
7806
|
const findings = [];
|
|
7351
|
-
if (!
|
|
7807
|
+
if (!isRec20(stack)) return findings;
|
|
7352
7808
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
7353
7809
|
if (bundles.length === 0) return findings;
|
|
7354
7810
|
const universe = buildUniverse(stack);
|
|
@@ -7357,13 +7813,13 @@ function validateTranslationReferences(stack) {
|
|
|
7357
7813
|
};
|
|
7358
7814
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
7359
7815
|
const bundle = bundles[bi];
|
|
7360
|
-
if (!
|
|
7816
|
+
if (!isRec20(bundle)) continue;
|
|
7361
7817
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
7362
|
-
if (!
|
|
7818
|
+
if (!isRec20(rawData)) continue;
|
|
7363
7819
|
const base = localePath(bi, locale);
|
|
7364
7820
|
const inLocale = `locale "${locale}"`;
|
|
7365
7821
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
7366
|
-
if (!
|
|
7822
|
+
if (!isRec20(rawNode)) continue;
|
|
7367
7823
|
const objPath = `${base}.objects.${objectName}`;
|
|
7368
7824
|
const facts = universe.objects.get(objectName);
|
|
7369
7825
|
if (!facts) {
|
|
@@ -7389,7 +7845,7 @@ function validateTranslationReferences(stack) {
|
|
|
7389
7845
|
);
|
|
7390
7846
|
continue;
|
|
7391
7847
|
}
|
|
7392
|
-
if (!
|
|
7848
|
+
if (!isRec20(rawField)) continue;
|
|
7393
7849
|
checkOptionKeys(findings, {
|
|
7394
7850
|
optionMap: rawField.options,
|
|
7395
7851
|
field,
|
|
@@ -7471,7 +7927,7 @@ function validateTranslationReferences(stack) {
|
|
|
7471
7927
|
);
|
|
7472
7928
|
continue;
|
|
7473
7929
|
}
|
|
7474
|
-
if (!
|
|
7930
|
+
if (!isRec20(rawApp)) continue;
|
|
7475
7931
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
7476
7932
|
if (navIds.has(navId)) continue;
|
|
7477
7933
|
orphan(
|
|
@@ -7494,7 +7950,7 @@ function validateTranslationReferences(stack) {
|
|
|
7494
7950
|
);
|
|
7495
7951
|
continue;
|
|
7496
7952
|
}
|
|
7497
|
-
if (!
|
|
7953
|
+
if (!isRec20(rawDash)) continue;
|
|
7498
7954
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
7499
7955
|
if (dash.widgets.has(widgetId)) continue;
|
|
7500
7956
|
orphan(
|
|
@@ -7519,7 +7975,7 @@ function validateTranslationReferences(stack) {
|
|
|
7519
7975
|
return findings;
|
|
7520
7976
|
}
|
|
7521
7977
|
function asRecord(v) {
|
|
7522
|
-
return
|
|
7978
|
+
return isRec20(v) ? v : {};
|
|
7523
7979
|
}
|
|
7524
7980
|
function checkOptionKeys(findings, ctx) {
|
|
7525
7981
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -7531,7 +7987,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
7531
7987
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
7532
7988
|
where: ctx.where,
|
|
7533
7989
|
path: ctx.path,
|
|
7534
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
7990
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName16(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
7535
7991
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
7536
7992
|
});
|
|
7537
7993
|
return;
|
|
@@ -7550,11 +8006,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
7550
8006
|
}
|
|
7551
8007
|
}
|
|
7552
8008
|
function checkActionParams(findings, ctx) {
|
|
7553
|
-
const rawParams = Object.keys(asRecord(
|
|
8009
|
+
const rawParams = Object.keys(asRecord(isRec20(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
7554
8010
|
if (rawParams.length === 0) return;
|
|
7555
8011
|
const declared = /* @__PURE__ */ new Set();
|
|
7556
|
-
for (const param of
|
|
7557
|
-
const name =
|
|
8012
|
+
for (const param of asArray35(ctx.action.params)) {
|
|
8013
|
+
const name = strName16(param.name) ?? strName16(param.field);
|
|
7558
8014
|
if (name) declared.add(name);
|
|
7559
8015
|
}
|
|
7560
8016
|
for (const paramName of rawParams) {
|
|
@@ -7572,14 +8028,14 @@ function checkActionParams(findings, ctx) {
|
|
|
7572
8028
|
|
|
7573
8029
|
// src/validate-translatable-sections.ts
|
|
7574
8030
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
7575
|
-
function
|
|
8031
|
+
function isRec21(v) {
|
|
7576
8032
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
7577
8033
|
}
|
|
7578
|
-
function
|
|
8034
|
+
function strName17(v) {
|
|
7579
8035
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7580
8036
|
}
|
|
7581
8037
|
function viewLabel(view) {
|
|
7582
|
-
const name =
|
|
8038
|
+
const name = strName17(view.name);
|
|
7583
8039
|
return name ? `view "${name}"` : "";
|
|
7584
8040
|
}
|
|
7585
8041
|
function joinWhere(...parts) {
|
|
@@ -7587,7 +8043,7 @@ function joinWhere(...parts) {
|
|
|
7587
8043
|
}
|
|
7588
8044
|
function collectViewSites(view, basePath, label2, sites) {
|
|
7589
8045
|
const recordObject = viewObjectName(view);
|
|
7590
|
-
const listBinding =
|
|
8046
|
+
const listBinding = isRec21(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
7591
8047
|
for (const site of viewContainerSites(view, basePath)) {
|
|
7592
8048
|
sites.push({
|
|
7593
8049
|
path: `${site.path}.sections`,
|
|
@@ -7601,11 +8057,11 @@ function translatedObjectNames(stack) {
|
|
|
7601
8057
|
const out = /* @__PURE__ */ new Set();
|
|
7602
8058
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
7603
8059
|
for (const bundle of bundles) {
|
|
7604
|
-
if (!
|
|
8060
|
+
if (!isRec21(bundle)) continue;
|
|
7605
8061
|
for (const data of Object.values(bundle)) {
|
|
7606
|
-
if (!
|
|
8062
|
+
if (!isRec21(data) || !isRec21(data.objects)) continue;
|
|
7607
8063
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
7608
|
-
if (
|
|
8064
|
+
if (isRec21(node)) out.add(objectName);
|
|
7609
8065
|
}
|
|
7610
8066
|
}
|
|
7611
8067
|
}
|
|
@@ -7617,22 +8073,22 @@ function suggestedName(label2) {
|
|
|
7617
8073
|
}
|
|
7618
8074
|
function validateTranslatableSections(stack) {
|
|
7619
8075
|
const findings = [];
|
|
7620
|
-
if (!
|
|
8076
|
+
if (!isRec21(stack)) return findings;
|
|
7621
8077
|
const translated = translatedObjectNames(stack);
|
|
7622
8078
|
if (translated.size === 0) return findings;
|
|
7623
8079
|
const sites = [];
|
|
7624
8080
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
7625
|
-
const objectName =
|
|
8081
|
+
const objectName = strName17(obj.name);
|
|
7626
8082
|
if (!objectName) continue;
|
|
7627
8083
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
7628
8084
|
collectViewSites(
|
|
7629
|
-
{ ...view, object:
|
|
8085
|
+
{ ...view, object: strName17(view.object) ?? objectName },
|
|
7630
8086
|
path,
|
|
7631
8087
|
viewLabel(view),
|
|
7632
8088
|
sites
|
|
7633
8089
|
);
|
|
7634
8090
|
}
|
|
7635
|
-
if (
|
|
8091
|
+
if (isRec21(obj.listViews)) {
|
|
7636
8092
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
7637
8093
|
}
|
|
7638
8094
|
}
|
|
@@ -7640,13 +8096,13 @@ function validateTranslatableSections(stack) {
|
|
|
7640
8096
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
7641
8097
|
}
|
|
7642
8098
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
7643
|
-
const pageName =
|
|
8099
|
+
const pageName = strName17(page.name);
|
|
7644
8100
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
7645
8101
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
7646
8102
|
if (!walked.objectName) continue;
|
|
7647
|
-
const props =
|
|
8103
|
+
const props = isRec21(walked.component.properties) ? walked.component.properties : void 0;
|
|
7648
8104
|
if (!props) continue;
|
|
7649
|
-
const type =
|
|
8105
|
+
const type = strName17(walked.component.type) ?? "component";
|
|
7650
8106
|
sites.push({
|
|
7651
8107
|
path: `${walked.path}.properties.sections`,
|
|
7652
8108
|
surface: joinWhere(pageLabel, type),
|
|
@@ -7661,9 +8117,9 @@ function validateTranslatableSections(stack) {
|
|
|
7661
8117
|
if (!Array.isArray(site.sections)) continue;
|
|
7662
8118
|
for (let i = 0; i < site.sections.length; i++) {
|
|
7663
8119
|
const section = site.sections[i];
|
|
7664
|
-
if (!
|
|
7665
|
-
if (
|
|
7666
|
-
const heading =
|
|
8120
|
+
if (!isRec21(section)) continue;
|
|
8121
|
+
if (strName17(section.name)) continue;
|
|
8122
|
+
const heading = strName17(section.label);
|
|
7667
8123
|
if (!heading) continue;
|
|
7668
8124
|
const slug = suggestedName(heading);
|
|
7669
8125
|
findings.push({
|
|
@@ -7681,14 +8137,14 @@ function validateTranslatableSections(stack) {
|
|
|
7681
8137
|
|
|
7682
8138
|
// src/validate-ai-surface-affinity.ts
|
|
7683
8139
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
7684
|
-
function
|
|
8140
|
+
function asArray36(v) {
|
|
7685
8141
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
7686
8142
|
if (v && typeof v === "object") {
|
|
7687
8143
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7688
8144
|
}
|
|
7689
8145
|
return [];
|
|
7690
8146
|
}
|
|
7691
|
-
function
|
|
8147
|
+
function strName18(v) {
|
|
7692
8148
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7693
8149
|
}
|
|
7694
8150
|
function surfaceOf(v) {
|
|
@@ -7698,18 +8154,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
7698
8154
|
const findings = [];
|
|
7699
8155
|
if (!stack || typeof stack !== "object") return findings;
|
|
7700
8156
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
7701
|
-
for (const skill of
|
|
7702
|
-
const n =
|
|
8157
|
+
for (const skill of asArray36(stack.skills)) {
|
|
8158
|
+
const n = strName18(skill.name);
|
|
7703
8159
|
if (n) skillsByName.set(n, skill);
|
|
7704
8160
|
}
|
|
7705
|
-
const agents =
|
|
8161
|
+
const agents = asArray36(stack.agents);
|
|
7706
8162
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
7707
8163
|
const agent = agents[ai];
|
|
7708
|
-
const agentName =
|
|
8164
|
+
const agentName = strName18(agent.name) ?? `#${ai}`;
|
|
7709
8165
|
const agentSurface = surfaceOf(agent.surface);
|
|
7710
8166
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
7711
8167
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
7712
|
-
const ref =
|
|
8168
|
+
const ref = strName18(skillRefs[si]);
|
|
7713
8169
|
if (!ref) continue;
|
|
7714
8170
|
const skill = skillsByName.get(ref);
|
|
7715
8171
|
if (!skill) continue;
|
|
@@ -7731,14 +8187,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
7731
8187
|
// src/validate-ai-tool-references.ts
|
|
7732
8188
|
var import_system5 = require("@objectstack/spec/system");
|
|
7733
8189
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
7734
|
-
function
|
|
8190
|
+
function asArray37(v) {
|
|
7735
8191
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
7736
8192
|
if (v && typeof v === "object") {
|
|
7737
8193
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7738
8194
|
}
|
|
7739
8195
|
return [];
|
|
7740
8196
|
}
|
|
7741
|
-
function
|
|
8197
|
+
function strName19(v) {
|
|
7742
8198
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7743
8199
|
}
|
|
7744
8200
|
function distance6(a, b) {
|
|
@@ -7779,26 +8235,26 @@ function materialisesAsTool(action) {
|
|
|
7779
8235
|
if (!ai || typeof ai !== "object") return false;
|
|
7780
8236
|
const aiRec = ai;
|
|
7781
8237
|
if (aiRec.exposed !== true) return false;
|
|
7782
|
-
if (!
|
|
7783
|
-
const type =
|
|
8238
|
+
if (!strName19(aiRec.description)) return false;
|
|
8239
|
+
const type = strName19(action.type);
|
|
7784
8240
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
7785
8241
|
if (type === "script") return Boolean(action.target || action.body);
|
|
7786
8242
|
return Boolean(action.target);
|
|
7787
8243
|
}
|
|
7788
8244
|
function collectToolUniverse(stack) {
|
|
7789
8245
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
7790
|
-
for (const tool of
|
|
7791
|
-
const n =
|
|
8246
|
+
for (const tool of asArray37(stack.tools)) {
|
|
8247
|
+
const n = strName19(tool.name);
|
|
7792
8248
|
if (n) universe.add(n);
|
|
7793
8249
|
}
|
|
7794
8250
|
const addActionFamily = (actions) => {
|
|
7795
|
-
for (const action of
|
|
7796
|
-
const n =
|
|
8251
|
+
for (const action of asArray37(actions)) {
|
|
8252
|
+
const n = strName19(action.name);
|
|
7797
8253
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
7798
8254
|
}
|
|
7799
8255
|
};
|
|
7800
8256
|
addActionFamily(stack.actions);
|
|
7801
|
-
for (const obj of
|
|
8257
|
+
for (const obj of asArray37(stack.objects)) {
|
|
7802
8258
|
addActionFamily(obj.actions);
|
|
7803
8259
|
}
|
|
7804
8260
|
return universe;
|
|
@@ -7806,13 +8262,13 @@ function collectToolUniverse(stack) {
|
|
|
7806
8262
|
function collectUnexposedActionNames(stack) {
|
|
7807
8263
|
const names = /* @__PURE__ */ new Set();
|
|
7808
8264
|
const scan = (actions) => {
|
|
7809
|
-
for (const action of
|
|
7810
|
-
const n =
|
|
8265
|
+
for (const action of asArray37(actions)) {
|
|
8266
|
+
const n = strName19(action.name);
|
|
7811
8267
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
7812
8268
|
}
|
|
7813
8269
|
};
|
|
7814
8270
|
scan(stack.actions);
|
|
7815
|
-
for (const obj of
|
|
8271
|
+
for (const obj of asArray37(stack.objects)) scan(obj.actions);
|
|
7816
8272
|
return names;
|
|
7817
8273
|
}
|
|
7818
8274
|
function validateAiToolReferences(stack) {
|
|
@@ -7830,13 +8286,13 @@ function validateAiToolReferences(stack) {
|
|
|
7830
8286
|
}
|
|
7831
8287
|
return universe.has(ref);
|
|
7832
8288
|
};
|
|
7833
|
-
const skills =
|
|
8289
|
+
const skills = asArray37(stack.skills);
|
|
7834
8290
|
for (let si = 0; si < skills.length; si++) {
|
|
7835
8291
|
const skill = skills[si];
|
|
7836
|
-
const skillName =
|
|
8292
|
+
const skillName = strName19(skill.name) ?? `#${si}`;
|
|
7837
8293
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
7838
8294
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
7839
|
-
const ref =
|
|
8295
|
+
const ref = strName19(refs[ti]);
|
|
7840
8296
|
if (!ref || resolves(ref)) continue;
|
|
7841
8297
|
const isPattern = ref.endsWith("*");
|
|
7842
8298
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -7856,24 +8312,24 @@ function validateAiToolReferences(stack) {
|
|
|
7856
8312
|
// src/validate-ai-agent-authoring.ts
|
|
7857
8313
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
7858
8314
|
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
7859
|
-
function
|
|
8315
|
+
function asArray38(v) {
|
|
7860
8316
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
7861
8317
|
if (v && typeof v === "object") {
|
|
7862
8318
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7863
8319
|
}
|
|
7864
8320
|
return [];
|
|
7865
8321
|
}
|
|
7866
|
-
function
|
|
8322
|
+
function strName20(v) {
|
|
7867
8323
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7868
8324
|
}
|
|
7869
8325
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
7870
8326
|
function validateAiAgentAuthoring(stack) {
|
|
7871
8327
|
const findings = [];
|
|
7872
8328
|
if (!stack || typeof stack !== "object") return findings;
|
|
7873
|
-
const agents =
|
|
8329
|
+
const agents = asArray38(stack.agents);
|
|
7874
8330
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
7875
8331
|
const agent = agents[ai];
|
|
7876
|
-
const name =
|
|
8332
|
+
const name = strName20(agent.name) ?? `#${ai}`;
|
|
7877
8333
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
7878
8334
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
7879
8335
|
findings.push({
|
|
@@ -7886,12 +8342,12 @@ function validateAiAgentAuthoring(stack) {
|
|
|
7886
8342
|
});
|
|
7887
8343
|
}
|
|
7888
8344
|
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
7889
|
-
const apps =
|
|
8345
|
+
const apps = asArray38(stack.apps);
|
|
7890
8346
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
7891
8347
|
const app = apps[appIdx];
|
|
7892
|
-
const defaultAgent =
|
|
8348
|
+
const defaultAgent = strName20(app.defaultAgent);
|
|
7893
8349
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
7894
|
-
const appName =
|
|
8350
|
+
const appName = strName20(app.name) ?? `#${appIdx}`;
|
|
7895
8351
|
findings.push({
|
|
7896
8352
|
severity: "warning",
|
|
7897
8353
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -7994,24 +8450,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
7994
8450
|
"owner",
|
|
7995
8451
|
"record_type"
|
|
7996
8452
|
]);
|
|
7997
|
-
var
|
|
7998
|
-
function
|
|
7999
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8000
|
-
if (
|
|
8453
|
+
var isRec22 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8454
|
+
function asArray39(v) {
|
|
8455
|
+
if (Array.isArray(v)) return v.filter((x) => isRec22(x));
|
|
8456
|
+
if (isRec22(v)) {
|
|
8001
8457
|
return Object.entries(v).map(([name, def]) => ({
|
|
8002
8458
|
name,
|
|
8003
|
-
...
|
|
8459
|
+
...isRec22(def) ? def : {}
|
|
8004
8460
|
}));
|
|
8005
8461
|
}
|
|
8006
8462
|
return [];
|
|
8007
8463
|
}
|
|
8008
8464
|
function indexObjectFields2(stack) {
|
|
8009
8465
|
const out = /* @__PURE__ */ new Map();
|
|
8010
|
-
for (const obj of
|
|
8466
|
+
for (const obj of asArray39(stack.objects)) {
|
|
8011
8467
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
8012
8468
|
if (!name) continue;
|
|
8013
8469
|
const names = /* @__PURE__ */ new Set();
|
|
8014
|
-
for (const f of
|
|
8470
|
+
for (const f of asArray39(obj.fields)) {
|
|
8015
8471
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
8016
8472
|
}
|
|
8017
8473
|
out.set(name, names);
|
|
@@ -8141,12 +8597,12 @@ ${source}
|
|
|
8141
8597
|
}
|
|
8142
8598
|
function validateHookBodyWrites(stack) {
|
|
8143
8599
|
const findings = [];
|
|
8144
|
-
const hooks =
|
|
8600
|
+
const hooks = asArray39(stack.hooks);
|
|
8145
8601
|
if (hooks.length === 0) return findings;
|
|
8146
8602
|
let objectFields = null;
|
|
8147
8603
|
hooks.forEach((hook, hookIndex) => {
|
|
8148
8604
|
const body = hook.body;
|
|
8149
|
-
if (!
|
|
8605
|
+
if (!isRec22(body) || body.language !== "js") return;
|
|
8150
8606
|
const source = body.source;
|
|
8151
8607
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8152
8608
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -8227,13 +8683,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
8227
8683
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
8228
8684
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
8229
8685
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
8230
|
-
var
|
|
8231
|
-
function
|
|
8232
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8233
|
-
if (
|
|
8686
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8687
|
+
function asArray40(v) {
|
|
8688
|
+
if (Array.isArray(v)) return v.filter((x) => isRec23(x));
|
|
8689
|
+
if (isRec23(v)) {
|
|
8234
8690
|
return Object.entries(v).map(([name, def]) => ({
|
|
8235
8691
|
name,
|
|
8236
|
-
...
|
|
8692
|
+
...isRec23(def) ? def : {}
|
|
8237
8693
|
}));
|
|
8238
8694
|
}
|
|
8239
8695
|
return [];
|
|
@@ -8247,11 +8703,11 @@ function collectActionBodies(stack) {
|
|
|
8247
8703
|
const sites = [];
|
|
8248
8704
|
const seen = /* @__PURE__ */ new Set();
|
|
8249
8705
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
8250
|
-
|
|
8706
|
+
asArray40(actions).forEach((action, index) => {
|
|
8251
8707
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
8252
8708
|
if (type !== "script") return;
|
|
8253
8709
|
const body = action.body;
|
|
8254
|
-
if (!
|
|
8710
|
+
if (!isRec23(body) || body.language !== "js") return;
|
|
8255
8711
|
const source = body.source;
|
|
8256
8712
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
8257
8713
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -8262,7 +8718,7 @@ function collectActionBodies(stack) {
|
|
|
8262
8718
|
});
|
|
8263
8719
|
};
|
|
8264
8720
|
collect(stack.actions, "actions");
|
|
8265
|
-
|
|
8721
|
+
asArray40(stack.objects).forEach((obj, objIndex) => {
|
|
8266
8722
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
8267
8723
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
8268
8724
|
});
|
|
@@ -8270,7 +8726,7 @@ function collectActionBodies(stack) {
|
|
|
8270
8726
|
}
|
|
8271
8727
|
function validateActionBodyWrites(stack) {
|
|
8272
8728
|
const findings = [];
|
|
8273
|
-
if (!
|
|
8729
|
+
if (!isRec23(stack)) return findings;
|
|
8274
8730
|
const sites = collectActionBodies(stack);
|
|
8275
8731
|
if (sites.length === 0) return findings;
|
|
8276
8732
|
let objectFields = null;
|
|
@@ -8329,13 +8785,13 @@ var import_shared3 = require("@objectstack/spec/shared");
|
|
|
8329
8785
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
8330
8786
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
8331
8787
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
8332
|
-
var
|
|
8333
|
-
function
|
|
8334
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
8335
|
-
if (
|
|
8788
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8789
|
+
function asArray41(v) {
|
|
8790
|
+
if (Array.isArray(v)) return v.filter((x) => isRec24(x));
|
|
8791
|
+
if (isRec24(v)) {
|
|
8336
8792
|
return Object.entries(v).map(([name, def]) => ({
|
|
8337
8793
|
name,
|
|
8338
|
-
...
|
|
8794
|
+
...isRec24(def) ? def : {}
|
|
8339
8795
|
}));
|
|
8340
8796
|
}
|
|
8341
8797
|
return [];
|
|
@@ -8348,8 +8804,8 @@ function readLiteralObjectName2(config) {
|
|
|
8348
8804
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
8349
8805
|
function validateFlowNodeWrites(stack) {
|
|
8350
8806
|
const findings = [];
|
|
8351
|
-
if (!
|
|
8352
|
-
const flows =
|
|
8807
|
+
if (!isRec24(stack)) return findings;
|
|
8808
|
+
const flows = asArray41(stack.flows);
|
|
8353
8809
|
if (flows.length === 0) return findings;
|
|
8354
8810
|
let objectFields = null;
|
|
8355
8811
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -8357,10 +8813,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
8357
8813
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
8358
8814
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
8359
8815
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
8360
|
-
const config =
|
|
8816
|
+
const config = isRec24(node.config) ? node.config : void 0;
|
|
8361
8817
|
if (!config) return;
|
|
8362
8818
|
const fields = config.fields;
|
|
8363
|
-
if (!
|
|
8819
|
+
if (!isRec24(fields)) return;
|
|
8364
8820
|
const written = Object.keys(fields);
|
|
8365
8821
|
if (written.length === 0) return;
|
|
8366
8822
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -8406,6 +8862,15 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
8406
8862
|
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
8407
8863
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
8408
8864
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
8865
|
+
// [#7912] The THIRD question about a nav entry, after "does the target
|
|
8866
|
+
// resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
|
|
8867
|
+
// destination serve at all? An object's own `enable` block can make its list
|
|
8868
|
+
// answer 404/405 for every persona, and no gate authorable on the entry
|
|
8869
|
+
// expresses that — which is how #7544's dead row survived review for a year.
|
|
8870
|
+
// The server now prunes such an entry from the `/meta` payload; the
|
|
8871
|
+
// maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
|
|
8872
|
+
// prune is never silent to the author who wrote the row.
|
|
8873
|
+
{ name: "validateNavObjectServability", run: validateNavObjectServability },
|
|
8409
8874
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
8410
8875
|
// The same family from the other end (#5417). Its sibling above asks "does
|
|
8411
8876
|
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
@@ -8501,8 +8966,8 @@ function validateReferenceIntegrity(stack) {
|
|
|
8501
8966
|
|
|
8502
8967
|
// src/lint-flow-patterns.ts
|
|
8503
8968
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
8504
|
-
var
|
|
8505
|
-
function
|
|
8969
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
8970
|
+
function asArray42(v) {
|
|
8506
8971
|
if (Array.isArray(v)) return v;
|
|
8507
8972
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8508
8973
|
return [];
|
|
@@ -8776,7 +9241,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
8776
9241
|
function filterCarriesNoCondition(filter) {
|
|
8777
9242
|
if (filter === void 0 || filter === null) return true;
|
|
8778
9243
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
8779
|
-
return (0,
|
|
9244
|
+
return (0, import_data10.reduceFilterVerdict)(filter) === "true";
|
|
8780
9245
|
}
|
|
8781
9246
|
function describeUnboundedFilter(filter) {
|
|
8782
9247
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -8886,7 +9351,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
8886
9351
|
}
|
|
8887
9352
|
function lintFlowPatterns(stack) {
|
|
8888
9353
|
const findings = [];
|
|
8889
|
-
for (const flow of
|
|
9354
|
+
for (const flow of asArray42(stack.flows)) {
|
|
8890
9355
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
8891
9356
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
8892
9357
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -8984,7 +9449,7 @@ var import_node_fs = require("fs");
|
|
|
8984
9449
|
var import_meta6 = {};
|
|
8985
9450
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
8986
9451
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
8987
|
-
function
|
|
9452
|
+
function asArray43(v) {
|
|
8988
9453
|
if (Array.isArray(v)) return v;
|
|
8989
9454
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8990
9455
|
return [];
|
|
@@ -9121,11 +9586,11 @@ function lintLivenessProperties(stack) {
|
|
|
9121
9586
|
const findings = [];
|
|
9122
9587
|
const objectWarn = loadWarnMap(dir, "object");
|
|
9123
9588
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
9124
|
-
for (const obj of
|
|
9589
|
+
for (const obj of asArray43(stack.objects)) {
|
|
9125
9590
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
9126
9591
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
9127
9592
|
if (fieldWarn.size > 0) {
|
|
9128
|
-
for (const field of
|
|
9593
|
+
for (const field of asArray43(obj.fields)) {
|
|
9129
9594
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
9130
9595
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
9131
9596
|
}
|
|
@@ -9134,7 +9599,7 @@ function lintLivenessProperties(stack) {
|
|
|
9134
9599
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
9135
9600
|
const warnMap = loadWarnMap(dir, type);
|
|
9136
9601
|
if (warnMap.size === 0) continue;
|
|
9137
|
-
for (const item of
|
|
9602
|
+
for (const item of asArray43(stack[key])) {
|
|
9138
9603
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
9139
9604
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
9140
9605
|
}
|
|
@@ -9143,12 +9608,12 @@ function lintLivenessProperties(stack) {
|
|
|
9143
9608
|
}
|
|
9144
9609
|
|
|
9145
9610
|
// src/lint-autonumber-formats.ts
|
|
9146
|
-
var
|
|
9611
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
9147
9612
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
9148
9613
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
9149
9614
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
9150
9615
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
9151
|
-
function
|
|
9616
|
+
function asArray44(v) {
|
|
9152
9617
|
if (Array.isArray(v)) return v;
|
|
9153
9618
|
if (v && typeof v === "object") {
|
|
9154
9619
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -9157,9 +9622,9 @@ function asArray43(v) {
|
|
|
9157
9622
|
}
|
|
9158
9623
|
function lintAutonumberFormats(stack) {
|
|
9159
9624
|
const findings = [];
|
|
9160
|
-
for (const obj of
|
|
9625
|
+
for (const obj of asArray44(stack.objects)) {
|
|
9161
9626
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
9162
|
-
const fields =
|
|
9627
|
+
const fields = asArray44(obj.fields);
|
|
9163
9628
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
9164
9629
|
for (const f of fields) {
|
|
9165
9630
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -9169,8 +9634,8 @@ function lintAutonumberFormats(stack) {
|
|
|
9169
9634
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
9170
9635
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
9171
9636
|
if (!fmt) continue;
|
|
9172
|
-
const tokens = (0,
|
|
9173
|
-
const refs = (0,
|
|
9637
|
+
const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
|
|
9638
|
+
const refs = (0, import_data11.referencedFields)(tokens);
|
|
9174
9639
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
9175
9640
|
for (const t of tokens) {
|
|
9176
9641
|
if (t.kind !== "literal") continue;
|
|
@@ -9225,7 +9690,7 @@ function lintAutonumberFormats(stack) {
|
|
|
9225
9690
|
|
|
9226
9691
|
// src/lint-view-refs.ts
|
|
9227
9692
|
var import_spec5 = require("@objectstack/spec");
|
|
9228
|
-
function
|
|
9693
|
+
function asArray45(v) {
|
|
9229
9694
|
if (Array.isArray(v)) return v;
|
|
9230
9695
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
9231
9696
|
return [];
|
|
@@ -9253,7 +9718,7 @@ function lintViewRefs(stack) {
|
|
|
9253
9718
|
s.add(kind);
|
|
9254
9719
|
};
|
|
9255
9720
|
const containers = [];
|
|
9256
|
-
for (const v of
|
|
9721
|
+
for (const v of asArray45(stack.views)) {
|
|
9257
9722
|
if (v.viewKind) {
|
|
9258
9723
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
9259
9724
|
continue;
|
|
@@ -9262,7 +9727,7 @@ function lintViewRefs(stack) {
|
|
|
9262
9727
|
const object = viewContainerObjectName(v);
|
|
9263
9728
|
if (object) containers.push({ object, container: v });
|
|
9264
9729
|
}
|
|
9265
|
-
for (const obj of
|
|
9730
|
+
for (const obj of asArray45(stack.objects)) {
|
|
9266
9731
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
9267
9732
|
if (!object) continue;
|
|
9268
9733
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -9316,11 +9781,11 @@ function lintViewRefs(stack) {
|
|
|
9316
9781
|
});
|
|
9317
9782
|
}
|
|
9318
9783
|
};
|
|
9319
|
-
for (const obj of
|
|
9784
|
+
for (const obj of asArray45(stack.objects)) {
|
|
9320
9785
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
9321
|
-
for (const action of
|
|
9786
|
+
for (const action of asArray45(obj.actions)) checkAction(action, object);
|
|
9322
9787
|
}
|
|
9323
|
-
for (const action of
|
|
9788
|
+
for (const action of asArray45(stack.actions)) checkAction(action);
|
|
9324
9789
|
return findings;
|
|
9325
9790
|
}
|
|
9326
9791
|
|
|
@@ -9624,7 +10089,6 @@ var CLI_ONLY = ["cli"];
|
|
|
9624
10089
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
9625
10090
|
var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
|
|
9626
10091
|
var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
|
|
9627
|
-
var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
|
|
9628
10092
|
var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
|
|
9629
10093
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
9630
10094
|
var AUTHORING_RULES = [
|
|
@@ -9690,6 +10154,31 @@ var AUTHORING_RULES = [
|
|
|
9690
10154
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9691
10155
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
9692
10156
|
},
|
|
10157
|
+
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
10158
|
+
// in `enable.apiMethods` that its own resolved affordances refuse. Every key
|
|
10159
|
+
// is one we know and each is individually valid, so #4001's unknown-key
|
|
10160
|
+
// rejection and the Zod parse both pass it; the contradiction is only visible
|
|
10161
|
+
// when the two keys are read TOGETHER, which nothing did at authoring time.
|
|
10162
|
+
//
|
|
10163
|
+
// `gating` because the declaration is already false when it ships: objectql's
|
|
10164
|
+
// registry strips the verb at registration, so the metadata advertises an API
|
|
10165
|
+
// the product does not serve. That strip has been correct and silent — a
|
|
10166
|
+
// `console.warn` on every control-plane boot that went unread for the life of
|
|
10167
|
+
// a real divergence (`sys_environment`/`sys_package`). This entry is the
|
|
10168
|
+
// ruling's "close it where the author is"; boot stays warn-and-strip.
|
|
10169
|
+
//
|
|
10170
|
+
// Pre-parse: the predicate reads only authored keys, and the finding must
|
|
10171
|
+
// survive an unrelated schema error elsewhere in the stack.
|
|
10172
|
+
{
|
|
10173
|
+
name: "validateManagedApiMethods",
|
|
10174
|
+
tier: "gating",
|
|
10175
|
+
input: "normalized",
|
|
10176
|
+
commands: ALL,
|
|
10177
|
+
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
10178
|
+
surfaces: CLI_ONLY,
|
|
10179
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
10180
|
+
run: (stack) => validateManagedApiMethods(stack)
|
|
10181
|
+
},
|
|
9693
10182
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
9694
10183
|
// in the Console, and the schema step cannot tell it from an intentionally
|
|
9695
10184
|
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
@@ -10069,14 +10558,47 @@ var AUTHORING_RULES = [
|
|
|
10069
10558
|
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
10070
10559
|
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
10071
10560
|
// that gains an `error` finding moves its registry tier in the same edit.
|
|
10561
|
+
//
|
|
10562
|
+
// ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
|
|
10563
|
+
//
|
|
10564
|
+
// This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
|
|
10565
|
+
// in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
|
|
10566
|
+
// `advisories` channel landed (PR #7435). Before that move a `view` written
|
|
10567
|
+
// through Studio / REST `/meta` / MCP — the only door most tenants have, and
|
|
10568
|
+
// the door AI authors use — was judged by NONE of the family's rule ids (six
|
|
10569
|
+
// at the time of the move; seven since #7659 added
|
|
10570
|
+
// `predicate-rhs-path-shaped` inside the second entry).
|
|
10571
|
+
//
|
|
10572
|
+
// They move together on purpose, and the two entries carry one comment because
|
|
10573
|
+
// they are one wall: #7214's implementer wired its own rule here alone and then
|
|
10574
|
+
// REVERTED it, because a `view` refused for an unresolvable predicate PATH
|
|
10575
|
+
// while a predicate that does not parse at all walks through the same door is
|
|
10576
|
+
// less predictable than refusing neither. A half-wired wall is worse than an
|
|
10577
|
+
// unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
|
|
10578
|
+
// directly: every id on this surface is gated at the runtime door, or none is.
|
|
10579
|
+
//
|
|
10580
|
+
// The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
|
|
10581
|
+
// and re-measuring it at move time found it false: both rule functions read
|
|
10582
|
+
// `stack.views` and `stack.pages` and NO other collection — never `objects` —
|
|
10583
|
+
// so the per-write snapshot the gate builds is not partial for them, it is
|
|
10584
|
+
// complete. (`pages` is simply absent on a `view` write, so the page half
|
|
10585
|
+
// contributes zero findings to both differential passes rather than inventing
|
|
10586
|
+
// any.) The reason was not describing this rule; it was the default a rule got
|
|
10587
|
+
// when nobody measured, which is the #4409/#4463 defect one layer in.
|
|
10588
|
+
//
|
|
10589
|
+
// Runtime input tier: the gate hands the rules the body as persisted, without
|
|
10590
|
+
// `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
|
|
10591
|
+
// door. That costs the family nothing — `validateVisibilityPredicates` reads
|
|
10592
|
+
// `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
|
|
10593
|
+
// so a caller handing it a raw authored object still gets a verdict.
|
|
10072
10594
|
{
|
|
10073
10595
|
name: "validateVisibilityPredicates",
|
|
10074
10596
|
tier: "gating",
|
|
10075
10597
|
input: "normalized",
|
|
10076
10598
|
commands: ALL,
|
|
10077
10599
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
10078
|
-
surfaces:
|
|
10079
|
-
|
|
10600
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10601
|
+
runtimeTypes: ["view"],
|
|
10080
10602
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
10081
10603
|
},
|
|
10082
10604
|
// #7010 — the same predicate surface, one question further in. The three
|
|
@@ -10093,14 +10615,28 @@ var AUTHORING_RULES = [
|
|
|
10093
10615
|
// object's addressable path set is NOT closed (lookup traversal, system
|
|
10094
10616
|
// columns, formula outputs), and an `error` gate over an open set generates
|
|
10095
10617
|
// false build errors. See the rule's module note.
|
|
10618
|
+
//
|
|
10619
|
+
// #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
|
|
10620
|
+
// resolution question at all: the metadata-admin renderer resolves paths only
|
|
10621
|
+
// on the LEFT of `==` / `!=` and hands the right side to its literal parser,
|
|
10622
|
+
// so `data.a == data.b` resolves both sides cleanly, passes the two rules
|
|
10623
|
+
// above, and still compares against the string "data.b" — a constant verdict.
|
|
10624
|
+
// It carries `error` on a dotted chain (no reading under which it worked) and
|
|
10625
|
+
// `warning` on a bare word (`status == active` compares as the text today, so
|
|
10626
|
+
// refusing it would fail a build over metadata that renders correctly). The
|
|
10627
|
+
// per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
|
|
10628
|
+
// since #3760; the entry's `gating` tier is unchanged because it already was.
|
|
10096
10629
|
{
|
|
10097
10630
|
name: "validatePredicatePathRefs",
|
|
10098
10631
|
tier: "gating",
|
|
10099
10632
|
input: "normalized",
|
|
10100
10633
|
commands: ALL,
|
|
10101
10634
|
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
10102
|
-
|
|
10103
|
-
|
|
10635
|
+
// The second half of the #7220 family move — see the block above the
|
|
10636
|
+
// `validateVisibilityPredicates` entry. This is the rule whose solo wiring
|
|
10637
|
+
// was reverted; it is wired now because its siblings are.
|
|
10638
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10639
|
+
runtimeTypes: ["view"],
|
|
10104
10640
|
run: (stack) => validatePredicatePathRefs(stack)
|
|
10105
10641
|
},
|
|
10106
10642
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
@@ -10260,16 +10796,110 @@ var AUTHORING_RULES = [
|
|
|
10260
10796
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
10261
10797
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
10262
10798
|
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
10799
|
+
//
|
|
10800
|
+
// [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
|
|
10801
|
+
// was false in both halves — it read: "Already gated at this surface by a
|
|
10802
|
+
// DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
|
|
10803
|
+
// `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
|
|
10804
|
+
// on every runtime write. Running the linter here as well would double-report
|
|
10805
|
+
// one refusal in two vocabularies."
|
|
10806
|
+
//
|
|
10807
|
+
// - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
|
|
10808
|
+
// `externalSharingModel` through a local `OWD_WIDTH`, and never touches
|
|
10809
|
+
// `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
|
|
10810
|
+
// block carries it covers ONE — `security-external-wider-than-internal`
|
|
10811
|
+
// (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
|
|
10812
|
+
// corresponds to no lint rule at all, so it is not coverage in the other
|
|
10813
|
+
// direction either. Twelve rules were enforced at no runtime door while
|
|
10814
|
+
// this field said they were.
|
|
10815
|
+
// - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
|
|
10816
|
+
// `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
|
|
10817
|
+
// `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
|
|
10818
|
+
// both refuse by THROWING. The first to fire ends the write, so an author
|
|
10819
|
+
// sees one refusal, never two. The stated cost of moving was imaginary; the
|
|
10820
|
+
// reason it has not moved is the measured one below.
|
|
10821
|
+
//
|
|
10822
|
+
// The move IS taken now — the #7891 programme's three slices, in order:
|
|
10823
|
+
//
|
|
10824
|
+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
|
|
10825
|
+
// the isolation proof that the differential cancels every finding this
|
|
10826
|
+
// function derives from the sibling collections.
|
|
10827
|
+
// - #8309: the snapshot repair. The gate used to carry `objects` and
|
|
10828
|
+
// nothing else, so the three cross-collection rules judged a universe
|
|
10829
|
+
// missing the collection they compare against (measured: 38 phantom
|
|
10830
|
+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
|
|
10831
|
+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
|
|
10832
|
+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
|
|
10833
|
+
// - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
|
|
10834
|
+
// `object` measured DIRTY on that tree and was escalated, not forced.
|
|
10835
|
+
// - #8310 slice 2 (this state): `object` crosses under the maintainer
|
|
10836
|
+
// ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
|
|
10837
|
+
// authored OWD is REQUIRED at the runtime object door — an object
|
|
10838
|
+
// publish with no authored `sharingModel` is refused with the 422 lint
|
|
10839
|
+
// envelope (`security-owd-unset`); absence is not a decision. The ~16
|
|
10840
|
+
// objectql/rest suite files that relied on OWD-less publishes were
|
|
10841
|
+
// repaired honestly (fixtures author their posture), and
|
|
10842
|
+
// `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
|
|
10843
|
+
// answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
|
|
10844
|
+
// ADR-0094-seam 403 doors answer for what passes lint. The same ruling
|
|
10845
|
+
// retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
|
|
10846
|
+
// of this door (R1 env-tighten-only STAYS — no lint rule covers it);
|
|
10847
|
+
// see `object-posture-gate.ts` and the ADR-0094 amendment.
|
|
10848
|
+
//
|
|
10849
|
+
// `security-role-word` is NOT in this entry any more — that is what the
|
|
10850
|
+
// `validateSecurityRoleWord` entry below records. It judges six collections
|
|
10851
|
+
// (objects, fields, actions, permission sets, positions, apps — plus books),
|
|
10852
|
+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
|
|
10853
|
+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
|
|
10854
|
+
// function that still contained it would have enforced ONE rule id for a
|
|
10855
|
+
// strict subset of its collections: a door where a permission set named
|
|
10856
|
+
// `role_manager` is refused and a position named `sales_role` walks through
|
|
10857
|
+
// — the #7220 failure this table refuses to build, in either direction. The
|
|
10858
|
+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
|
|
10859
|
+
// entry.
|
|
10860
|
+
//
|
|
10861
|
+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
|
|
10862
|
+
// registration, not a per-rule split: the baseline/candidate differential is
|
|
10863
|
+
// what keeps a write of one declared type from leaking the other rules'
|
|
10864
|
+
// whole-stack findings — every finding derived from a sibling collection is
|
|
10865
|
+
// produced byte-identically in both passes and cancels in the diff. Only
|
|
10866
|
+
// findings the written item itself adds are attributed to the write.
|
|
10263
10867
|
{
|
|
10264
10868
|
name: "validateSecurityPosture",
|
|
10265
10869
|
tier: "gating",
|
|
10266
10870
|
input: "parsed",
|
|
10267
10871
|
commands: ALL,
|
|
10268
10872
|
source: "packages/lint/src/validate-security-posture.ts",
|
|
10269
|
-
surfaces:
|
|
10270
|
-
|
|
10873
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10874
|
+
runtimeTypes: ["seed", "permission", "book", "object"],
|
|
10271
10875
|
run: (stack) => validateSecurityPosture(stack)
|
|
10272
10876
|
},
|
|
10877
|
+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
|
|
10878
|
+
// `validateSecurityPosture` the day the rest of that block crossed the
|
|
10879
|
+
// runtime wall — so that it could stay behind WHOLE rather than cross for
|
|
10880
|
+
// three of the six collections it judges (#7220: one rule id must sit on ONE
|
|
10881
|
+
// side of the wall). The split is a surface boundary, not taste: the rule's
|
|
10882
|
+
// verdict and findings are byte-identical to before on every CLI command
|
|
10883
|
+
// (both entries run on all three), and the runtime door does not run it for
|
|
10884
|
+
// ANY type.
|
|
10885
|
+
//
|
|
10886
|
+
// The road to crossing is concrete and short, recorded here so the next
|
|
10887
|
+
// seat prices it correctly: carry `positions`/`apps` in
|
|
10888
|
+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
|
|
10889
|
+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
|
|
10890
|
+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
|
|
10891
|
+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
|
|
10892
|
+
// #7220 discipline satisfied.
|
|
10893
|
+
{
|
|
10894
|
+
name: "validateSecurityRoleWord",
|
|
10895
|
+
tier: "gating",
|
|
10896
|
+
input: "parsed",
|
|
10897
|
+
commands: ALL,
|
|
10898
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
10899
|
+
surfaces: CLI_ONLY,
|
|
10900
|
+
surfaceReason: "P2 (#4463)/#8310: judges six collections (objects, fields, actions, permission sets, positions, apps \u2014 plus books), and the per-write snapshot neither carries nor maps positions/apps. Wiring it for the mapped types alone would enforce one rule id for three of its six collections \u2014 the #7220 split (an object named sales_role refused while a position named sales_role walks through). It crosses whole \u2014 positions/apps carried, mapped and declared \u2014 or stays behind; it stays behind until that wiring exists.",
|
|
10901
|
+
run: (stack) => validateSecurityRoleWord(stack)
|
|
10902
|
+
},
|
|
10273
10903
|
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
10274
10904
|
// sharing rule that walks it builds a second permission hierarchy (the
|
|
10275
10905
|
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
@@ -10320,7 +10950,7 @@ var AUTHORING_RULES = [
|
|
|
10320
10950
|
commands: ALL,
|
|
10321
10951
|
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
10322
10952
|
surfaces: CLI_ONLY,
|
|
10323
|
-
surfaceReason: "
|
|
10953
|
+
surfaceReason: "The rule reads `stack.permissions[]`, which the per-write snapshot DOES carry since #8309 \u2014 the remaining gap is only the declaration: no `runtimeTypes` names `permission` here, and that flip is a rollout decision on #8310's axis, not a wiring fix. Recorded as pending rather than done, because a rule that has never run at a door should not claim it.",
|
|
10324
10954
|
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
10325
10955
|
},
|
|
10326
10956
|
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
@@ -10396,8 +11026,34 @@ var TYPE_TO_STACK_KEY = {
|
|
|
10396
11026
|
dashboard: "dashboards",
|
|
10397
11027
|
agent: "agents",
|
|
10398
11028
|
hook: "hooks",
|
|
10399
|
-
seed
|
|
11029
|
+
// [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
|
|
11030
|
+
// holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
|
|
11031
|
+
// — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
|
|
11032
|
+
// mapping onto one either.
|
|
11033
|
+
//
|
|
11034
|
+
// The wrong spelling was INERT rather than harmless, and it is the #4449 shape
|
|
11035
|
+
// one surface over: the wiring guard asks only that a declared type HAS a
|
|
11036
|
+
// mapping, never that the mapping names a key some rule reads. So it would
|
|
11037
|
+
// have stayed green while the gate built `{ objects, seeds: [item] }` for
|
|
11038
|
+
// every seed write and every rule reading `stack.data` saw nothing — wired,
|
|
11039
|
+
// and running on nothing, with `rulesRun` reporting the rules as having run.
|
|
11040
|
+
// Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
|
|
11041
|
+
// changed no behaviour then; it was corrected here, with the measurement
|
|
11042
|
+
// that found it (#7576), rather than left for the rollout card to trip
|
|
11043
|
+
// over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
|
|
11044
|
+
// mapping is load-bearing today, not merely inert-and-correct.
|
|
11045
|
+
seed: "data",
|
|
11046
|
+
// [#8309] `permission`/`book` map ahead of their registration (#8310), the
|
|
11047
|
+
// same order `seed` arrived in: the mapping plus the enriched snapshot below
|
|
11048
|
+
// are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
|
|
11049
|
+
// a mapping without a declaring rule is inert by construction (the gate
|
|
11050
|
+
// filters by `runtimeTypes` before it ever consults this table), while a
|
|
11051
|
+
// declaration without the mapping is the wired-onto-nothing state the wiring
|
|
11052
|
+
// guard refuses. Landing the mapping first keeps #8310 a registry data edit.
|
|
11053
|
+
permission: "permissions",
|
|
11054
|
+
book: "books"
|
|
10400
11055
|
};
|
|
11056
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
|
|
10401
11057
|
function runtimeAuthoringRulesFor(type) {
|
|
10402
11058
|
return AUTHORING_RULES.filter(
|
|
10403
11059
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -10415,6 +11071,23 @@ function stackKeyForType(type) {
|
|
|
10415
11071
|
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
10416
11072
|
}
|
|
10417
11073
|
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
11074
|
+
function buildRuntimeWriteSnapshots(args) {
|
|
11075
|
+
const stackKey = stackKeyForType(args.type);
|
|
11076
|
+
if (!stackKey) return null;
|
|
11077
|
+
if (!args.item || typeof args.item !== "object") return null;
|
|
11078
|
+
const item = args.item;
|
|
11079
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
11080
|
+
const baseline = {};
|
|
11081
|
+
for (const key of CONTEXT_STACK_KEYS) {
|
|
11082
|
+
const collection = args.context?.[key] ?? [];
|
|
11083
|
+
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
11084
|
+
}
|
|
11085
|
+
const candidate = {
|
|
11086
|
+
...baseline,
|
|
11087
|
+
[stackKey]: [...baseline[stackKey] ?? [], item]
|
|
11088
|
+
};
|
|
11089
|
+
return { baseline, candidate };
|
|
11090
|
+
}
|
|
10418
11091
|
function runRules(rules, stack, ctx) {
|
|
10419
11092
|
const findings = [];
|
|
10420
11093
|
for (const rule of rules) {
|
|
@@ -10437,19 +11110,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10437
11110
|
const rules = runtimeAuthoringRulesFor(args.type);
|
|
10438
11111
|
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
10439
11112
|
if (rules.length === 0) return empty;
|
|
10440
|
-
const
|
|
10441
|
-
|
|
10442
|
-
|
|
10443
|
-
|
|
10444
|
-
|
|
10445
|
-
|
|
11113
|
+
const snapshots = buildRuntimeWriteSnapshots({
|
|
11114
|
+
type: args.type,
|
|
11115
|
+
item: args.item,
|
|
11116
|
+
...args.context !== void 0 ? { context: args.context } : {}
|
|
11117
|
+
});
|
|
11118
|
+
if (!snapshots) return empty;
|
|
10446
11119
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
10447
|
-
const
|
|
10448
|
-
const
|
|
10449
|
-
const baseline = { objects: baselineObjects };
|
|
10450
|
-
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
10451
|
-
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
10452
|
-
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
11120
|
+
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
11121
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
10453
11122
|
return {
|
|
10454
11123
|
errors: added.filter((f) => f.severity === "error"),
|
|
10455
11124
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -10499,6 +11168,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10499
11168
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
10500
11169
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
10501
11170
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
11171
|
+
DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
10502
11172
|
DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
10503
11173
|
EXPRESSION_INVALID,
|
|
10504
11174
|
FIELD_GROUP_EMPTY,
|
|
@@ -10525,6 +11195,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10525
11195
|
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
10526
11196
|
FLOW_PHANTOM_AGGREGATION,
|
|
10527
11197
|
FLOW_RUNAS_UNSCOPED,
|
|
11198
|
+
FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
10528
11199
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
10529
11200
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
10530
11201
|
FLOW_TIME_RELATIVE_ANTIPATTERN,
|
|
@@ -10546,9 +11217,11 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10546
11217
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
10547
11218
|
LIVENESS_DEAD_PROPERTY,
|
|
10548
11219
|
LIVENESS_EXPERIMENTAL_PROPERTY,
|
|
11220
|
+
MANAGED_API_METHOD_UNAFFORDABLE,
|
|
10549
11221
|
MAX_SCHEMA_WALK_DEPTH,
|
|
10550
11222
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
10551
11223
|
NAV_OBJECT_UNGRANTED,
|
|
11224
|
+
NAV_OBJECT_UNSERVABLE,
|
|
10552
11225
|
NAV_TARGET_UNRESOLVED,
|
|
10553
11226
|
NULL_GUARD_HINT,
|
|
10554
11227
|
OBJECT_REFERENCE_UNKNOWN,
|
|
@@ -10557,15 +11230,18 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10557
11230
|
ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
10558
11231
|
ORG_AXIS_PERMISSION_INHERITANCE,
|
|
10559
11232
|
PAGE_FIELD_UNKNOWN,
|
|
11233
|
+
PAGE_FIELD_UNPROVISIONED,
|
|
10560
11234
|
PAGE_SOURCE_CLASSNAME,
|
|
10561
11235
|
PREDICATE_PATH_UNRESOLVED,
|
|
10562
11236
|
PREDICATE_PATH_UNROOTED,
|
|
11237
|
+
PREDICATE_RHS_PATH_SHAPED,
|
|
10563
11238
|
PRE_SEAL_PHASES,
|
|
10564
11239
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
10565
11240
|
REACT_CHART_AGGREGATE_INVALID,
|
|
10566
11241
|
REACT_CHART_AXIS_UNKNOWN,
|
|
10567
11242
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
10568
11243
|
REACT_CHART_FIELD_UNKNOWN,
|
|
11244
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
10569
11245
|
REFERENCE_INTEGRITY_RULES,
|
|
10570
11246
|
RLS_PREDICATE_OVER_BUDGET,
|
|
10571
11247
|
RLS_PREDICATE_UNENFORCEABLE,
|
|
@@ -10576,6 +11252,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10576
11252
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
10577
11253
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
10578
11254
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
11255
|
+
SECURITY_CBP_NO_RELATION,
|
|
10579
11256
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
10580
11257
|
SECURITY_EXTERNAL_WIDER,
|
|
10581
11258
|
SECURITY_FLS_UNQUALIFIED_KEY,
|
|
@@ -10589,6 +11266,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10589
11266
|
SEED_INSERT_MODE_DUPLICATES_ON_REPLAY,
|
|
10590
11267
|
SEED_VALUE_OUTSIDE_STATE_MACHINE,
|
|
10591
11268
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
11269
|
+
SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
10592
11270
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
10593
11271
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
10594
11272
|
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
@@ -10626,6 +11304,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10626
11304
|
WIDGET_MEASURE_UNKNOWN,
|
|
10627
11305
|
authoringRulesFor,
|
|
10628
11306
|
buildAccessMatrix,
|
|
11307
|
+
buildRuntimeWriteSnapshots,
|
|
10629
11308
|
diffAccessMatrix,
|
|
10630
11309
|
extractHookBodyWriteSet,
|
|
10631
11310
|
extractHookBodyWrites,
|
|
@@ -10669,7 +11348,9 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10669
11348
|
validateHookBodyWrites,
|
|
10670
11349
|
validateJsxPages,
|
|
10671
11350
|
validateListViewMode,
|
|
11351
|
+
validateManagedApiMethods,
|
|
10672
11352
|
validateNavAccess,
|
|
11353
|
+
validateNavObjectServability,
|
|
10673
11354
|
validateNavTargetRefs,
|
|
10674
11355
|
validateObjectReferences,
|
|
10675
11356
|
validateOrgAxisRedLines,
|
|
@@ -10687,6 +11368,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
10687
11368
|
validateRuleSchemaFormats,
|
|
10688
11369
|
validateSearchableFields,
|
|
10689
11370
|
validateSecurityPosture,
|
|
11371
|
+
validateSecurityRoleWord,
|
|
10690
11372
|
validateSeedReplaySafety,
|
|
10691
11373
|
validateSeedStateMachine,
|
|
10692
11374
|
validateSemanticRoles,
|