@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/runtime.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/runtime.ts
|
|
21
21
|
var runtime_exports = {};
|
|
22
22
|
__export(runtime_exports, {
|
|
23
|
+
buildRuntimeWriteSnapshots: () => buildRuntimeWriteSnapshots,
|
|
23
24
|
runRuntimeAuthoringRules: () => runRuntimeAuthoringRules,
|
|
24
25
|
runtimeAuthoringRulesFor: () => runtimeAuthoringRulesFor,
|
|
25
26
|
runtimeGatedTypes: () => runtimeGatedTypes,
|
|
@@ -41,6 +42,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
41
42
|
function injectedColumnsFor(objectDef) {
|
|
42
43
|
return (0, import_data.resolveInjectedSystemColumns)(objectDef).names;
|
|
43
44
|
}
|
|
45
|
+
function unprovisionedInjectedColumnsFor(objectDef) {
|
|
46
|
+
return new Set((0, import_data.unprovisionedInjectedColumns)(objectDef));
|
|
47
|
+
}
|
|
48
|
+
function objectDefsOf(stack) {
|
|
49
|
+
if (!stack || typeof stack !== "object") return [];
|
|
50
|
+
const objects = stack.objects;
|
|
51
|
+
if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
|
|
52
|
+
if (objects && typeof objects === "object") {
|
|
53
|
+
return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
|
|
54
|
+
}
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
function indexUnprovisionedAnchors(stack) {
|
|
58
|
+
const index = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const obj of objectDefsOf(stack)) {
|
|
60
|
+
const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
|
|
61
|
+
if (!name) continue;
|
|
62
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
63
|
+
if (anchors.size > 0) index.set(name, anchors);
|
|
64
|
+
}
|
|
65
|
+
return index;
|
|
66
|
+
}
|
|
67
|
+
function unprovisionedAnchorCause(objectName, field) {
|
|
68
|
+
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`;
|
|
69
|
+
}
|
|
70
|
+
function unprovisionedAnchorHint(objectName, field) {
|
|
71
|
+
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).`;
|
|
72
|
+
}
|
|
44
73
|
|
|
45
74
|
// src/validate-null-guards.ts
|
|
46
75
|
var import_formula = require("@objectstack/formula");
|
|
@@ -274,6 +303,37 @@ function buildFieldIndex(objects) {
|
|
|
274
303
|
}
|
|
275
304
|
return idx;
|
|
276
305
|
}
|
|
306
|
+
var BOUND_RECORD_ROOTS = ["record", "previous"];
|
|
307
|
+
function isCelNode(v) {
|
|
308
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
309
|
+
}
|
|
310
|
+
function collectBoundRecordReads(source) {
|
|
311
|
+
const out = /* @__PURE__ */ new Map();
|
|
312
|
+
const ast = (0, import_formula2.parseCelToAst)(source);
|
|
313
|
+
if (!ast) return out;
|
|
314
|
+
const pending = [ast];
|
|
315
|
+
while (pending.length > 0) {
|
|
316
|
+
const celNode = pending.pop();
|
|
317
|
+
if (!isCelNode(celNode)) continue;
|
|
318
|
+
if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
|
|
319
|
+
const [celRecv, seg] = celNode.args;
|
|
320
|
+
if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
|
|
321
|
+
if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const celArgs = celNode.args;
|
|
325
|
+
if (isCelNode(celArgs)) pending.push(celArgs);
|
|
326
|
+
else if (Array.isArray(celArgs)) {
|
|
327
|
+
for (const a of celArgs) {
|
|
328
|
+
if (isCelNode(a)) pending.push(a);
|
|
329
|
+
else if (Array.isArray(a)) {
|
|
330
|
+
for (const b of a) if (isCelNode(b)) pending.push(b);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return out;
|
|
336
|
+
}
|
|
277
337
|
function buildFieldTypeIndex(objects) {
|
|
278
338
|
const idx = /* @__PURE__ */ new Map();
|
|
279
339
|
for (const obj of objects) {
|
|
@@ -373,6 +433,29 @@ function validateStackExpressions(stack) {
|
|
|
373
433
|
const fieldIndex = buildFieldIndex(objects);
|
|
374
434
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
375
435
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
436
|
+
const unprovisionedIndex = /* @__PURE__ */ new Map();
|
|
437
|
+
for (const obj of objects) {
|
|
438
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
439
|
+
if (!name) continue;
|
|
440
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
441
|
+
if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
|
|
442
|
+
}
|
|
443
|
+
const warnUnprovisionedAnchors = (where, raw, objectName) => {
|
|
444
|
+
if (!objectName) return;
|
|
445
|
+
const anchors = unprovisionedIndex.get(objectName);
|
|
446
|
+
if (!anchors) return;
|
|
447
|
+
const source = celSourceOf(raw);
|
|
448
|
+
if (!source) return;
|
|
449
|
+
for (const [field, operand] of collectBoundRecordReads(source)) {
|
|
450
|
+
if (!anchors.has(field)) continue;
|
|
451
|
+
issues.push({
|
|
452
|
+
where,
|
|
453
|
+
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).`,
|
|
454
|
+
source,
|
|
455
|
+
severity: "warning"
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
};
|
|
376
459
|
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
377
460
|
if (!objectName) return;
|
|
378
461
|
const nullableFields = nullableIndex.get(objectName);
|
|
@@ -399,6 +482,7 @@ function validateStackExpressions(stack) {
|
|
|
399
482
|
);
|
|
400
483
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
401
484
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
485
|
+
warnUnprovisionedAnchors(where, raw, objectName);
|
|
402
486
|
};
|
|
403
487
|
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
404
488
|
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
@@ -545,6 +629,7 @@ function validateStackExpressions(stack) {
|
|
|
545
629
|
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
546
630
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
547
631
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
632
|
+
warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
|
|
548
633
|
}
|
|
549
634
|
}
|
|
550
635
|
}
|
|
@@ -750,6 +835,43 @@ function validateFunctionalCompleteness(stack) {
|
|
|
750
835
|
return out;
|
|
751
836
|
}
|
|
752
837
|
|
|
838
|
+
// src/validate-managed-api-methods.ts
|
|
839
|
+
var import_data2 = require("@objectstack/spec/data");
|
|
840
|
+
var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
|
|
841
|
+
var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
842
|
+
function entriesOf2(v) {
|
|
843
|
+
if (Array.isArray(v)) {
|
|
844
|
+
return v.flatMap(
|
|
845
|
+
(def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
if (isRec2(v)) {
|
|
849
|
+
return Object.entries(v).flatMap(
|
|
850
|
+
([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
function validateManagedApiMethods(stack) {
|
|
856
|
+
const out = [];
|
|
857
|
+
if (!isRec2(stack)) return out;
|
|
858
|
+
for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
|
|
859
|
+
const conflicts = (0, import_data2.checkManagedApiMethodAffordances)(obj.def);
|
|
860
|
+
if (conflicts.length === 0) continue;
|
|
861
|
+
const verbs = conflicts.map((c) => c.verb).join(", ");
|
|
862
|
+
const flags = [...new Set(conflicts.map((c) => c.needs))];
|
|
863
|
+
out.push({
|
|
864
|
+
severity: "error",
|
|
865
|
+
rule: MANAGED_API_METHOD_UNAFFORDABLE,
|
|
866
|
+
where: `object "${obj.name}"`,
|
|
867
|
+
path: `objects[${oi}].enable.apiMethods`,
|
|
868
|
+
message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + (0, import_data2.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.`,
|
|
869
|
+
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.`
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
|
|
753
875
|
// src/validate-view-containers.ts
|
|
754
876
|
var VIEW_CONTAINER_SHAPE = "view-container-shape";
|
|
755
877
|
var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
|
|
@@ -767,10 +889,32 @@ function containerViewCount(rec) {
|
|
|
767
889
|
function validateViewContainers(stack) {
|
|
768
890
|
const out = [];
|
|
769
891
|
if (!stack || typeof stack !== "object") return out;
|
|
892
|
+
const viewItems = stack.viewItems;
|
|
893
|
+
if (viewItems != null && asEntries(viewItems).length > 0) {
|
|
894
|
+
out.push({
|
|
895
|
+
severity: "error",
|
|
896
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
897
|
+
where: "viewItems",
|
|
898
|
+
path: "viewItems",
|
|
899
|
+
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.",
|
|
900
|
+
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."
|
|
901
|
+
});
|
|
902
|
+
}
|
|
770
903
|
for (const { key, value } of asEntries(stack.views)) {
|
|
771
904
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
772
905
|
const rec = value;
|
|
773
|
-
if (rec.viewKind != null)
|
|
906
|
+
if (rec.viewKind != null) {
|
|
907
|
+
const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
908
|
+
out.push({
|
|
909
|
+
severity: "error",
|
|
910
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
911
|
+
where: `views${key}${label3}`,
|
|
912
|
+
path: `views${key}`,
|
|
913
|
+
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).",
|
|
914
|
+
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:`."
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
774
918
|
if (containerViewCount(rec) > 0) continue;
|
|
775
919
|
const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
776
920
|
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
@@ -788,7 +932,7 @@ function validateViewContainers(stack) {
|
|
|
788
932
|
}
|
|
789
933
|
|
|
790
934
|
// src/validate-widget-bindings.ts
|
|
791
|
-
var
|
|
935
|
+
var import_data3 = require("@objectstack/spec/data");
|
|
792
936
|
var import_ui = require("@objectstack/spec/ui");
|
|
793
937
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
794
938
|
var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
|
|
@@ -800,6 +944,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
|
800
944
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
801
945
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
802
946
|
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
947
|
+
var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
|
|
803
948
|
var LEGACY_ANALYTICS_KEYS = [
|
|
804
949
|
"categoryField",
|
|
805
950
|
"valueField",
|
|
@@ -919,6 +1064,7 @@ function validateWidgetBindings(stack) {
|
|
|
919
1064
|
}
|
|
920
1065
|
objectFieldTypes.set(o.name, fm);
|
|
921
1066
|
}
|
|
1067
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
922
1068
|
const datasetList = asArray3(stack.datasets);
|
|
923
1069
|
for (let i = 0; i < datasetList.length; i++) {
|
|
924
1070
|
const ds = datasetList[i];
|
|
@@ -931,7 +1077,7 @@ function validateWidgetBindings(stack) {
|
|
|
931
1077
|
const aggregate = typeof m.aggregate === "string" ? m.aggregate : void 0;
|
|
932
1078
|
if (!field || !aggregate) continue;
|
|
933
1079
|
const ftype = fieldTypes.get(field);
|
|
934
|
-
if (ftype && (0,
|
|
1080
|
+
if (ftype && (0, import_data3.isIncoherentAggregate)(aggregate, ftype)) {
|
|
935
1081
|
findings.push({
|
|
936
1082
|
severity: "warning",
|
|
937
1083
|
rule: MEASURE_AGGREGATE_INCOHERENT,
|
|
@@ -1005,13 +1151,24 @@ function validateWidgetBindings(stack) {
|
|
|
1005
1151
|
if (dashFilterDefs.length > 0) {
|
|
1006
1152
|
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
1007
1153
|
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
1008
|
-
|
|
1154
|
+
const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
|
|
1155
|
+
if (objectFields && datasetObject) {
|
|
1009
1156
|
for (const def of dashFilterDefs) {
|
|
1010
1157
|
const eff = effectiveFilterField(w, def);
|
|
1011
1158
|
if (!eff) continue;
|
|
1012
1159
|
const field = eff.field;
|
|
1013
1160
|
if (field.includes(".")) continue;
|
|
1014
|
-
if (objectFields.has(field) || SYSTEM_FIELDS.has(field))
|
|
1161
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
|
|
1162
|
+
if (anchors?.has(field)) {
|
|
1163
|
+
push2({
|
|
1164
|
+
severity: "warning",
|
|
1165
|
+
rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
1166
|
+
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).`,
|
|
1167
|
+
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.`
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1015
1172
|
push2({
|
|
1016
1173
|
severity: "error",
|
|
1017
1174
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
@@ -1248,7 +1405,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
1248
1405
|
}
|
|
1249
1406
|
|
|
1250
1407
|
// src/validate-filter-tokens.ts
|
|
1251
|
-
var
|
|
1408
|
+
var import_data4 = require("@objectstack/spec/data");
|
|
1252
1409
|
|
|
1253
1410
|
// src/filter-walk.ts
|
|
1254
1411
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
@@ -1308,7 +1465,7 @@ function walkAuthoredFilters(stack, surfaces, visit) {
|
|
|
1308
1465
|
|
|
1309
1466
|
// src/validate-filter-tokens.ts
|
|
1310
1467
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
1311
|
-
var KNOWN_LIST =
|
|
1468
|
+
var KNOWN_LIST = import_data4.CONTEXT_TOKENS.join("}, {");
|
|
1312
1469
|
var TOKEN_FILTER_SURFACES = [
|
|
1313
1470
|
{ key: "dashboards", kind: "dashboard" },
|
|
1314
1471
|
{ key: "objects", kind: "object" },
|
|
@@ -1321,7 +1478,7 @@ var TOKEN_FILTER_SURFACES = [
|
|
|
1321
1478
|
function walkFilterValues(node, path, where, out, seen) {
|
|
1322
1479
|
if (node === null || node === void 0) return;
|
|
1323
1480
|
if (typeof node === "string") {
|
|
1324
|
-
const cls = (0,
|
|
1481
|
+
const cls = (0, import_data4.classifyFilterToken)(node);
|
|
1325
1482
|
if (cls?.kind === "unknown") {
|
|
1326
1483
|
const suggestion = cls.suggestion;
|
|
1327
1484
|
out.push({
|
|
@@ -1356,7 +1513,7 @@ function validateFilterTokens(stack) {
|
|
|
1356
1513
|
}
|
|
1357
1514
|
|
|
1358
1515
|
// src/validate-empty-combinators.ts
|
|
1359
|
-
var
|
|
1516
|
+
var import_data5 = require("@objectstack/spec/data");
|
|
1360
1517
|
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
1361
1518
|
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
1362
1519
|
var EMPTY_COMBINATOR_SURFACES = [
|
|
@@ -1375,12 +1532,12 @@ function isFilterNode(value) {
|
|
|
1375
1532
|
return proto === Object.prototype || proto === null;
|
|
1376
1533
|
}
|
|
1377
1534
|
var VERDICT_OF = {
|
|
1378
|
-
$and: (0,
|
|
1379
|
-
$or: (0,
|
|
1380
|
-
$not: (0,
|
|
1381
|
-
node: (0,
|
|
1535
|
+
$and: (0, import_data5.reduceFilterVerdict)({ $and: [] }),
|
|
1536
|
+
$or: (0, import_data5.reduceFilterVerdict)({ $or: [] }),
|
|
1537
|
+
$not: (0, import_data5.reduceFilterVerdict)({ $not: {} }),
|
|
1538
|
+
node: (0, import_data5.reduceFilterVerdict)({}),
|
|
1382
1539
|
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
1383
|
-
orWithEmptyBranch: (0,
|
|
1540
|
+
orWithEmptyBranch: (0, import_data5.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
1384
1541
|
};
|
|
1385
1542
|
function rows(verdict) {
|
|
1386
1543
|
if (verdict === "true") return "matches EVERY row";
|
|
@@ -1665,7 +1822,7 @@ function validateObjectReferences(stack) {
|
|
|
1665
1822
|
}
|
|
1666
1823
|
|
|
1667
1824
|
// src/validate-searchable-fields.ts
|
|
1668
|
-
var
|
|
1825
|
+
var import_data6 = require("@objectstack/spec/data");
|
|
1669
1826
|
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1670
1827
|
var SEARCHABLE_FIELD_UNSEARCHABLE = "searchable-field-unsearchable";
|
|
1671
1828
|
function asArray7(v) {
|
|
@@ -1675,7 +1832,7 @@ function asArray7(v) {
|
|
|
1675
1832
|
}
|
|
1676
1833
|
return [];
|
|
1677
1834
|
}
|
|
1678
|
-
function
|
|
1835
|
+
function isRec3(v) {
|
|
1679
1836
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1680
1837
|
}
|
|
1681
1838
|
function strName3(v) {
|
|
@@ -1713,7 +1870,7 @@ function resolveAllowedSet(target) {
|
|
|
1713
1870
|
fields = { ...fields };
|
|
1714
1871
|
for (const f of systemDeclared) fields[f] = {};
|
|
1715
1872
|
}
|
|
1716
|
-
const { allowed, source } = (0,
|
|
1873
|
+
const { allowed, source } = (0, import_data6.resolveSearchFieldResolution)({
|
|
1717
1874
|
fields,
|
|
1718
1875
|
searchableFields: target.searchableFields,
|
|
1719
1876
|
displayField: target.displayField
|
|
@@ -1751,7 +1908,7 @@ function distance2(a, b) {
|
|
|
1751
1908
|
}
|
|
1752
1909
|
function indexObjectSearchTargets(stack) {
|
|
1753
1910
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1754
|
-
if (!
|
|
1911
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1755
1912
|
for (const obj of asArray7(stack.objects)) {
|
|
1756
1913
|
const name = strName3(obj.name);
|
|
1757
1914
|
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
@@ -1783,7 +1940,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1783
1940
|
});
|
|
1784
1941
|
continue;
|
|
1785
1942
|
}
|
|
1786
|
-
if ((0,
|
|
1943
|
+
if ((0, import_data6.isVirtualSearchField)(target.fields[name])) {
|
|
1787
1944
|
const vtype = target.fields[name]?.type;
|
|
1788
1945
|
findings.push({
|
|
1789
1946
|
severity: "error",
|
|
@@ -1811,7 +1968,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1811
1968
|
}
|
|
1812
1969
|
const isReference = meta?.type === "lookup" || meta?.type === "master_detail";
|
|
1813
1970
|
let why;
|
|
1814
|
-
if (
|
|
1971
|
+
if (import_data6.SEARCH_AUTO_EXCLUDED_FIELDS.has(name)) {
|
|
1815
1972
|
why = "a system/audit column, which the auto-default set never includes";
|
|
1816
1973
|
} else if (meta?.hidden) {
|
|
1817
1974
|
why = "hidden";
|
|
@@ -1825,7 +1982,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1825
1982
|
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
1826
1983
|
where,
|
|
1827
1984
|
path: `${path}[${i}]`,
|
|
1828
|
-
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...
|
|
1985
|
+
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data6.SEARCHABLE_TEXTUAL_TYPES, ...import_data6.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).`,
|
|
1829
1986
|
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.`
|
|
1830
1987
|
});
|
|
1831
1988
|
}
|
|
@@ -1833,7 +1990,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1833
1990
|
}
|
|
1834
1991
|
function validateSearchableFields(stack) {
|
|
1835
1992
|
const findings = [];
|
|
1836
|
-
if (!
|
|
1993
|
+
if (!isRec3(stack)) return findings;
|
|
1837
1994
|
const objects = asArray7(stack.objects);
|
|
1838
1995
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1839
1996
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
@@ -1843,7 +2000,7 @@ function validateSearchableFields(stack) {
|
|
|
1843
2000
|
};
|
|
1844
2001
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1845
2002
|
const obj = objects[oi];
|
|
1846
|
-
if (!
|
|
2003
|
+
if (!isRec3(obj)) continue;
|
|
1847
2004
|
const objName = strName3(obj.name);
|
|
1848
2005
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1849
2006
|
check(
|
|
@@ -1854,9 +2011,9 @@ function validateSearchableFields(stack) {
|
|
|
1854
2011
|
"searchableFields",
|
|
1855
2012
|
"canonical"
|
|
1856
2013
|
);
|
|
1857
|
-
if (
|
|
2014
|
+
if (isRec3(obj.listViews)) {
|
|
1858
2015
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1859
|
-
if (!
|
|
2016
|
+
if (!isRec3(lv)) continue;
|
|
1860
2017
|
check(
|
|
1861
2018
|
lv.searchableFields,
|
|
1862
2019
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1873,10 +2030,10 @@ function validateSearchableFields(stack) {
|
|
|
1873
2030
|
const views = asArray7(stack.views);
|
|
1874
2031
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1875
2032
|
const view = views[vi];
|
|
1876
|
-
if (!
|
|
2033
|
+
if (!isRec3(view)) continue;
|
|
1877
2034
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
1878
2035
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
1879
|
-
if (
|
|
2036
|
+
if (isRec3(view.list)) {
|
|
1880
2037
|
check(
|
|
1881
2038
|
view.list.searchableFields,
|
|
1882
2039
|
listViewObject(view.list) ?? viewObject,
|
|
@@ -1886,9 +2043,9 @@ function validateSearchableFields(stack) {
|
|
|
1886
2043
|
"narrowing"
|
|
1887
2044
|
);
|
|
1888
2045
|
}
|
|
1889
|
-
if (
|
|
2046
|
+
if (isRec3(view.listViews)) {
|
|
1890
2047
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1891
|
-
if (!
|
|
2048
|
+
if (!isRec3(lv)) continue;
|
|
1892
2049
|
check(
|
|
1893
2050
|
lv.searchableFields,
|
|
1894
2051
|
listViewObject(lv) ?? viewObject,
|
|
@@ -1904,11 +2061,11 @@ function validateSearchableFields(stack) {
|
|
|
1904
2061
|
}
|
|
1905
2062
|
function listViewObject(listView) {
|
|
1906
2063
|
const data = listView.data;
|
|
1907
|
-
return
|
|
2064
|
+
return isRec3(data) ? strName3(data.object) : void 0;
|
|
1908
2065
|
}
|
|
1909
2066
|
|
|
1910
2067
|
// src/page-walk.ts
|
|
1911
|
-
function
|
|
2068
|
+
function isRec4(v) {
|
|
1912
2069
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1913
2070
|
}
|
|
1914
2071
|
function strName4(v) {
|
|
@@ -1921,19 +2078,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1921
2078
|
}
|
|
1922
2079
|
function walkPageComponents(page, pagePath) {
|
|
1923
2080
|
const out = [];
|
|
1924
|
-
if (!
|
|
2081
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1925
2082
|
const pageObject = strName4(page.object);
|
|
1926
2083
|
const visit = (node, path, inheritedObject) => {
|
|
1927
|
-
if (!
|
|
1928
|
-
const props =
|
|
1929
|
-
const dataSource =
|
|
2084
|
+
if (!isRec4(node)) return;
|
|
2085
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2086
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1930
2087
|
const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
|
|
1931
2088
|
out.push({ component: node, path, objectName });
|
|
1932
2089
|
if (!props) return;
|
|
1933
2090
|
if (Array.isArray(props.items)) {
|
|
1934
2091
|
for (let i = 0; i < props.items.length; i++) {
|
|
1935
2092
|
const item = props.items[i];
|
|
1936
|
-
if (!
|
|
2093
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1937
2094
|
for (let c = 0; c < item.children.length; c++) {
|
|
1938
2095
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1939
2096
|
}
|
|
@@ -1955,12 +2112,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1955
2112
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1956
2113
|
for (let r = 0; r < regions.length; r++) {
|
|
1957
2114
|
const region = regions[r];
|
|
1958
|
-
if (!
|
|
2115
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1959
2116
|
for (let c = 0; c < region.components.length; c++) {
|
|
1960
2117
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1961
2118
|
}
|
|
1962
2119
|
}
|
|
1963
|
-
const slots =
|
|
2120
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1964
2121
|
if (slots) {
|
|
1965
2122
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1966
2123
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2168,6 +2325,7 @@ function validateActionNameRefs(stack) {
|
|
|
2168
2325
|
|
|
2169
2326
|
// src/validate-page-field-bindings.ts
|
|
2170
2327
|
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
2328
|
+
var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
|
|
2171
2329
|
function asArray9(v) {
|
|
2172
2330
|
if (Array.isArray(v)) return v;
|
|
2173
2331
|
if (v && typeof v === "object") {
|
|
@@ -2178,7 +2336,7 @@ function asArray9(v) {
|
|
|
2178
2336
|
function strName6(v) {
|
|
2179
2337
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2180
2338
|
}
|
|
2181
|
-
function
|
|
2339
|
+
function isRec5(v) {
|
|
2182
2340
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2183
2341
|
}
|
|
2184
2342
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -2189,7 +2347,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
2189
2347
|
out.push({ name: bare, path });
|
|
2190
2348
|
return;
|
|
2191
2349
|
}
|
|
2192
|
-
if (!
|
|
2350
|
+
if (!isRec5(v)) return;
|
|
2193
2351
|
const named = strName6(v.field) ?? strName6(v.name);
|
|
2194
2352
|
if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
|
|
2195
2353
|
};
|
|
@@ -2247,15 +2405,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2247
2405
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2248
2406
|
for (let si = 0; si < sections.length; si++) {
|
|
2249
2407
|
const section = sections[si];
|
|
2250
|
-
if (!
|
|
2408
|
+
if (!isRec5(section)) continue;
|
|
2251
2409
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2252
2410
|
}
|
|
2253
2411
|
}
|
|
2254
2412
|
return refs;
|
|
2255
2413
|
}
|
|
2256
2414
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2257
|
-
const add =
|
|
2258
|
-
const picker = add &&
|
|
2415
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2416
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
2259
2417
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2260
2418
|
return {
|
|
2261
2419
|
relatedObject: strName6(props.objectName),
|
|
@@ -2276,7 +2434,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2276
2434
|
}
|
|
2277
2435
|
function indexObjectFields(stack) {
|
|
2278
2436
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2279
|
-
if (!
|
|
2437
|
+
if (!isRec5(stack)) return objectFields;
|
|
2280
2438
|
for (const obj of asArray9(stack.objects)) {
|
|
2281
2439
|
const name = strName6(obj.name);
|
|
2282
2440
|
if (!name) continue;
|
|
@@ -2289,14 +2447,27 @@ function indexObjectFields(stack) {
|
|
|
2289
2447
|
}
|
|
2290
2448
|
return objectFields;
|
|
2291
2449
|
}
|
|
2292
|
-
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2450
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
|
|
2293
2451
|
const findings = [];
|
|
2294
2452
|
if (!objectName) return findings;
|
|
2295
2453
|
const known = objectFields.get(objectName);
|
|
2296
2454
|
if (!known) return findings;
|
|
2455
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2297
2456
|
for (const ref of refs) {
|
|
2298
2457
|
if (ref.name.includes(".")) continue;
|
|
2299
|
-
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name))
|
|
2458
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
|
|
2459
|
+
if (anchors?.has(ref.name)) {
|
|
2460
|
+
findings.push({
|
|
2461
|
+
severity: "warning",
|
|
2462
|
+
rule: PAGE_FIELD_UNPROVISIONED,
|
|
2463
|
+
where,
|
|
2464
|
+
path: ref.path,
|
|
2465
|
+
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."),
|
|
2466
|
+
hint: unprovisionedAnchorHint(objectName, ref.name)
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2300
2471
|
findings.push({
|
|
2301
2472
|
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2302
2473
|
rule: PAGE_FIELD_UNKNOWN,
|
|
@@ -2312,17 +2483,20 @@ function validatePageFieldBindings(stack) {
|
|
|
2312
2483
|
const findings = [];
|
|
2313
2484
|
if (!stack || typeof stack !== "object") return findings;
|
|
2314
2485
|
const objectFields = indexObjectFields(stack);
|
|
2486
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2315
2487
|
const pages = asArray9(stack.pages);
|
|
2316
2488
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2317
2489
|
const page = pages[pi];
|
|
2318
2490
|
if (!page || typeof page !== "object") continue;
|
|
2319
2491
|
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2320
2492
|
const checkRefs = (refs, objectName, where) => {
|
|
2321
|
-
findings.push(
|
|
2493
|
+
findings.push(
|
|
2494
|
+
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2495
|
+
);
|
|
2322
2496
|
};
|
|
2323
2497
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2324
2498
|
const type = strName6(component.type);
|
|
2325
|
-
const props =
|
|
2499
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
2326
2500
|
if (!type || !props) continue;
|
|
2327
2501
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2328
2502
|
const base = `${path}.properties`;
|
|
@@ -2337,7 +2511,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2337
2511
|
if (!refs) continue;
|
|
2338
2512
|
checkRefs(refs, objectName, where);
|
|
2339
2513
|
}
|
|
2340
|
-
const cfg =
|
|
2514
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2341
2515
|
if (cfg) {
|
|
2342
2516
|
const cfgObject = strName6(cfg.source) ?? strName6(page.object);
|
|
2343
2517
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -2346,7 +2520,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2346
2520
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2347
2521
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2348
2522
|
];
|
|
2349
|
-
const userFilters =
|
|
2523
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2350
2524
|
if (userFilters) {
|
|
2351
2525
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2352
2526
|
}
|
|
@@ -2374,7 +2548,7 @@ function strName7(v) {
|
|
|
2374
2548
|
function strList2(v) {
|
|
2375
2549
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2376
2550
|
}
|
|
2377
|
-
function
|
|
2551
|
+
function isRec6(v) {
|
|
2378
2552
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2379
2553
|
}
|
|
2380
2554
|
function distance4(a, b) {
|
|
@@ -2503,10 +2677,10 @@ function validateChartBindings(stack) {
|
|
|
2503
2677
|
const reports = asArray10(stack.reports);
|
|
2504
2678
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2505
2679
|
const report = reports[ri];
|
|
2506
|
-
if (!
|
|
2680
|
+
if (!isRec6(report)) continue;
|
|
2507
2681
|
const reportName = strName7(report.name) ?? `#${ri}`;
|
|
2508
2682
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2509
|
-
if (!
|
|
2683
|
+
if (!isRec6(chart)) return;
|
|
2510
2684
|
check({
|
|
2511
2685
|
dataset,
|
|
2512
2686
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
@@ -2530,7 +2704,7 @@ function validateChartBindings(stack) {
|
|
|
2530
2704
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2531
2705
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2532
2706
|
const block = blocks[bi];
|
|
2533
|
-
if (!
|
|
2707
|
+
if (!isRec6(block)) continue;
|
|
2534
2708
|
checkReportChart(
|
|
2535
2709
|
block.chart,
|
|
2536
2710
|
strName7(block.dataset),
|
|
@@ -2541,9 +2715,9 @@ function validateChartBindings(stack) {
|
|
|
2541
2715
|
}
|
|
2542
2716
|
}
|
|
2543
2717
|
const checkListChart = (container, where, path) => {
|
|
2544
|
-
if (!
|
|
2718
|
+
if (!isRec6(container)) return;
|
|
2545
2719
|
const chart = container.chart;
|
|
2546
|
-
if (!
|
|
2720
|
+
if (!isRec6(chart)) return;
|
|
2547
2721
|
check({
|
|
2548
2722
|
dataset: strName7(chart.dataset),
|
|
2549
2723
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
@@ -2555,10 +2729,10 @@ function validateChartBindings(stack) {
|
|
|
2555
2729
|
const views = asArray10(stack.views);
|
|
2556
2730
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2557
2731
|
const view = views[vi];
|
|
2558
|
-
if (!
|
|
2732
|
+
if (!isRec6(view)) continue;
|
|
2559
2733
|
const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
|
|
2560
2734
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2561
|
-
if (
|
|
2735
|
+
if (isRec6(view.listViews)) {
|
|
2562
2736
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2563
2737
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2564
2738
|
}
|
|
@@ -2567,7 +2741,7 @@ function validateChartBindings(stack) {
|
|
|
2567
2741
|
const objects = asArray10(stack.objects);
|
|
2568
2742
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2569
2743
|
const obj = objects[oi];
|
|
2570
|
-
if (!
|
|
2744
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
2571
2745
|
const objName = strName7(obj.name) ?? `#${oi}`;
|
|
2572
2746
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2573
2747
|
checkListChart(
|
|
@@ -2580,10 +2754,10 @@ function validateChartBindings(stack) {
|
|
|
2580
2754
|
const pages = asArray10(stack.pages);
|
|
2581
2755
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2582
2756
|
const page = pages[pi];
|
|
2583
|
-
if (!
|
|
2757
|
+
if (!isRec6(page)) continue;
|
|
2584
2758
|
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2585
2759
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2586
|
-
const props =
|
|
2760
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2587
2761
|
if (!props || !strName7(props.dataset)) continue;
|
|
2588
2762
|
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
2589
2763
|
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
@@ -2733,10 +2907,10 @@ function validateNavAccess(stack) {
|
|
|
2733
2907
|
|
|
2734
2908
|
// src/validate-nav-target-refs.ts
|
|
2735
2909
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2736
|
-
var
|
|
2910
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2737
2911
|
function asArray13(v) {
|
|
2738
|
-
if (Array.isArray(v)) return v.filter(
|
|
2739
|
-
if (
|
|
2912
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
2913
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
2740
2914
|
return [];
|
|
2741
2915
|
}
|
|
2742
2916
|
function strName9(v) {
|
|
@@ -2758,7 +2932,7 @@ function namesOf(collection) {
|
|
|
2758
2932
|
}
|
|
2759
2933
|
function validateNavTargetRefs(stack) {
|
|
2760
2934
|
const findings = [];
|
|
2761
|
-
if (!
|
|
2935
|
+
if (!isRec7(stack)) return findings;
|
|
2762
2936
|
const apps = asArray13(stack.apps);
|
|
2763
2937
|
if (apps.length === 0) return findings;
|
|
2764
2938
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2770,7 +2944,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2770
2944
|
const walk = (items, basePath) => {
|
|
2771
2945
|
if (!Array.isArray(items)) return;
|
|
2772
2946
|
for (const [ni, raw] of items.entries()) {
|
|
2773
|
-
if (!
|
|
2947
|
+
if (!isRec7(raw)) continue;
|
|
2774
2948
|
const nav = raw;
|
|
2775
2949
|
const navPath = `${basePath}[${ni}]`;
|
|
2776
2950
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -2801,32 +2975,100 @@ function validateNavTargetRefs(stack) {
|
|
|
2801
2975
|
return findings;
|
|
2802
2976
|
}
|
|
2803
2977
|
|
|
2978
|
+
// src/validate-nav-object-servability.ts
|
|
2979
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
2980
|
+
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2981
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2982
|
+
function asArray14(v) {
|
|
2983
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
2984
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2985
|
+
return [];
|
|
2986
|
+
}
|
|
2987
|
+
function strName10(v) {
|
|
2988
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2989
|
+
}
|
|
2990
|
+
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2991
|
+
function validateNavObjectServability(stack) {
|
|
2992
|
+
const findings = [];
|
|
2993
|
+
if (!isRec8(stack)) return findings;
|
|
2994
|
+
const apps = asArray14(stack.apps);
|
|
2995
|
+
if (apps.length === 0) return findings;
|
|
2996
|
+
const ownEnable = /* @__PURE__ */ new Map();
|
|
2997
|
+
const objects = asArray14(stack.objects);
|
|
2998
|
+
for (const [oi, obj] of objects.entries()) {
|
|
2999
|
+
const n = strName10(obj.name);
|
|
3000
|
+
if (!n) continue;
|
|
3001
|
+
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
3002
|
+
}
|
|
3003
|
+
if (ownEnable.size === 0) return findings;
|
|
3004
|
+
for (const [ai, app] of apps.entries()) {
|
|
3005
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
3006
|
+
const walk = (items, basePath) => {
|
|
3007
|
+
if (!Array.isArray(items)) return;
|
|
3008
|
+
for (const [ni, raw] of items.entries()) {
|
|
3009
|
+
if (!isRec8(raw)) continue;
|
|
3010
|
+
const nav = raw;
|
|
3011
|
+
const navPath = `${basePath}[${ni}]`;
|
|
3012
|
+
if (nav.type === "object") {
|
|
3013
|
+
const target = strName10(nav.objectName);
|
|
3014
|
+
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3015
|
+
if (target && declared && !(0, import_data7.canServeApiOperation)(declared.enable, "list")) {
|
|
3016
|
+
const enable = isRec8(declared.enable) ? declared.enable : {};
|
|
3017
|
+
const apiDisabled = enable.apiEnabled === false;
|
|
3018
|
+
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(", ")})` : "");
|
|
3019
|
+
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
3020
|
+
const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
|
|
3021
|
+
findings.push({
|
|
3022
|
+
severity: "error",
|
|
3023
|
+
rule: NAV_OBJECT_UNSERVABLE,
|
|
3024
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
3025
|
+
// The nav entry is where the dead row is authored; the `enable`
|
|
3026
|
+
// key that condemns it is named in the message, because the fix
|
|
3027
|
+
// may belong at either end.
|
|
3028
|
+
path: `${navPath}.objectName`,
|
|
3029
|
+
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.`,
|
|
3030
|
+
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.`
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
3035
|
+
}
|
|
3036
|
+
};
|
|
3037
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
3038
|
+
for (const [ari, area] of asArray14(app.areas).entries()) {
|
|
3039
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
3040
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return findings;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
2804
3046
|
// src/validate-translation-references.ts
|
|
2805
3047
|
var import_spec = require("@objectstack/spec");
|
|
2806
3048
|
var import_system4 = require("@objectstack/spec/system");
|
|
2807
3049
|
|
|
2808
3050
|
// src/view-walk.ts
|
|
2809
|
-
function
|
|
3051
|
+
function isRec9(v) {
|
|
2810
3052
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2811
3053
|
}
|
|
2812
|
-
function
|
|
3054
|
+
function strName11(v) {
|
|
2813
3055
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2814
3056
|
}
|
|
2815
3057
|
function viewObjectName(view) {
|
|
2816
|
-
return
|
|
3058
|
+
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
|
|
2817
3059
|
}
|
|
2818
3060
|
function viewContainerSites(view, basePath) {
|
|
2819
|
-
if (!
|
|
3061
|
+
if (!isRec9(view)) return [];
|
|
2820
3062
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
2821
|
-
if (
|
|
3063
|
+
if (isRec9(view.form)) {
|
|
2822
3064
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
2823
3065
|
}
|
|
2824
3066
|
for (const key of ["listViews", "formViews"]) {
|
|
2825
3067
|
const container = view[key];
|
|
2826
|
-
if (!
|
|
3068
|
+
if (!isRec9(container)) continue;
|
|
2827
3069
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
2828
3070
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
2829
|
-
if (!
|
|
3071
|
+
if (!isRec9(sub)) continue;
|
|
2830
3072
|
sites.push({
|
|
2831
3073
|
view: sub,
|
|
2832
3074
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -2844,15 +3086,15 @@ function formViewSites(view, basePath) {
|
|
|
2844
3086
|
// src/validate-translation-references.ts
|
|
2845
3087
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
2846
3088
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
2847
|
-
function
|
|
3089
|
+
function isRec10(v) {
|
|
2848
3090
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2849
3091
|
}
|
|
2850
|
-
function
|
|
3092
|
+
function asArray15(v) {
|
|
2851
3093
|
if (Array.isArray(v)) return v;
|
|
2852
|
-
if (
|
|
3094
|
+
if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
|
|
2853
3095
|
return [];
|
|
2854
3096
|
}
|
|
2855
|
-
function
|
|
3097
|
+
function strName12(v) {
|
|
2856
3098
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2857
3099
|
}
|
|
2858
3100
|
function distance5(a, b) {
|
|
@@ -2912,34 +3154,34 @@ function collectViewRecord(view, factsFor) {
|
|
|
2912
3154
|
};
|
|
2913
3155
|
const addSections = (container, binding) => {
|
|
2914
3156
|
if (!binding) return;
|
|
2915
|
-
for (const section of
|
|
2916
|
-
const sectionName =
|
|
3157
|
+
for (const section of asArray15(container.sections)) {
|
|
3158
|
+
const sectionName = strName12(section.name);
|
|
2917
3159
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2918
3160
|
}
|
|
2919
3161
|
};
|
|
2920
|
-
const listBinding =
|
|
2921
|
-
if (
|
|
2922
|
-
addView(recordObject ?? listBinding,
|
|
3162
|
+
const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
|
|
3163
|
+
if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3164
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
2923
3165
|
const named = namedViewKeys(view);
|
|
2924
3166
|
for (const family of ["listViews", "formViews"]) {
|
|
2925
3167
|
const container = view[family];
|
|
2926
|
-
if (!
|
|
3168
|
+
if (!isRec10(container)) continue;
|
|
2927
3169
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
2928
3170
|
let at = 0;
|
|
2929
3171
|
for (const sub of Object.values(container)) {
|
|
2930
3172
|
if (!sub || typeof sub !== "object") continue;
|
|
2931
3173
|
const registryKey = registryKeys[at++];
|
|
2932
|
-
if (!
|
|
3174
|
+
if (!isRec10(sub)) continue;
|
|
2933
3175
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2934
3176
|
addView(binding, registryKey);
|
|
2935
3177
|
addSections(sub, binding);
|
|
2936
3178
|
}
|
|
2937
3179
|
}
|
|
2938
|
-
if (
|
|
3180
|
+
if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2939
3181
|
addSections(view, recordObject ?? listBinding);
|
|
2940
3182
|
}
|
|
2941
3183
|
function defaultListViewKey(object, container) {
|
|
2942
|
-
if (!object || !
|
|
3184
|
+
if (!object || !isRec10(container.list)) return void 0;
|
|
2943
3185
|
const item = (0, import_spec.expandViewContainer)(object, container).find(
|
|
2944
3186
|
(i) => i.viewKind === "list" && i.isDefault
|
|
2945
3187
|
);
|
|
@@ -2951,7 +3193,7 @@ function namedViewKeys(container) {
|
|
|
2951
3193
|
const object = "probe";
|
|
2952
3194
|
const prefix = `${object}.`;
|
|
2953
3195
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
2954
|
-
const countEntries = (v) =>
|
|
3196
|
+
const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
2955
3197
|
const listCount = countEntries(container.listViews);
|
|
2956
3198
|
const formCount = countEntries(container.formViews);
|
|
2957
3199
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -2969,14 +3211,14 @@ function readOptions(field) {
|
|
|
2969
3211
|
values.add(opt);
|
|
2970
3212
|
continue;
|
|
2971
3213
|
}
|
|
2972
|
-
if (!
|
|
2973
|
-
const value =
|
|
3214
|
+
if (!isRec10(opt)) continue;
|
|
3215
|
+
const value = strName12(opt.value);
|
|
2974
3216
|
if (!value) continue;
|
|
2975
3217
|
values.add(value);
|
|
2976
|
-
const label2 =
|
|
3218
|
+
const label2 = strName12(opt.label);
|
|
2977
3219
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2978
3220
|
}
|
|
2979
|
-
} else if (
|
|
3221
|
+
} else if (isRec10(raw)) {
|
|
2980
3222
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2981
3223
|
values.add(value);
|
|
2982
3224
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2996,48 +3238,48 @@ function buildUniverse(stack) {
|
|
|
2996
3238
|
}
|
|
2997
3239
|
return facts;
|
|
2998
3240
|
};
|
|
2999
|
-
for (const obj of
|
|
3000
|
-
const objectName =
|
|
3241
|
+
for (const obj of asArray15(stack.objects)) {
|
|
3242
|
+
const objectName = strName12(obj.name);
|
|
3001
3243
|
if (!objectName) continue;
|
|
3002
3244
|
const facts = factsFor(objectName);
|
|
3003
|
-
for (const field of
|
|
3004
|
-
const fieldName =
|
|
3245
|
+
for (const field of asArray15(obj.fields)) {
|
|
3246
|
+
const fieldName = strName12(field.name);
|
|
3005
3247
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
3006
3248
|
}
|
|
3007
|
-
for (const action of
|
|
3008
|
-
const actionName =
|
|
3249
|
+
for (const action of asArray15(obj.actions)) {
|
|
3250
|
+
const actionName = strName12(action.name);
|
|
3009
3251
|
if (actionName) facts.actions.set(actionName, action);
|
|
3010
3252
|
}
|
|
3011
|
-
for (const view of
|
|
3012
|
-
collectViewRecord({ ...view, object:
|
|
3253
|
+
for (const view of asArray15(obj.views)) {
|
|
3254
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
3013
3255
|
}
|
|
3014
3256
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3015
|
-
for (const group of
|
|
3016
|
-
const key =
|
|
3257
|
+
for (const group of asArray15(obj.fieldGroups)) {
|
|
3258
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
3017
3259
|
if (key) facts.sections.add(key);
|
|
3018
3260
|
}
|
|
3019
3261
|
}
|
|
3020
|
-
for (const view of
|
|
3262
|
+
for (const view of asArray15(stack.views)) {
|
|
3021
3263
|
collectViewRecord(view, factsFor);
|
|
3022
3264
|
}
|
|
3023
|
-
const pages =
|
|
3265
|
+
const pages = asArray15(stack.pages);
|
|
3024
3266
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3025
3267
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3026
3268
|
if (!walked.objectName) continue;
|
|
3027
|
-
const props =
|
|
3269
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
3028
3270
|
if (!props) continue;
|
|
3029
|
-
for (const section of
|
|
3030
|
-
const sectionName =
|
|
3271
|
+
for (const section of asArray15(props.sections)) {
|
|
3272
|
+
const sectionName = strName12(section.name);
|
|
3031
3273
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3032
3274
|
}
|
|
3033
3275
|
}
|
|
3034
3276
|
}
|
|
3035
3277
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3036
3278
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3037
|
-
for (const action of
|
|
3038
|
-
const actionName =
|
|
3279
|
+
for (const action of asArray15(stack.actions)) {
|
|
3280
|
+
const actionName = strName12(action.name);
|
|
3039
3281
|
if (!actionName) continue;
|
|
3040
|
-
const owner =
|
|
3282
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
3041
3283
|
if (owner) {
|
|
3042
3284
|
factsFor(owner).actions.set(actionName, action);
|
|
3043
3285
|
actionOwners.set(actionName, owner);
|
|
@@ -3051,41 +3293,41 @@ function buildUniverse(stack) {
|
|
|
3051
3293
|
}
|
|
3052
3294
|
}
|
|
3053
3295
|
const apps = /* @__PURE__ */ new Map();
|
|
3054
|
-
for (const app of
|
|
3055
|
-
const appName =
|
|
3296
|
+
for (const app of asArray15(stack.apps)) {
|
|
3297
|
+
const appName = strName12(app.name);
|
|
3056
3298
|
if (!appName) continue;
|
|
3057
3299
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3058
3300
|
const walkNav = (items) => {
|
|
3059
|
-
for (const item of
|
|
3060
|
-
const id =
|
|
3301
|
+
for (const item of asArray15(items)) {
|
|
3302
|
+
const id = strName12(item.id);
|
|
3061
3303
|
if (id) navIds.add(id);
|
|
3062
3304
|
if (item.children) walkNav(item.children);
|
|
3063
3305
|
}
|
|
3064
3306
|
};
|
|
3065
3307
|
walkNav(app.navigation);
|
|
3066
|
-
for (const area of
|
|
3067
|
-
const areaId =
|
|
3308
|
+
for (const area of asArray15(app.areas)) {
|
|
3309
|
+
const areaId = strName12(area.id);
|
|
3068
3310
|
if (areaId) navIds.add(areaId);
|
|
3069
3311
|
walkNav(area.navigation);
|
|
3070
3312
|
}
|
|
3071
3313
|
apps.set(appName, navIds);
|
|
3072
3314
|
}
|
|
3073
3315
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3074
|
-
for (const dash of
|
|
3075
|
-
const dashName =
|
|
3316
|
+
for (const dash of asArray15(stack.dashboards)) {
|
|
3317
|
+
const dashName = strName12(dash.name);
|
|
3076
3318
|
if (!dashName) continue;
|
|
3077
3319
|
const widgets = /* @__PURE__ */ new Set();
|
|
3078
|
-
for (const widget of
|
|
3079
|
-
const id =
|
|
3320
|
+
for (const widget of asArray15(dash.widgets)) {
|
|
3321
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
3080
3322
|
if (id) widgets.add(id);
|
|
3081
3323
|
}
|
|
3082
3324
|
const actions = /* @__PURE__ */ new Set();
|
|
3083
3325
|
const headerActions = [
|
|
3084
|
-
...
|
|
3085
|
-
...
|
|
3326
|
+
...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
|
|
3327
|
+
...asArray15(dash.actions)
|
|
3086
3328
|
];
|
|
3087
3329
|
for (const action of headerActions) {
|
|
3088
|
-
const key =
|
|
3330
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
3089
3331
|
if (key) actions.add(key);
|
|
3090
3332
|
}
|
|
3091
3333
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3097,7 +3339,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3097
3339
|
}
|
|
3098
3340
|
function validateTranslationReferences(stack) {
|
|
3099
3341
|
const findings = [];
|
|
3100
|
-
if (!
|
|
3342
|
+
if (!isRec10(stack)) return findings;
|
|
3101
3343
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3102
3344
|
if (bundles.length === 0) return findings;
|
|
3103
3345
|
const universe = buildUniverse(stack);
|
|
@@ -3106,13 +3348,13 @@ function validateTranslationReferences(stack) {
|
|
|
3106
3348
|
};
|
|
3107
3349
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3108
3350
|
const bundle = bundles[bi];
|
|
3109
|
-
if (!
|
|
3351
|
+
if (!isRec10(bundle)) continue;
|
|
3110
3352
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3111
|
-
if (!
|
|
3353
|
+
if (!isRec10(rawData)) continue;
|
|
3112
3354
|
const base = localePath(bi, locale);
|
|
3113
3355
|
const inLocale = `locale "${locale}"`;
|
|
3114
3356
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3115
|
-
if (!
|
|
3357
|
+
if (!isRec10(rawNode)) continue;
|
|
3116
3358
|
const objPath = `${base}.objects.${objectName}`;
|
|
3117
3359
|
const facts = universe.objects.get(objectName);
|
|
3118
3360
|
if (!facts) {
|
|
@@ -3138,7 +3380,7 @@ function validateTranslationReferences(stack) {
|
|
|
3138
3380
|
);
|
|
3139
3381
|
continue;
|
|
3140
3382
|
}
|
|
3141
|
-
if (!
|
|
3383
|
+
if (!isRec10(rawField)) continue;
|
|
3142
3384
|
checkOptionKeys(findings, {
|
|
3143
3385
|
optionMap: rawField.options,
|
|
3144
3386
|
field,
|
|
@@ -3220,7 +3462,7 @@ function validateTranslationReferences(stack) {
|
|
|
3220
3462
|
);
|
|
3221
3463
|
continue;
|
|
3222
3464
|
}
|
|
3223
|
-
if (!
|
|
3465
|
+
if (!isRec10(rawApp)) continue;
|
|
3224
3466
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3225
3467
|
if (navIds.has(navId)) continue;
|
|
3226
3468
|
orphan(
|
|
@@ -3243,7 +3485,7 @@ function validateTranslationReferences(stack) {
|
|
|
3243
3485
|
);
|
|
3244
3486
|
continue;
|
|
3245
3487
|
}
|
|
3246
|
-
if (!
|
|
3488
|
+
if (!isRec10(rawDash)) continue;
|
|
3247
3489
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3248
3490
|
if (dash.widgets.has(widgetId)) continue;
|
|
3249
3491
|
orphan(
|
|
@@ -3268,7 +3510,7 @@ function validateTranslationReferences(stack) {
|
|
|
3268
3510
|
return findings;
|
|
3269
3511
|
}
|
|
3270
3512
|
function asRecord(v) {
|
|
3271
|
-
return
|
|
3513
|
+
return isRec10(v) ? v : {};
|
|
3272
3514
|
}
|
|
3273
3515
|
function checkOptionKeys(findings, ctx) {
|
|
3274
3516
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3280,7 +3522,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3280
3522
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3281
3523
|
where: ctx.where,
|
|
3282
3524
|
path: ctx.path,
|
|
3283
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3525
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName12(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
3284
3526
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3285
3527
|
});
|
|
3286
3528
|
return;
|
|
@@ -3299,11 +3541,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3299
3541
|
}
|
|
3300
3542
|
}
|
|
3301
3543
|
function checkActionParams(findings, ctx) {
|
|
3302
|
-
const rawParams = Object.keys(asRecord(
|
|
3544
|
+
const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3303
3545
|
if (rawParams.length === 0) return;
|
|
3304
3546
|
const declared = /* @__PURE__ */ new Set();
|
|
3305
|
-
for (const param of
|
|
3306
|
-
const name =
|
|
3547
|
+
for (const param of asArray15(ctx.action.params)) {
|
|
3548
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
3307
3549
|
if (name) declared.add(name);
|
|
3308
3550
|
}
|
|
3309
3551
|
for (const paramName of rawParams) {
|
|
@@ -3320,33 +3562,33 @@ function checkActionParams(findings, ctx) {
|
|
|
3320
3562
|
}
|
|
3321
3563
|
|
|
3322
3564
|
// src/collection-entries.ts
|
|
3323
|
-
function
|
|
3565
|
+
function isRec11(v) {
|
|
3324
3566
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3325
3567
|
}
|
|
3326
3568
|
function collectionEntries(v, base) {
|
|
3327
3569
|
if (Array.isArray(v)) {
|
|
3328
3570
|
const out = [];
|
|
3329
3571
|
for (let i = 0; i < v.length; i++) {
|
|
3330
|
-
if (
|
|
3572
|
+
if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3331
3573
|
}
|
|
3332
3574
|
return out;
|
|
3333
3575
|
}
|
|
3334
|
-
if (
|
|
3335
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3576
|
+
if (isRec11(v)) {
|
|
3577
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3336
3578
|
}
|
|
3337
3579
|
return [];
|
|
3338
3580
|
}
|
|
3339
3581
|
|
|
3340
3582
|
// src/validate-translatable-sections.ts
|
|
3341
3583
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3342
|
-
function
|
|
3584
|
+
function isRec12(v) {
|
|
3343
3585
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3344
3586
|
}
|
|
3345
|
-
function
|
|
3587
|
+
function strName13(v) {
|
|
3346
3588
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3347
3589
|
}
|
|
3348
3590
|
function viewLabel(view) {
|
|
3349
|
-
const name =
|
|
3591
|
+
const name = strName13(view.name);
|
|
3350
3592
|
return name ? `view "${name}"` : "";
|
|
3351
3593
|
}
|
|
3352
3594
|
function joinWhere(...parts) {
|
|
@@ -3354,7 +3596,7 @@ function joinWhere(...parts) {
|
|
|
3354
3596
|
}
|
|
3355
3597
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3356
3598
|
const recordObject = viewObjectName(view);
|
|
3357
|
-
const listBinding =
|
|
3599
|
+
const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3358
3600
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3359
3601
|
sites.push({
|
|
3360
3602
|
path: `${site.path}.sections`,
|
|
@@ -3368,11 +3610,11 @@ function translatedObjectNames(stack) {
|
|
|
3368
3610
|
const out = /* @__PURE__ */ new Set();
|
|
3369
3611
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3370
3612
|
for (const bundle of bundles) {
|
|
3371
|
-
if (!
|
|
3613
|
+
if (!isRec12(bundle)) continue;
|
|
3372
3614
|
for (const data of Object.values(bundle)) {
|
|
3373
|
-
if (!
|
|
3615
|
+
if (!isRec12(data) || !isRec12(data.objects)) continue;
|
|
3374
3616
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3375
|
-
if (
|
|
3617
|
+
if (isRec12(node)) out.add(objectName);
|
|
3376
3618
|
}
|
|
3377
3619
|
}
|
|
3378
3620
|
}
|
|
@@ -3384,22 +3626,22 @@ function suggestedName(label2) {
|
|
|
3384
3626
|
}
|
|
3385
3627
|
function validateTranslatableSections(stack) {
|
|
3386
3628
|
const findings = [];
|
|
3387
|
-
if (!
|
|
3629
|
+
if (!isRec12(stack)) return findings;
|
|
3388
3630
|
const translated = translatedObjectNames(stack);
|
|
3389
3631
|
if (translated.size === 0) return findings;
|
|
3390
3632
|
const sites = [];
|
|
3391
3633
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3392
|
-
const objectName =
|
|
3634
|
+
const objectName = strName13(obj.name);
|
|
3393
3635
|
if (!objectName) continue;
|
|
3394
3636
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3395
3637
|
collectViewSites(
|
|
3396
|
-
{ ...view, object:
|
|
3638
|
+
{ ...view, object: strName13(view.object) ?? objectName },
|
|
3397
3639
|
path,
|
|
3398
3640
|
viewLabel(view),
|
|
3399
3641
|
sites
|
|
3400
3642
|
);
|
|
3401
3643
|
}
|
|
3402
|
-
if (
|
|
3644
|
+
if (isRec12(obj.listViews)) {
|
|
3403
3645
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3404
3646
|
}
|
|
3405
3647
|
}
|
|
@@ -3407,13 +3649,13 @@ function validateTranslatableSections(stack) {
|
|
|
3407
3649
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3408
3650
|
}
|
|
3409
3651
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3410
|
-
const pageName =
|
|
3652
|
+
const pageName = strName13(page.name);
|
|
3411
3653
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3412
3654
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3413
3655
|
if (!walked.objectName) continue;
|
|
3414
|
-
const props =
|
|
3656
|
+
const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
|
|
3415
3657
|
if (!props) continue;
|
|
3416
|
-
const type =
|
|
3658
|
+
const type = strName13(walked.component.type) ?? "component";
|
|
3417
3659
|
sites.push({
|
|
3418
3660
|
path: `${walked.path}.properties.sections`,
|
|
3419
3661
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3428,9 +3670,9 @@ function validateTranslatableSections(stack) {
|
|
|
3428
3670
|
if (!Array.isArray(site.sections)) continue;
|
|
3429
3671
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3430
3672
|
const section = site.sections[i];
|
|
3431
|
-
if (!
|
|
3432
|
-
if (
|
|
3433
|
-
const heading =
|
|
3673
|
+
if (!isRec12(section)) continue;
|
|
3674
|
+
if (strName13(section.name)) continue;
|
|
3675
|
+
const heading = strName13(section.label);
|
|
3434
3676
|
if (!heading) continue;
|
|
3435
3677
|
const slug = suggestedName(heading);
|
|
3436
3678
|
findings.push({
|
|
@@ -3448,10 +3690,10 @@ function validateTranslatableSections(stack) {
|
|
|
3448
3690
|
|
|
3449
3691
|
// src/flow-walk.ts
|
|
3450
3692
|
var import_automation2 = require("@objectstack/spec/automation");
|
|
3451
|
-
function
|
|
3693
|
+
function isRec13(v) {
|
|
3452
3694
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3453
3695
|
}
|
|
3454
|
-
function
|
|
3696
|
+
function strName14(v) {
|
|
3455
3697
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3456
3698
|
}
|
|
3457
3699
|
var REGION_SLOTS = new Map(
|
|
@@ -3460,10 +3702,10 @@ var REGION_SLOTS = new Map(
|
|
|
3460
3702
|
var REGION_CONFIG_KEYS = import_automation2.FLOW_REGION_CONFIG_KEYS;
|
|
3461
3703
|
var MAX_REGION_DEPTH = 16;
|
|
3462
3704
|
function flowNodeLabel(node, index) {
|
|
3463
|
-
return
|
|
3705
|
+
return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
|
|
3464
3706
|
}
|
|
3465
3707
|
function stripRegions(config) {
|
|
3466
|
-
if (!
|
|
3708
|
+
if (!isRec13(config)) return void 0;
|
|
3467
3709
|
let out;
|
|
3468
3710
|
for (const key of Object.keys(config)) {
|
|
3469
3711
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3474,11 +3716,11 @@ function stripRegions(config) {
|
|
|
3474
3716
|
}
|
|
3475
3717
|
function walkFlowNodes(flow, flowPath) {
|
|
3476
3718
|
const out = [];
|
|
3477
|
-
if (!
|
|
3719
|
+
if (!isRec13(flow)) return out;
|
|
3478
3720
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3479
3721
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3480
3722
|
nodes.forEach((raw, index) => {
|
|
3481
|
-
if (!
|
|
3723
|
+
if (!isRec13(raw)) return;
|
|
3482
3724
|
const path = `${basePath}[${index}]`;
|
|
3483
3725
|
out.push({
|
|
3484
3726
|
node: raw,
|
|
@@ -3487,9 +3729,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3487
3729
|
regionTrail: trail,
|
|
3488
3730
|
depth
|
|
3489
3731
|
});
|
|
3490
|
-
const type =
|
|
3732
|
+
const type = strName14(raw.type);
|
|
3491
3733
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3492
|
-
if (!slots || !
|
|
3734
|
+
if (!slots || !isRec13(raw.config)) return;
|
|
3493
3735
|
const config = raw.config;
|
|
3494
3736
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3495
3737
|
for (const slot of slots) {
|
|
@@ -3497,8 +3739,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3497
3739
|
if (slot === "branches") {
|
|
3498
3740
|
if (!Array.isArray(value)) continue;
|
|
3499
3741
|
value.forEach((branch, b) => {
|
|
3500
|
-
if (!
|
|
3501
|
-
const branchName =
|
|
3742
|
+
if (!isRec13(branch)) return;
|
|
3743
|
+
const branchName = strName14(branch.name) ?? `#${b}`;
|
|
3502
3744
|
visitList(
|
|
3503
3745
|
branch.nodes,
|
|
3504
3746
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3508,7 +3750,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3508
3750
|
});
|
|
3509
3751
|
continue;
|
|
3510
3752
|
}
|
|
3511
|
-
if (!
|
|
3753
|
+
if (!isRec13(value)) continue;
|
|
3512
3754
|
visitList(
|
|
3513
3755
|
value.nodes,
|
|
3514
3756
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3528,7 +3770,8 @@ function joinTrail(trail, segment) {
|
|
|
3528
3770
|
// src/validate-flow-template-paths.ts
|
|
3529
3771
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
3530
3772
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
3531
|
-
|
|
3773
|
+
var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
|
|
3774
|
+
function asArray16(v) {
|
|
3532
3775
|
if (Array.isArray(v)) return v;
|
|
3533
3776
|
if (v && typeof v === "object") {
|
|
3534
3777
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -3557,7 +3800,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3557
3800
|
]);
|
|
3558
3801
|
function fieldTypesOf(obj) {
|
|
3559
3802
|
const types = /* @__PURE__ */ new Map();
|
|
3560
|
-
for (const f of
|
|
3803
|
+
for (const f of asArray16(obj.fields)) {
|
|
3561
3804
|
if (typeof f.name === "string") {
|
|
3562
3805
|
types.set(f.name, typeof f.type === "string" ? f.type : "");
|
|
3563
3806
|
}
|
|
@@ -3654,10 +3897,10 @@ function declaredExpandOf(flow) {
|
|
|
3654
3897
|
}
|
|
3655
3898
|
function validateFlowTemplatePaths(stack) {
|
|
3656
3899
|
const findings = [];
|
|
3657
|
-
const flows =
|
|
3900
|
+
const flows = asArray16(stack.flows);
|
|
3658
3901
|
if (flows.length === 0) return findings;
|
|
3659
3902
|
const objectsByName = /* @__PURE__ */ new Map();
|
|
3660
|
-
for (const obj of
|
|
3903
|
+
for (const obj of asArray16(stack.objects)) {
|
|
3661
3904
|
if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
|
|
3662
3905
|
}
|
|
3663
3906
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -3670,6 +3913,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3670
3913
|
const obj = objectsByName.get(objectName);
|
|
3671
3914
|
if (!obj) return;
|
|
3672
3915
|
const fieldTypes = fieldTypesOf(obj);
|
|
3916
|
+
const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
|
|
3673
3917
|
const expandSet = declaredExpandOf(flow);
|
|
3674
3918
|
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
3675
3919
|
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
@@ -3681,6 +3925,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3681
3925
|
if (leaves.length === 0) return;
|
|
3682
3926
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
3683
3927
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
3928
|
+
const seenUnprovisioned = /* @__PURE__ */ new Set();
|
|
3684
3929
|
for (const leaf of leaves) {
|
|
3685
3930
|
const inFilter = leaf.inFilter;
|
|
3686
3931
|
for (const rest of recordRefsIn(leaf.text)) {
|
|
@@ -3688,6 +3933,19 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3688
3933
|
const hasSubPath = rest.length > 1;
|
|
3689
3934
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
3690
3935
|
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
3936
|
+
if (unprovisionedAnchors.has(head)) {
|
|
3937
|
+
if (!seenUnprovisioned.has(head)) {
|
|
3938
|
+
seenUnprovisioned.add(head);
|
|
3939
|
+
findings.push({
|
|
3940
|
+
severity: "warning",
|
|
3941
|
+
rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
3942
|
+
where,
|
|
3943
|
+
path: nodePath,
|
|
3944
|
+
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).`),
|
|
3945
|
+
hint: unprovisionedAnchorHint(objectName, head)
|
|
3946
|
+
});
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3691
3949
|
if (!isKnown) {
|
|
3692
3950
|
if (seenUnknown.has(head)) continue;
|
|
3693
3951
|
seenUnknown.add(head);
|
|
@@ -3726,14 +3984,14 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3726
3984
|
|
|
3727
3985
|
// src/validate-ai-surface-affinity.ts
|
|
3728
3986
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
3729
|
-
function
|
|
3987
|
+
function asArray17(v) {
|
|
3730
3988
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3731
3989
|
if (v && typeof v === "object") {
|
|
3732
3990
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3733
3991
|
}
|
|
3734
3992
|
return [];
|
|
3735
3993
|
}
|
|
3736
|
-
function
|
|
3994
|
+
function strName15(v) {
|
|
3737
3995
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3738
3996
|
}
|
|
3739
3997
|
function surfaceOf(v) {
|
|
@@ -3743,18 +4001,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3743
4001
|
const findings = [];
|
|
3744
4002
|
if (!stack || typeof stack !== "object") return findings;
|
|
3745
4003
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3746
|
-
for (const skill of
|
|
3747
|
-
const n =
|
|
4004
|
+
for (const skill of asArray17(stack.skills)) {
|
|
4005
|
+
const n = strName15(skill.name);
|
|
3748
4006
|
if (n) skillsByName.set(n, skill);
|
|
3749
4007
|
}
|
|
3750
|
-
const agents =
|
|
4008
|
+
const agents = asArray17(stack.agents);
|
|
3751
4009
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3752
4010
|
const agent = agents[ai];
|
|
3753
|
-
const agentName =
|
|
4011
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
3754
4012
|
const agentSurface = surfaceOf(agent.surface);
|
|
3755
4013
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3756
4014
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3757
|
-
const ref =
|
|
4015
|
+
const ref = strName15(skillRefs[si]);
|
|
3758
4016
|
if (!ref) continue;
|
|
3759
4017
|
const skill = skillsByName.get(ref);
|
|
3760
4018
|
if (!skill) continue;
|
|
@@ -3776,14 +4034,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3776
4034
|
// src/validate-ai-tool-references.ts
|
|
3777
4035
|
var import_system5 = require("@objectstack/spec/system");
|
|
3778
4036
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
3779
|
-
function
|
|
4037
|
+
function asArray18(v) {
|
|
3780
4038
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3781
4039
|
if (v && typeof v === "object") {
|
|
3782
4040
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3783
4041
|
}
|
|
3784
4042
|
return [];
|
|
3785
4043
|
}
|
|
3786
|
-
function
|
|
4044
|
+
function strName16(v) {
|
|
3787
4045
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3788
4046
|
}
|
|
3789
4047
|
function distance6(a, b) {
|
|
@@ -3824,26 +4082,26 @@ function materialisesAsTool(action) {
|
|
|
3824
4082
|
if (!ai || typeof ai !== "object") return false;
|
|
3825
4083
|
const aiRec = ai;
|
|
3826
4084
|
if (aiRec.exposed !== true) return false;
|
|
3827
|
-
if (!
|
|
3828
|
-
const type =
|
|
4085
|
+
if (!strName16(aiRec.description)) return false;
|
|
4086
|
+
const type = strName16(action.type);
|
|
3829
4087
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3830
4088
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3831
4089
|
return Boolean(action.target);
|
|
3832
4090
|
}
|
|
3833
4091
|
function collectToolUniverse(stack) {
|
|
3834
4092
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3835
|
-
for (const tool of
|
|
3836
|
-
const n =
|
|
4093
|
+
for (const tool of asArray18(stack.tools)) {
|
|
4094
|
+
const n = strName16(tool.name);
|
|
3837
4095
|
if (n) universe.add(n);
|
|
3838
4096
|
}
|
|
3839
4097
|
const addActionFamily = (actions) => {
|
|
3840
|
-
for (const action of
|
|
3841
|
-
const n =
|
|
4098
|
+
for (const action of asArray18(actions)) {
|
|
4099
|
+
const n = strName16(action.name);
|
|
3842
4100
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3843
4101
|
}
|
|
3844
4102
|
};
|
|
3845
4103
|
addActionFamily(stack.actions);
|
|
3846
|
-
for (const obj of
|
|
4104
|
+
for (const obj of asArray18(stack.objects)) {
|
|
3847
4105
|
addActionFamily(obj.actions);
|
|
3848
4106
|
}
|
|
3849
4107
|
return universe;
|
|
@@ -3851,13 +4109,13 @@ function collectToolUniverse(stack) {
|
|
|
3851
4109
|
function collectUnexposedActionNames(stack) {
|
|
3852
4110
|
const names = /* @__PURE__ */ new Set();
|
|
3853
4111
|
const scan = (actions) => {
|
|
3854
|
-
for (const action of
|
|
3855
|
-
const n =
|
|
4112
|
+
for (const action of asArray18(actions)) {
|
|
4113
|
+
const n = strName16(action.name);
|
|
3856
4114
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3857
4115
|
}
|
|
3858
4116
|
};
|
|
3859
4117
|
scan(stack.actions);
|
|
3860
|
-
for (const obj of
|
|
4118
|
+
for (const obj of asArray18(stack.objects)) scan(obj.actions);
|
|
3861
4119
|
return names;
|
|
3862
4120
|
}
|
|
3863
4121
|
function validateAiToolReferences(stack) {
|
|
@@ -3875,13 +4133,13 @@ function validateAiToolReferences(stack) {
|
|
|
3875
4133
|
}
|
|
3876
4134
|
return universe.has(ref);
|
|
3877
4135
|
};
|
|
3878
|
-
const skills =
|
|
4136
|
+
const skills = asArray18(stack.skills);
|
|
3879
4137
|
for (let si = 0; si < skills.length; si++) {
|
|
3880
4138
|
const skill = skills[si];
|
|
3881
|
-
const skillName =
|
|
4139
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
3882
4140
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3883
4141
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3884
|
-
const ref =
|
|
4142
|
+
const ref = strName16(refs[ti]);
|
|
3885
4143
|
if (!ref || resolves(ref)) continue;
|
|
3886
4144
|
const isPattern = ref.endsWith("*");
|
|
3887
4145
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3901,24 +4159,24 @@ function validateAiToolReferences(stack) {
|
|
|
3901
4159
|
// src/validate-ai-agent-authoring.ts
|
|
3902
4160
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3903
4161
|
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
3904
|
-
function
|
|
4162
|
+
function asArray19(v) {
|
|
3905
4163
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3906
4164
|
if (v && typeof v === "object") {
|
|
3907
4165
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3908
4166
|
}
|
|
3909
4167
|
return [];
|
|
3910
4168
|
}
|
|
3911
|
-
function
|
|
4169
|
+
function strName17(v) {
|
|
3912
4170
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3913
4171
|
}
|
|
3914
4172
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
3915
4173
|
function validateAiAgentAuthoring(stack) {
|
|
3916
4174
|
const findings = [];
|
|
3917
4175
|
if (!stack || typeof stack !== "object") return findings;
|
|
3918
|
-
const agents =
|
|
4176
|
+
const agents = asArray19(stack.agents);
|
|
3919
4177
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3920
4178
|
const agent = agents[ai];
|
|
3921
|
-
const name =
|
|
4179
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
3922
4180
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3923
4181
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3924
4182
|
findings.push({
|
|
@@ -3931,12 +4189,12 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3931
4189
|
});
|
|
3932
4190
|
}
|
|
3933
4191
|
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
3934
|
-
const apps =
|
|
4192
|
+
const apps = asArray19(stack.apps);
|
|
3935
4193
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
3936
4194
|
const app = apps[appIdx];
|
|
3937
|
-
const defaultAgent =
|
|
4195
|
+
const defaultAgent = strName17(app.defaultAgent);
|
|
3938
4196
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
3939
|
-
const appName =
|
|
4197
|
+
const appName = strName17(app.name) ?? `#${appIdx}`;
|
|
3940
4198
|
findings.push({
|
|
3941
4199
|
severity: "warning",
|
|
3942
4200
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4033,24 +4291,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4033
4291
|
"owner",
|
|
4034
4292
|
"record_type"
|
|
4035
4293
|
]);
|
|
4036
|
-
var
|
|
4037
|
-
function
|
|
4038
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4039
|
-
if (
|
|
4294
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4295
|
+
function asArray20(v) {
|
|
4296
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4297
|
+
if (isRec14(v)) {
|
|
4040
4298
|
return Object.entries(v).map(([name, def]) => ({
|
|
4041
4299
|
name,
|
|
4042
|
-
...
|
|
4300
|
+
...isRec14(def) ? def : {}
|
|
4043
4301
|
}));
|
|
4044
4302
|
}
|
|
4045
4303
|
return [];
|
|
4046
4304
|
}
|
|
4047
4305
|
function indexObjectFields2(stack) {
|
|
4048
4306
|
const out = /* @__PURE__ */ new Map();
|
|
4049
|
-
for (const obj of
|
|
4307
|
+
for (const obj of asArray20(stack.objects)) {
|
|
4050
4308
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
4051
4309
|
if (!name) continue;
|
|
4052
4310
|
const names = /* @__PURE__ */ new Set();
|
|
4053
|
-
for (const f of
|
|
4311
|
+
for (const f of asArray20(obj.fields)) {
|
|
4054
4312
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
4055
4313
|
}
|
|
4056
4314
|
out.set(name, names);
|
|
@@ -4180,12 +4438,12 @@ ${source}
|
|
|
4180
4438
|
}
|
|
4181
4439
|
function validateHookBodyWrites(stack) {
|
|
4182
4440
|
const findings = [];
|
|
4183
|
-
const hooks =
|
|
4441
|
+
const hooks = asArray20(stack.hooks);
|
|
4184
4442
|
if (hooks.length === 0) return findings;
|
|
4185
4443
|
let objectFields = null;
|
|
4186
4444
|
hooks.forEach((hook, hookIndex) => {
|
|
4187
4445
|
const body = hook.body;
|
|
4188
|
-
if (!
|
|
4446
|
+
if (!isRec14(body) || body.language !== "js") return;
|
|
4189
4447
|
const source = body.source;
|
|
4190
4448
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4191
4449
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -4256,13 +4514,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
4256
4514
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4257
4515
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4258
4516
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4259
|
-
var
|
|
4260
|
-
function
|
|
4261
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4262
|
-
if (
|
|
4517
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4518
|
+
function asArray21(v) {
|
|
4519
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4520
|
+
if (isRec15(v)) {
|
|
4263
4521
|
return Object.entries(v).map(([name, def]) => ({
|
|
4264
4522
|
name,
|
|
4265
|
-
...
|
|
4523
|
+
...isRec15(def) ? def : {}
|
|
4266
4524
|
}));
|
|
4267
4525
|
}
|
|
4268
4526
|
return [];
|
|
@@ -4276,11 +4534,11 @@ function collectActionBodies(stack) {
|
|
|
4276
4534
|
const sites = [];
|
|
4277
4535
|
const seen = /* @__PURE__ */ new Set();
|
|
4278
4536
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
4279
|
-
|
|
4537
|
+
asArray21(actions).forEach((action, index) => {
|
|
4280
4538
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4281
4539
|
if (type !== "script") return;
|
|
4282
4540
|
const body = action.body;
|
|
4283
|
-
if (!
|
|
4541
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4284
4542
|
const source = body.source;
|
|
4285
4543
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4286
4544
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4291,7 +4549,7 @@ function collectActionBodies(stack) {
|
|
|
4291
4549
|
});
|
|
4292
4550
|
};
|
|
4293
4551
|
collect(stack.actions, "actions");
|
|
4294
|
-
|
|
4552
|
+
asArray21(stack.objects).forEach((obj, objIndex) => {
|
|
4295
4553
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
4296
4554
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
4297
4555
|
});
|
|
@@ -4299,7 +4557,7 @@ function collectActionBodies(stack) {
|
|
|
4299
4557
|
}
|
|
4300
4558
|
function validateActionBodyWrites(stack) {
|
|
4301
4559
|
const findings = [];
|
|
4302
|
-
if (!
|
|
4560
|
+
if (!isRec15(stack)) return findings;
|
|
4303
4561
|
const sites = collectActionBodies(stack);
|
|
4304
4562
|
if (sites.length === 0) return findings;
|
|
4305
4563
|
let objectFields = null;
|
|
@@ -4357,13 +4615,13 @@ function fixHint2(field, declared) {
|
|
|
4357
4615
|
var import_shared3 = require("@objectstack/spec/shared");
|
|
4358
4616
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4359
4617
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4360
|
-
var
|
|
4361
|
-
function
|
|
4362
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4363
|
-
if (
|
|
4618
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4619
|
+
function asArray22(v) {
|
|
4620
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4621
|
+
if (isRec16(v)) {
|
|
4364
4622
|
return Object.entries(v).map(([name, def]) => ({
|
|
4365
4623
|
name,
|
|
4366
|
-
...
|
|
4624
|
+
...isRec16(def) ? def : {}
|
|
4367
4625
|
}));
|
|
4368
4626
|
}
|
|
4369
4627
|
return [];
|
|
@@ -4376,8 +4634,8 @@ function readLiteralObjectName(config) {
|
|
|
4376
4634
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4377
4635
|
function validateFlowNodeWrites(stack) {
|
|
4378
4636
|
const findings = [];
|
|
4379
|
-
if (!
|
|
4380
|
-
const flows =
|
|
4637
|
+
if (!isRec16(stack)) return findings;
|
|
4638
|
+
const flows = asArray22(stack.flows);
|
|
4381
4639
|
if (flows.length === 0) return findings;
|
|
4382
4640
|
let objectFields = null;
|
|
4383
4641
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -4385,10 +4643,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4385
4643
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4386
4644
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4387
4645
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4388
|
-
const config =
|
|
4646
|
+
const config = isRec16(node.config) ? node.config : void 0;
|
|
4389
4647
|
if (!config) return;
|
|
4390
4648
|
const fields = config.fields;
|
|
4391
|
-
if (!
|
|
4649
|
+
if (!isRec16(fields)) return;
|
|
4392
4650
|
const written = Object.keys(fields);
|
|
4393
4651
|
if (written.length === 0) return;
|
|
4394
4652
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4422,7 +4680,7 @@ function fixHint3(field, declared) {
|
|
|
4422
4680
|
// src/validate-readonly-flow-writes.ts
|
|
4423
4681
|
var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
|
|
4424
4682
|
var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
|
|
4425
|
-
function
|
|
4683
|
+
function asArray23(v) {
|
|
4426
4684
|
if (Array.isArray(v)) return v;
|
|
4427
4685
|
if (v && typeof v === "object") {
|
|
4428
4686
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -4463,9 +4721,9 @@ function readLiteralObjectName2(config) {
|
|
|
4463
4721
|
}
|
|
4464
4722
|
function validateReadonlyFlowWrites(stack) {
|
|
4465
4723
|
const findings = [];
|
|
4466
|
-
const flows =
|
|
4724
|
+
const flows = asArray23(stack.flows);
|
|
4467
4725
|
if (flows.length === 0) return findings;
|
|
4468
|
-
const roIndex = buildReadonlyIndex(
|
|
4726
|
+
const roIndex = buildReadonlyIndex(asArray23(stack.objects));
|
|
4469
4727
|
flows.forEach((flow, flowIndex) => {
|
|
4470
4728
|
if (flow.runAs === "system") return;
|
|
4471
4729
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
@@ -4513,14 +4771,14 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
4513
4771
|
// src/validate-react-page-props.ts
|
|
4514
4772
|
var import_node_module2 = require("module");
|
|
4515
4773
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
4516
|
-
var
|
|
4774
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
4517
4775
|
|
|
4518
4776
|
// src/zod-issue-format.ts
|
|
4519
|
-
var
|
|
4777
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4520
4778
|
var valueAtPath = (root, path) => {
|
|
4521
4779
|
let cur = root;
|
|
4522
4780
|
for (const key of path) {
|
|
4523
|
-
if (!
|
|
4781
|
+
if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
|
|
4524
4782
|
cur = cur[key];
|
|
4525
4783
|
}
|
|
4526
4784
|
return cur;
|
|
@@ -4566,7 +4824,7 @@ function loadTypeScript2() {
|
|
|
4566
4824
|
}
|
|
4567
4825
|
return cachedTs2;
|
|
4568
4826
|
}
|
|
4569
|
-
var
|
|
4827
|
+
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4570
4828
|
var BLOCKS = new Map(
|
|
4571
4829
|
import_ui2.REACT_BLOCKS.map((b) => [
|
|
4572
4830
|
b.tag,
|
|
@@ -4654,12 +4912,13 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4654
4912
|
return perPosition(init.expression);
|
|
4655
4913
|
}
|
|
4656
4914
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4915
|
+
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4657
4916
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
4658
4917
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
4659
4918
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4660
4919
|
function checkChartDrillDown(raw, push2) {
|
|
4661
4920
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4662
|
-
if (!
|
|
4921
|
+
if (!isRec18(raw)) {
|
|
4663
4922
|
push2(
|
|
4664
4923
|
"error",
|
|
4665
4924
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4682,7 +4941,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4682
4941
|
}
|
|
4683
4942
|
function checkChartAggregate(raw, push2) {
|
|
4684
4943
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4685
|
-
if (!
|
|
4944
|
+
if (!isRec18(raw)) {
|
|
4686
4945
|
push2(
|
|
4687
4946
|
"error",
|
|
4688
4947
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4713,9 +4972,9 @@ function checkChartAggregate(raw, push2) {
|
|
|
4713
4972
|
);
|
|
4714
4973
|
}
|
|
4715
4974
|
}
|
|
4716
|
-
var
|
|
4975
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4717
4976
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4718
|
-
function checkObjectChart(attrs, objectFields, findings) {
|
|
4977
|
+
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4719
4978
|
const { values, where, path } = attrs;
|
|
4720
4979
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
4721
4980
|
checkChartDrillDown(values.get("drillDown"), push2);
|
|
@@ -4723,18 +4982,29 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4723
4982
|
const aggregate = values.get("aggregate");
|
|
4724
4983
|
checkChartAggregate(aggregate, push2);
|
|
4725
4984
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4726
|
-
if (!
|
|
4985
|
+
if (!isRec18(aggregate)) return;
|
|
4727
4986
|
const fn = strOf(aggregate.function);
|
|
4728
4987
|
const field = strOf(aggregate.field);
|
|
4729
4988
|
const groupBy = aggregate.groupBy;
|
|
4730
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4989
|
+
const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4731
4990
|
const objectName = strOf(values.get("objectName"));
|
|
4732
4991
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4733
4992
|
if (objectName && known) {
|
|
4993
|
+
const anchors = unprovisionedAnchors.get(objectName);
|
|
4734
4994
|
const fieldRef = (name, prop) => {
|
|
4735
4995
|
if (!name) return;
|
|
4736
4996
|
if (name.includes(".")) return;
|
|
4737
|
-
if (known.has(name) || SYSTEM_FIELDS.has(name))
|
|
4997
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) {
|
|
4998
|
+
if (anchors?.has(name)) {
|
|
4999
|
+
push2(
|
|
5000
|
+
"warning",
|
|
5001
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
5002
|
+
`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.`,
|
|
5003
|
+
unprovisionedAnchorHint(objectName, name)
|
|
5004
|
+
);
|
|
5005
|
+
}
|
|
5006
|
+
return;
|
|
5007
|
+
}
|
|
4738
5008
|
push2(
|
|
4739
5009
|
"error",
|
|
4740
5010
|
REACT_CHART_FIELD_UNKNOWN,
|
|
@@ -4760,18 +5030,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4760
5030
|
);
|
|
4761
5031
|
};
|
|
4762
5032
|
const xAxisRaw = values.get("xAxis");
|
|
4763
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5033
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4764
5034
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4765
5035
|
axisRef(categoryAxis, categoryProp);
|
|
4766
5036
|
const yAxisRaw = values.get("yAxis");
|
|
4767
5037
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4768
5038
|
for (const a of yAxisList) {
|
|
4769
|
-
axisRef(strOf(a) ?? (
|
|
5039
|
+
axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4770
5040
|
}
|
|
4771
5041
|
const series = values.get("series");
|
|
4772
5042
|
if (Array.isArray(series)) {
|
|
4773
5043
|
for (const s of series) {
|
|
4774
|
-
if (!
|
|
5044
|
+
if (!isRec18(s)) continue;
|
|
4775
5045
|
const dataKey = strOf(s.dataKey);
|
|
4776
5046
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4777
5047
|
}
|
|
@@ -4827,7 +5097,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4827
5097
|
if (!Array.isArray(value)) return { child, parent };
|
|
4828
5098
|
for (let i = 0; i < value.length; i++) {
|
|
4829
5099
|
const sub = value[i];
|
|
4830
|
-
if (!
|
|
5100
|
+
if (!isRec18(sub)) continue;
|
|
4831
5101
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4832
5102
|
child.push({
|
|
4833
5103
|
objectName: strOf(sub.childObject),
|
|
@@ -4852,7 +5122,7 @@ function filterFieldRefs(node, basePath, out) {
|
|
|
4852
5122
|
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
4853
5123
|
return;
|
|
4854
5124
|
}
|
|
4855
|
-
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" &&
|
|
5125
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && import_data8.VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
4856
5126
|
out.push({ name: head, path: `${basePath}[0]` });
|
|
4857
5127
|
}
|
|
4858
5128
|
}
|
|
@@ -4872,20 +5142,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4872
5142
|
}
|
|
4873
5143
|
for (const key of spec.nestedFields ?? []) {
|
|
4874
5144
|
const v = readable(key);
|
|
4875
|
-
if (
|
|
5145
|
+
if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4876
5146
|
}
|
|
4877
5147
|
for (const key of spec.sections ?? []) {
|
|
4878
5148
|
const v = readable(key);
|
|
4879
5149
|
if (!Array.isArray(v)) continue;
|
|
4880
5150
|
for (let i = 0; i < v.length; i++) {
|
|
4881
5151
|
const section = v[i];
|
|
4882
|
-
if (!
|
|
5152
|
+
if (!isRec18(section)) continue;
|
|
4883
5153
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4884
5154
|
}
|
|
4885
5155
|
}
|
|
4886
5156
|
for (const key of spec.keyedByField ?? []) {
|
|
4887
5157
|
const v = readable(key);
|
|
4888
|
-
if (!
|
|
5158
|
+
if (!isRec18(v)) continue;
|
|
4889
5159
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4890
5160
|
}
|
|
4891
5161
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4893,22 +5163,30 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4893
5163
|
}
|
|
4894
5164
|
return { own, queried };
|
|
4895
5165
|
}
|
|
4896
|
-
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
5166
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
|
|
4897
5167
|
const objectName = strOf(values.get("objectName"));
|
|
4898
5168
|
const out = [];
|
|
4899
5169
|
const spec = REACT_FIELD_SPECS[tag];
|
|
4900
5170
|
if (spec) {
|
|
4901
5171
|
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
4902
|
-
out.push(
|
|
4903
|
-
|
|
5172
|
+
out.push(
|
|
5173
|
+
...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5174
|
+
);
|
|
5175
|
+
out.push(
|
|
5176
|
+
...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
|
|
5177
|
+
);
|
|
4904
5178
|
}
|
|
4905
5179
|
if (tag === "ObjectForm") {
|
|
4906
5180
|
const raw = values.get("subforms");
|
|
4907
5181
|
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
4908
5182
|
for (const sub of subs.child) {
|
|
4909
|
-
out.push(
|
|
5183
|
+
out.push(
|
|
5184
|
+
...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5185
|
+
);
|
|
4910
5186
|
}
|
|
4911
|
-
out.push(
|
|
5187
|
+
out.push(
|
|
5188
|
+
...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5189
|
+
);
|
|
4912
5190
|
}
|
|
4913
5191
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
4914
5192
|
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
@@ -4917,7 +5195,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
4917
5195
|
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
4918
5196
|
objectName,
|
|
4919
5197
|
objectFields,
|
|
4920
|
-
where
|
|
5198
|
+
where,
|
|
5199
|
+
"skipped",
|
|
5200
|
+
unprovisionedAnchors
|
|
4921
5201
|
)
|
|
4922
5202
|
);
|
|
4923
5203
|
}
|
|
@@ -4949,8 +5229,9 @@ function localComponentNames(tsc, sf) {
|
|
|
4949
5229
|
function validateReactPageProps(stack) {
|
|
4950
5230
|
const findings = [];
|
|
4951
5231
|
const objectFields = indexObjectFields(stack);
|
|
5232
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
4952
5233
|
const searchTargets = indexObjectSearchTargets(stack);
|
|
4953
|
-
const pages =
|
|
5234
|
+
const pages = asArray24(stack.pages);
|
|
4954
5235
|
for (let p = 0; p < pages.length; p++) {
|
|
4955
5236
|
const page = pages[p];
|
|
4956
5237
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5031,7 +5312,7 @@ function validateReactPageProps(stack) {
|
|
|
5031
5312
|
}
|
|
5032
5313
|
}
|
|
5033
5314
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
5034
|
-
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
5315
|
+
checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
|
|
5035
5316
|
}
|
|
5036
5317
|
if (tag === "ListView" && !hasSpread) {
|
|
5037
5318
|
findings.push(
|
|
@@ -5047,7 +5328,7 @@ function validateReactPageProps(stack) {
|
|
|
5047
5328
|
}
|
|
5048
5329
|
if (!hasSpread) {
|
|
5049
5330
|
findings.push(
|
|
5050
|
-
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
5331
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
|
|
5051
5332
|
);
|
|
5052
5333
|
}
|
|
5053
5334
|
}
|
|
@@ -5074,6 +5355,15 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5074
5355
|
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
5075
5356
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
5076
5357
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5358
|
+
// [#7912] The THIRD question about a nav entry, after "does the target
|
|
5359
|
+
// resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
|
|
5360
|
+
// destination serve at all? An object's own `enable` block can make its list
|
|
5361
|
+
// answer 404/405 for every persona, and no gate authorable on the entry
|
|
5362
|
+
// expresses that — which is how #7544's dead row survived review for a year.
|
|
5363
|
+
// The server now prunes such an entry from the `/meta` payload; the
|
|
5364
|
+
// maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
|
|
5365
|
+
// prune is never silent to the author who wrote the row.
|
|
5366
|
+
{ name: "validateNavObjectServability", run: validateNavObjectServability },
|
|
5077
5367
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
5078
5368
|
// The same family from the other end (#5417). Its sibling above asks "does
|
|
5079
5369
|
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
@@ -5172,13 +5462,13 @@ var import_ui3 = require("@objectstack/spec/ui");
|
|
|
5172
5462
|
var import_spec2 = require("@objectstack/spec");
|
|
5173
5463
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5174
5464
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5175
|
-
function
|
|
5465
|
+
function isRec19(v) {
|
|
5176
5466
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5177
5467
|
}
|
|
5178
|
-
function
|
|
5468
|
+
function strName18(v) {
|
|
5179
5469
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5180
5470
|
}
|
|
5181
|
-
function
|
|
5471
|
+
function asArray25(v) {
|
|
5182
5472
|
if (Array.isArray(v)) return v;
|
|
5183
5473
|
if (v && typeof v === "object") {
|
|
5184
5474
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5189,23 +5479,36 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
|
5189
5479
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5190
5480
|
function suppliedByDataSource(issue, component) {
|
|
5191
5481
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5192
|
-
const dataSource =
|
|
5193
|
-
return
|
|
5482
|
+
const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
|
|
5483
|
+
return strName18(dataSource?.object) !== void 0;
|
|
5484
|
+
}
|
|
5485
|
+
function unrecognizedKeysFromUnionArm(issue) {
|
|
5486
|
+
if (issue.code !== "invalid_union") return void 0;
|
|
5487
|
+
const arms = issue.errors;
|
|
5488
|
+
if (!arms || arms.length === 0) return void 0;
|
|
5489
|
+
let found;
|
|
5490
|
+
for (const arm of arms) {
|
|
5491
|
+
const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
|
|
5492
|
+
if (keyIssues.length === 0) continue;
|
|
5493
|
+
if (keyIssues.length !== arm.length || found) return void 0;
|
|
5494
|
+
found = keyIssues[0];
|
|
5495
|
+
}
|
|
5496
|
+
return found;
|
|
5194
5497
|
}
|
|
5195
5498
|
function validateComponentProps(stack) {
|
|
5196
5499
|
const findings = [];
|
|
5197
|
-
if (!
|
|
5198
|
-
const pages =
|
|
5500
|
+
if (!isRec19(stack)) return findings;
|
|
5501
|
+
const pages = asArray25(stack.pages);
|
|
5199
5502
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5200
5503
|
const page = pages[pi];
|
|
5201
|
-
if (!
|
|
5202
|
-
const pageName =
|
|
5504
|
+
if (!isRec19(page)) continue;
|
|
5505
|
+
const pageName = strName18(page.name) ?? `#${pi}`;
|
|
5203
5506
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5204
|
-
const type =
|
|
5507
|
+
const type = strName18(component.type);
|
|
5205
5508
|
if (!type) continue;
|
|
5206
5509
|
const schema = PROPS_SCHEMAS[type];
|
|
5207
5510
|
if (!schema) continue;
|
|
5208
|
-
const props =
|
|
5511
|
+
const props = isRec19(component.properties) ? component.properties : void 0;
|
|
5209
5512
|
if (!props) continue;
|
|
5210
5513
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5211
5514
|
const base = `${path}.properties`;
|
|
@@ -5224,6 +5527,20 @@ function validateComponentProps(stack) {
|
|
|
5224
5527
|
for (const issue of parsed.error?.issues ?? []) {
|
|
5225
5528
|
if (suppliedByDataSource(issue, component)) continue;
|
|
5226
5529
|
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5530
|
+
const armIssue = unrecognizedKeysFromUnionArm(issue);
|
|
5531
|
+
if (armIssue) {
|
|
5532
|
+
for (const key of armIssue.keys ?? []) {
|
|
5533
|
+
findings.push({
|
|
5534
|
+
severity: "warning",
|
|
5535
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5536
|
+
where,
|
|
5537
|
+
path: `${at}.${key}`,
|
|
5538
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
|
|
5539
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5540
|
+
});
|
|
5541
|
+
}
|
|
5542
|
+
continue;
|
|
5543
|
+
}
|
|
5227
5544
|
if (issue.code === "unrecognized_keys") {
|
|
5228
5545
|
for (const key of issue.keys ?? []) {
|
|
5229
5546
|
findings.push({
|
|
@@ -5454,7 +5771,7 @@ function looksLikeTailwind(className) {
|
|
|
5454
5771
|
return false;
|
|
5455
5772
|
});
|
|
5456
5773
|
}
|
|
5457
|
-
function
|
|
5774
|
+
function asArray26(v) {
|
|
5458
5775
|
if (Array.isArray(v)) return v;
|
|
5459
5776
|
if (v && typeof v === "object") {
|
|
5460
5777
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5547,13 +5864,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
5547
5864
|
}
|
|
5548
5865
|
function validateResponsiveStyles(stack) {
|
|
5549
5866
|
const findings = [];
|
|
5550
|
-
const pages =
|
|
5867
|
+
const pages = asArray26(stack.pages);
|
|
5551
5868
|
for (let p = 0; p < pages.length; p++) {
|
|
5552
5869
|
const page = pages[p];
|
|
5553
5870
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
5554
|
-
const regions =
|
|
5871
|
+
const regions = asArray26(page.regions);
|
|
5555
5872
|
for (let r = 0; r < regions.length; r++) {
|
|
5556
|
-
const components =
|
|
5873
|
+
const components = asArray26(regions[r].components);
|
|
5557
5874
|
for (let c = 0; c < components.length; c++) {
|
|
5558
5875
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
5559
5876
|
}
|
|
@@ -5564,10 +5881,10 @@ function validateResponsiveStyles(stack) {
|
|
|
5564
5881
|
|
|
5565
5882
|
// src/validate-jsx-pages.ts
|
|
5566
5883
|
var import_sdui_parser = require("@objectstack/sdui-parser");
|
|
5567
|
-
var
|
|
5884
|
+
var asArray27 = (v) => Array.isArray(v) ? v : [];
|
|
5568
5885
|
function validateJsxPages(stack, opts = {}) {
|
|
5569
5886
|
const findings = [];
|
|
5570
|
-
const pages =
|
|
5887
|
+
const pages = asArray27(stack.pages);
|
|
5571
5888
|
for (let p = 0; p < pages.length; p++) {
|
|
5572
5889
|
const page = pages[p];
|
|
5573
5890
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -5615,10 +5932,10 @@ function loadSucraseTransform() {
|
|
|
5615
5932
|
}
|
|
5616
5933
|
return cachedTransform;
|
|
5617
5934
|
}
|
|
5618
|
-
var
|
|
5935
|
+
var asArray28 = (v) => Array.isArray(v) ? v : [];
|
|
5619
5936
|
function validateReactPages(stack) {
|
|
5620
5937
|
const findings = [];
|
|
5621
|
-
const pages =
|
|
5938
|
+
const pages = asArray28(stack.pages);
|
|
5622
5939
|
for (let p = 0; p < pages.length; p++) {
|
|
5623
5940
|
const page = pages[p];
|
|
5624
5941
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5655,11 +5972,11 @@ function validateReactPages(stack) {
|
|
|
5655
5972
|
|
|
5656
5973
|
// src/validate-page-source-styling.ts
|
|
5657
5974
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
5658
|
-
var
|
|
5975
|
+
var asArray29 = (v) => Array.isArray(v) ? v : [];
|
|
5659
5976
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
5660
5977
|
function validatePageSourceStyling(stack) {
|
|
5661
5978
|
const findings = [];
|
|
5662
|
-
const pages =
|
|
5979
|
+
const pages = asArray29(stack.pages);
|
|
5663
5980
|
for (let p = 0; p < pages.length; p++) {
|
|
5664
5981
|
const page = pages[p];
|
|
5665
5982
|
if (!page) continue;
|
|
@@ -5687,7 +6004,7 @@ function validatePageSourceStyling(stack) {
|
|
|
5687
6004
|
// src/validate-capability-references.ts
|
|
5688
6005
|
var import_security = require("@objectstack/spec/security");
|
|
5689
6006
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
5690
|
-
function
|
|
6007
|
+
function asArray30(v) {
|
|
5691
6008
|
if (Array.isArray(v)) return v;
|
|
5692
6009
|
if (v && typeof v === "object") {
|
|
5693
6010
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5712,13 +6029,13 @@ function validateCapabilityReferences(stack) {
|
|
|
5712
6029
|
const findings = [];
|
|
5713
6030
|
if (!stack || typeof stack !== "object") return findings;
|
|
5714
6031
|
const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
|
|
5715
|
-
for (const cap of
|
|
6032
|
+
for (const cap of asArray30(stack.capabilities)) {
|
|
5716
6033
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
5717
6034
|
}
|
|
5718
|
-
for (const ps of
|
|
6035
|
+
for (const ps of asArray30(stack.permissions)) {
|
|
5719
6036
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
5720
6037
|
}
|
|
5721
|
-
for (const seed of
|
|
6038
|
+
for (const seed of asArray30(stack.data)) {
|
|
5722
6039
|
if (seed.object !== "sys_capability") continue;
|
|
5723
6040
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
5724
6041
|
const name = rec?.name;
|
|
@@ -5737,7 +6054,7 @@ function validateCapabilityReferences(stack) {
|
|
|
5737
6054
|
hint
|
|
5738
6055
|
});
|
|
5739
6056
|
};
|
|
5740
|
-
const objects =
|
|
6057
|
+
const objects = asArray30(stack.objects);
|
|
5741
6058
|
for (let i = 0; i < objects.length; i++) {
|
|
5742
6059
|
const obj = objects[i];
|
|
5743
6060
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5746,27 +6063,27 @@ function validateCapabilityReferences(stack) {
|
|
|
5746
6063
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
5747
6064
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
5748
6065
|
}
|
|
5749
|
-
const fields =
|
|
6066
|
+
const fields = asArray30(obj.fields);
|
|
5750
6067
|
for (const f of fields) {
|
|
5751
6068
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
5752
6069
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
5753
6070
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
5754
6071
|
}
|
|
5755
6072
|
}
|
|
5756
|
-
for (const [ai, action] of
|
|
6073
|
+
for (const [ai, action] of asArray30(obj.actions).entries()) {
|
|
5757
6074
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
5758
6075
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5759
6076
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
5760
6077
|
}
|
|
5761
6078
|
}
|
|
5762
6079
|
}
|
|
5763
|
-
for (const [i, action] of
|
|
6080
|
+
for (const [i, action] of asArray30(stack.actions).entries()) {
|
|
5764
6081
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
5765
6082
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5766
6083
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
5767
6084
|
}
|
|
5768
6085
|
}
|
|
5769
|
-
const apps =
|
|
6086
|
+
const apps = asArray30(stack.apps);
|
|
5770
6087
|
for (let i = 0; i < apps.length; i++) {
|
|
5771
6088
|
const app = apps[i];
|
|
5772
6089
|
if (!app || typeof app !== "object") continue;
|
|
@@ -5801,7 +6118,7 @@ var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-inval
|
|
|
5801
6118
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
5802
6119
|
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5803
6120
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5804
|
-
function
|
|
6121
|
+
function asArray31(v) {
|
|
5805
6122
|
if (Array.isArray(v)) return v;
|
|
5806
6123
|
if (v && typeof v === "object") {
|
|
5807
6124
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -5829,10 +6146,10 @@ function startNodeOf(flow) {
|
|
|
5829
6146
|
}
|
|
5830
6147
|
function validateFlowTriggerReadiness(stack) {
|
|
5831
6148
|
const findings = [];
|
|
5832
|
-
const flows =
|
|
6149
|
+
const flows = asArray31(stack.flows);
|
|
5833
6150
|
if (flows.length === 0) return findings;
|
|
5834
6151
|
const objectNames = new Set(
|
|
5835
|
-
|
|
6152
|
+
asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
|
|
5836
6153
|
);
|
|
5837
6154
|
flows.forEach((flow, flowIndex) => {
|
|
5838
6155
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
@@ -5997,7 +6314,7 @@ var TYPE_FIX = {
|
|
|
5997
6314
|
business_unit: "department",
|
|
5998
6315
|
bu: "department"
|
|
5999
6316
|
};
|
|
6000
|
-
function
|
|
6317
|
+
function asArray32(v) {
|
|
6001
6318
|
if (Array.isArray(v)) return v;
|
|
6002
6319
|
if (v && typeof v === "object") {
|
|
6003
6320
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6007,7 +6324,7 @@ function asArray31(v) {
|
|
|
6007
6324
|
function validateApprovalApprovers(stack) {
|
|
6008
6325
|
const findings = [];
|
|
6009
6326
|
if (!stack || typeof stack !== "object") return findings;
|
|
6010
|
-
const flows =
|
|
6327
|
+
const flows = asArray32(stack.flows);
|
|
6011
6328
|
const validTypes = new Set(import_automation4.ApproverType.options);
|
|
6012
6329
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
6013
6330
|
const flow = flows[fi];
|
|
@@ -6187,10 +6504,10 @@ function validateApprovalApprovers(stack) {
|
|
|
6187
6504
|
}
|
|
6188
6505
|
|
|
6189
6506
|
// src/validate-record-title.ts
|
|
6190
|
-
var
|
|
6507
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
6191
6508
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
6192
6509
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
6193
|
-
function
|
|
6510
|
+
function asArray33(v) {
|
|
6194
6511
|
if (Array.isArray(v)) return v;
|
|
6195
6512
|
if (v && typeof v === "object") {
|
|
6196
6513
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6199,7 +6516,7 @@ function asArray32(v) {
|
|
|
6199
6516
|
}
|
|
6200
6517
|
function validateRecordTitle(stack) {
|
|
6201
6518
|
const findings = [];
|
|
6202
|
-
const objects =
|
|
6519
|
+
const objects = asArray33(stack.objects);
|
|
6203
6520
|
for (let i = 0; i < objects.length; i++) {
|
|
6204
6521
|
const obj = objects[i];
|
|
6205
6522
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -6215,7 +6532,7 @@ function validateRecordTitle(stack) {
|
|
|
6215
6532
|
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.`
|
|
6216
6533
|
});
|
|
6217
6534
|
}
|
|
6218
|
-
const completeness = (0,
|
|
6535
|
+
const completeness = (0, import_data9.objectTitleCompleteness)(obj);
|
|
6219
6536
|
if (completeness.status === "none") {
|
|
6220
6537
|
findings.push({
|
|
6221
6538
|
severity: "warning",
|
|
@@ -6235,7 +6552,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
6235
6552
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
6236
6553
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
6237
6554
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
6238
|
-
|
|
6555
|
+
var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
|
|
6556
|
+
function asArray34(v) {
|
|
6239
6557
|
if (Array.isArray(v)) return v;
|
|
6240
6558
|
if (v && typeof v === "object") {
|
|
6241
6559
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6244,7 +6562,7 @@ function asArray33(v) {
|
|
|
6244
6562
|
}
|
|
6245
6563
|
function validateSemanticRoles(stack) {
|
|
6246
6564
|
const findings = [];
|
|
6247
|
-
const objects =
|
|
6565
|
+
const objects = asArray34(stack.objects);
|
|
6248
6566
|
for (let i = 0; i < objects.length; i++) {
|
|
6249
6567
|
const obj = objects[i];
|
|
6250
6568
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -6253,6 +6571,15 @@ function validateSemanticRoles(stack) {
|
|
|
6253
6571
|
const path = `objects[${i}]`;
|
|
6254
6572
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
6255
6573
|
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
6574
|
+
const unprovisioned = unprovisionedInjectedColumnsFor(obj);
|
|
6575
|
+
const unprovisionedPointer = (slot, entry) => ({
|
|
6576
|
+
severity: "warning",
|
|
6577
|
+
rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
6578
|
+
where,
|
|
6579
|
+
path: `${path}.${slot}`,
|
|
6580
|
+
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.`,
|
|
6581
|
+
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.`
|
|
6582
|
+
});
|
|
6256
6583
|
const declaredGroups = new Set(
|
|
6257
6584
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
6258
6585
|
);
|
|
@@ -6294,10 +6621,16 @@ function validateSemanticRoles(stack) {
|
|
|
6294
6621
|
message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
|
|
6295
6622
|
hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
|
|
6296
6623
|
});
|
|
6624
|
+
} else if (typeof stage === "string" && unprovisioned.has(stage)) {
|
|
6625
|
+
findings.push(unprovisionedPointer("stageField", stage));
|
|
6297
6626
|
}
|
|
6298
6627
|
const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
|
|
6299
6628
|
for (const entry of highlights) {
|
|
6300
|
-
if (typeof entry !== "string" || entry.length === 0
|
|
6629
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
6630
|
+
if (fieldNames.has(entry)) {
|
|
6631
|
+
if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
|
|
6632
|
+
continue;
|
|
6633
|
+
}
|
|
6301
6634
|
findings.push({
|
|
6302
6635
|
severity: "warning",
|
|
6303
6636
|
rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -6339,17 +6672,17 @@ function validateSemanticRoles(stack) {
|
|
|
6339
6672
|
// src/validate-form-layout.ts
|
|
6340
6673
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
6341
6674
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
6342
|
-
function
|
|
6675
|
+
function asArray35(v) {
|
|
6343
6676
|
if (Array.isArray(v)) return v;
|
|
6344
6677
|
if (v && typeof v === "object") {
|
|
6345
6678
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6346
6679
|
}
|
|
6347
6680
|
return [];
|
|
6348
6681
|
}
|
|
6349
|
-
function
|
|
6682
|
+
function isRec20(v) {
|
|
6350
6683
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6351
6684
|
}
|
|
6352
|
-
function
|
|
6685
|
+
function strName19(v) {
|
|
6353
6686
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6354
6687
|
}
|
|
6355
6688
|
function fieldNameOf(entry) {
|
|
@@ -6363,14 +6696,14 @@ function fieldNameOf(entry) {
|
|
|
6363
6696
|
function validateFormLayout(stack) {
|
|
6364
6697
|
const findings = [];
|
|
6365
6698
|
const objectFields = /* @__PURE__ */ new Map();
|
|
6366
|
-
for (const obj of
|
|
6699
|
+
for (const obj of asArray35(stack.objects)) {
|
|
6367
6700
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6368
6701
|
if (!name) continue;
|
|
6369
6702
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6370
6703
|
objectFields.set(name, new Set(fields));
|
|
6371
6704
|
}
|
|
6372
6705
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6373
|
-
const viewName =
|
|
6706
|
+
const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
|
|
6374
6707
|
const containerObject = viewObjectName(view);
|
|
6375
6708
|
for (const site of formViewSites(view, viewPath)) {
|
|
6376
6709
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6380,7 +6713,7 @@ function validateFormLayout(stack) {
|
|
|
6380
6713
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6381
6714
|
for (let s = 0; s < sections.length; s++) {
|
|
6382
6715
|
const sec = sections[s];
|
|
6383
|
-
const secFields =
|
|
6716
|
+
const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6384
6717
|
for (let f = 0; f < secFields.length; f++) {
|
|
6385
6718
|
const entry = secFields[f];
|
|
6386
6719
|
const fname = fieldNameOf(entry);
|
|
@@ -6395,7 +6728,7 @@ function validateFormLayout(stack) {
|
|
|
6395
6728
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6396
6729
|
});
|
|
6397
6730
|
}
|
|
6398
|
-
const colSpan =
|
|
6731
|
+
const colSpan = isRec20(entry) ? entry.colSpan : void 0;
|
|
6399
6732
|
if (colSpan != null) {
|
|
6400
6733
|
findings.push({
|
|
6401
6734
|
severity: "warning",
|
|
@@ -6506,6 +6839,64 @@ function validateSeedStateMachine(stack) {
|
|
|
6506
6839
|
|
|
6507
6840
|
// src/validate-visibility-predicates.ts
|
|
6508
6841
|
var import_formula4 = require("@objectstack/formula");
|
|
6842
|
+
|
|
6843
|
+
// src/predicate-rhs-position.ts
|
|
6844
|
+
function isNode2(v) {
|
|
6845
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6846
|
+
}
|
|
6847
|
+
var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
|
|
6848
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6849
|
+
function bareId(node) {
|
|
6850
|
+
if (!isNode2(node)) return null;
|
|
6851
|
+
return node.op === "id" && typeof node.args === "string" ? node.args : null;
|
|
6852
|
+
}
|
|
6853
|
+
function bareRhsOnlyIdentifiers(ast) {
|
|
6854
|
+
const rhs = /* @__PURE__ */ new Set();
|
|
6855
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
6856
|
+
const walk = (node, suppressible) => {
|
|
6857
|
+
if (Array.isArray(node)) {
|
|
6858
|
+
for (const child of node) walk(child, suppressible);
|
|
6859
|
+
return;
|
|
6860
|
+
}
|
|
6861
|
+
if (!isNode2(node)) return;
|
|
6862
|
+
const args = node.args;
|
|
6863
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6864
|
+
walk(args[1], suppressible);
|
|
6865
|
+
walk(args[2], false);
|
|
6866
|
+
return;
|
|
6867
|
+
}
|
|
6868
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
6869
|
+
const right = suppressible ? bareId(args[1]) : null;
|
|
6870
|
+
walk(args[0], suppressible);
|
|
6871
|
+
if (right !== null) {
|
|
6872
|
+
rhs.add(right);
|
|
6873
|
+
return;
|
|
6874
|
+
}
|
|
6875
|
+
walk(args[1], suppressible);
|
|
6876
|
+
return;
|
|
6877
|
+
}
|
|
6878
|
+
const name = bareId(node);
|
|
6879
|
+
if (name !== null) {
|
|
6880
|
+
elsewhere.add(name);
|
|
6881
|
+
return;
|
|
6882
|
+
}
|
|
6883
|
+
walk(args, suppressible);
|
|
6884
|
+
};
|
|
6885
|
+
walk(ast, true);
|
|
6886
|
+
for (const name of elsewhere) rhs.delete(name);
|
|
6887
|
+
return rhs;
|
|
6888
|
+
}
|
|
6889
|
+
function isRec21(v) {
|
|
6890
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6891
|
+
}
|
|
6892
|
+
function schemaIdOf(view) {
|
|
6893
|
+
const data = view.data;
|
|
6894
|
+
if (!isRec21(data)) return void 0;
|
|
6895
|
+
if (data.provider !== "schema") return void 0;
|
|
6896
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6897
|
+
}
|
|
6898
|
+
|
|
6899
|
+
// src/validate-visibility-predicates.ts
|
|
6509
6900
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6510
6901
|
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6511
6902
|
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
@@ -6553,7 +6944,7 @@ function boundName(overrun) {
|
|
|
6553
6944
|
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6554
6945
|
}
|
|
6555
6946
|
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6556
|
-
function
|
|
6947
|
+
function isNode3(v) {
|
|
6557
6948
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6558
6949
|
}
|
|
6559
6950
|
function namespaceRoots(node, out) {
|
|
@@ -6561,22 +6952,27 @@ function namespaceRoots(node, out) {
|
|
|
6561
6952
|
for (const child of node) namespaceRoots(child, out);
|
|
6562
6953
|
return;
|
|
6563
6954
|
}
|
|
6564
|
-
if (!
|
|
6955
|
+
if (!isNode3(node)) return;
|
|
6565
6956
|
const args = node.args;
|
|
6566
6957
|
if (Array.isArray(args)) {
|
|
6567
6958
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6568
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
6959
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6569
6960
|
out.add(receiver.args);
|
|
6570
6961
|
}
|
|
6571
6962
|
}
|
|
6572
6963
|
namespaceRoots(args, out);
|
|
6573
6964
|
}
|
|
6574
|
-
function firstBareIdentifier(source) {
|
|
6965
|
+
function firstBareIdentifier(source, literalRhs) {
|
|
6575
6966
|
const ast = (0, import_formula4.parseCelToAst)(source);
|
|
6576
6967
|
if (!ast) return null;
|
|
6577
6968
|
const rooted = /* @__PURE__ */ new Set();
|
|
6578
6969
|
namespaceRoots(ast, rooted);
|
|
6579
|
-
|
|
6970
|
+
const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
|
|
6971
|
+
return (0, import_formula4.firstUndeclaredReference)(source, [
|
|
6972
|
+
...VIEW_PAGE_EXTRA_ROOTS,
|
|
6973
|
+
...rooted,
|
|
6974
|
+
...literalSlot
|
|
6975
|
+
]);
|
|
6580
6976
|
}
|
|
6581
6977
|
var CANONICAL_ROOT_BY_LAYER = {
|
|
6582
6978
|
runtime: "record",
|
|
@@ -6585,16 +6981,16 @@ var CANONICAL_ROOT_BY_LAYER = {
|
|
|
6585
6981
|
var MISLAYER_BY_LAYER = {
|
|
6586
6982
|
runtime: {
|
|
6587
6983
|
forbiddenRoot: "data",
|
|
6588
|
-
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing
|
|
6984
|
+
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).",
|
|
6589
6985
|
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
6590
6986
|
},
|
|
6591
6987
|
metadata: {
|
|
6592
6988
|
forbiddenRoot: "record",
|
|
6593
|
-
message: "visibility predicate is rooted at `record.` \u2014 that is the
|
|
6989
|
+
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).",
|
|
6594
6990
|
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
6595
6991
|
}
|
|
6596
6992
|
};
|
|
6597
|
-
function checkElement(el, where, path, layer, findings) {
|
|
6993
|
+
function checkElement(el, where, path, layer, findings, literalRhs = false) {
|
|
6598
6994
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6599
6995
|
const source = predicateSource(raw);
|
|
6600
6996
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -6632,7 +7028,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6632
7028
|
});
|
|
6633
7029
|
}
|
|
6634
7030
|
if (source && !refusal) {
|
|
6635
|
-
const bare = firstBareIdentifier(source);
|
|
7031
|
+
const bare = firstBareIdentifier(source, literalRhs);
|
|
6636
7032
|
if (bare) {
|
|
6637
7033
|
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6638
7034
|
findings.push({
|
|
@@ -6641,7 +7037,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6641
7037
|
where,
|
|
6642
7038
|
path,
|
|
6643
7039
|
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).`,
|
|
6644
|
-
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
|
|
7040
|
+
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`).")
|
|
6645
7041
|
});
|
|
6646
7042
|
}
|
|
6647
7043
|
}
|
|
@@ -6650,24 +7046,27 @@ function isFieldObject(entry) {
|
|
|
6650
7046
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
6651
7047
|
}
|
|
6652
7048
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
6653
|
-
const
|
|
7049
|
+
const declaredLayer = opts.layer ?? "runtime";
|
|
6654
7050
|
const findings = [];
|
|
6655
7051
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6656
7052
|
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6657
7053
|
for (const site of formViewSites(view, viewPath)) {
|
|
6658
7054
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
7055
|
+
const schemaBound = schemaIdOf(site.view) !== void 0;
|
|
7056
|
+
const literalRhs = schemaBound;
|
|
7057
|
+
const layer = schemaBound ? "metadata" : declaredLayer;
|
|
6659
7058
|
for (const bucket of ["sections", "groups"]) {
|
|
6660
7059
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6661
7060
|
for (let s = 0; s < sections.length; s++) {
|
|
6662
7061
|
const sec = sections[s];
|
|
6663
7062
|
if (!sec || typeof sec !== "object") continue;
|
|
6664
7063
|
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
6665
|
-
checkElement(sec, where, secPath, layer, findings);
|
|
7064
|
+
checkElement(sec, where, secPath, layer, findings, literalRhs);
|
|
6666
7065
|
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
6667
7066
|
for (let f = 0; f < secFields.length; f++) {
|
|
6668
7067
|
const entry = secFields[f];
|
|
6669
7068
|
if (isFieldObject(entry)) {
|
|
6670
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
7069
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
|
|
6671
7070
|
}
|
|
6672
7071
|
}
|
|
6673
7072
|
}
|
|
@@ -6678,7 +7077,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
6678
7077
|
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
6679
7078
|
const where = `page "${pageName ?? pagePath}"`;
|
|
6680
7079
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6681
|
-
checkElement(walked.component, where, walked.path,
|
|
7080
|
+
checkElement(walked.component, where, walked.path, declaredLayer, findings);
|
|
6682
7081
|
}
|
|
6683
7082
|
}
|
|
6684
7083
|
return findings;
|
|
@@ -6690,9 +7089,9 @@ var import_kernel2 = require("@objectstack/spec/kernel");
|
|
|
6690
7089
|
var import_spec4 = require("@objectstack/spec");
|
|
6691
7090
|
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
6692
7091
|
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
7092
|
+
var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
|
|
6693
7093
|
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
6694
7094
|
var ROOT = "data";
|
|
6695
|
-
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6696
7095
|
function defOf(schema) {
|
|
6697
7096
|
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
6698
7097
|
const s = schema;
|
|
@@ -6785,11 +7184,11 @@ function stepInto(scope, segment) {
|
|
|
6785
7184
|
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
6786
7185
|
return { kind: "declared", next: propertyOf(u, segment) };
|
|
6787
7186
|
}
|
|
6788
|
-
function
|
|
7187
|
+
function isNode4(v) {
|
|
6789
7188
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6790
7189
|
}
|
|
6791
7190
|
function memberChain(node) {
|
|
6792
|
-
if (!
|
|
7191
|
+
if (!isNode4(node)) return null;
|
|
6793
7192
|
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
6794
7193
|
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
6795
7194
|
const head = memberChain(node.args[0]);
|
|
@@ -6802,7 +7201,7 @@ function rootedPaths(node, out) {
|
|
|
6802
7201
|
for (const child of node) rootedPaths(child, out);
|
|
6803
7202
|
return;
|
|
6804
7203
|
}
|
|
6805
|
-
if (!
|
|
7204
|
+
if (!isNode4(node)) return;
|
|
6806
7205
|
if (node.op === ".") {
|
|
6807
7206
|
const chain = memberChain(node);
|
|
6808
7207
|
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
@@ -6812,23 +7211,40 @@ function rootedPaths(node, out) {
|
|
|
6812
7211
|
}
|
|
6813
7212
|
rootedPaths(node.args, out);
|
|
6814
7213
|
}
|
|
7214
|
+
var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
|
|
7215
|
+
function equalitySites(node, out) {
|
|
7216
|
+
if (Array.isArray(node)) {
|
|
7217
|
+
for (const child of node) equalitySites(child, out);
|
|
7218
|
+
return;
|
|
7219
|
+
}
|
|
7220
|
+
if (!isNode4(node)) return;
|
|
7221
|
+
const args = node.args;
|
|
7222
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7223
|
+
equalitySites(args[1], out);
|
|
7224
|
+
return;
|
|
7225
|
+
}
|
|
7226
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
7227
|
+
out.push({ op: node.op, right: args[1] });
|
|
7228
|
+
}
|
|
7229
|
+
equalitySites(args, out);
|
|
7230
|
+
}
|
|
6815
7231
|
function classifyIdentifiers(node, values, excluded) {
|
|
6816
7232
|
if (Array.isArray(node)) {
|
|
6817
7233
|
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
6818
7234
|
return;
|
|
6819
7235
|
}
|
|
6820
|
-
if (!
|
|
7236
|
+
if (!isNode4(node)) return;
|
|
6821
7237
|
const args = node.args;
|
|
6822
7238
|
if (Array.isArray(args)) {
|
|
6823
7239
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6824
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
7240
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6825
7241
|
excluded.add(receiver.args);
|
|
6826
7242
|
}
|
|
6827
7243
|
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6828
7244
|
const macroArgs = args[2];
|
|
6829
7245
|
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
6830
7246
|
const bound = macroArgs[0];
|
|
6831
|
-
if (
|
|
7247
|
+
if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
6832
7248
|
excluded.add(bound.args);
|
|
6833
7249
|
}
|
|
6834
7250
|
}
|
|
@@ -6847,15 +7263,9 @@ function predicateSource2(v) {
|
|
|
6847
7263
|
}
|
|
6848
7264
|
return void 0;
|
|
6849
7265
|
}
|
|
6850
|
-
function
|
|
7266
|
+
function isRec22(v) {
|
|
6851
7267
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6852
7268
|
}
|
|
6853
|
-
function schemaIdOf(view) {
|
|
6854
|
-
const data = view.data;
|
|
6855
|
-
if (!isRec19(data)) return void 0;
|
|
6856
|
-
if (data.provider !== "schema") return void 0;
|
|
6857
|
-
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6858
|
-
}
|
|
6859
7269
|
function checkPredicate(source, scope, where, path, findings) {
|
|
6860
7270
|
const ast = (0, import_formula5.parseCelToAst)(source);
|
|
6861
7271
|
if (!ast) return;
|
|
@@ -6885,19 +7295,38 @@ function checkPredicate(source, scope, where, path, findings) {
|
|
|
6885
7295
|
}
|
|
6886
7296
|
}
|
|
6887
7297
|
const declaredHere = keysOf(scope);
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
7298
|
+
const rhsOnly = bareRhsOnlyIdentifiers(ast);
|
|
7299
|
+
if (declaredHere) {
|
|
7300
|
+
const values = /* @__PURE__ */ new Set();
|
|
7301
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
7302
|
+
classifyIdentifiers(ast, values, excluded);
|
|
7303
|
+
for (const id of values) {
|
|
7304
|
+
if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
|
|
7305
|
+
findings.push({
|
|
7306
|
+
severity: "error",
|
|
7307
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
7308
|
+
where,
|
|
7309
|
+
path,
|
|
7310
|
+
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).`,
|
|
7311
|
+
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).`
|
|
7312
|
+
});
|
|
7313
|
+
}
|
|
7314
|
+
}
|
|
7315
|
+
const sites = [];
|
|
7316
|
+
equalitySites(ast, sites);
|
|
7317
|
+
for (const { op, right } of sites) {
|
|
7318
|
+
const chain = memberChain(right);
|
|
7319
|
+
if (!chain) continue;
|
|
7320
|
+
const text = chain.join(".");
|
|
7321
|
+
if (!PATH_SHAPED_RHS.test(text)) continue;
|
|
7322
|
+
const dotted = chain.length > 1;
|
|
6894
7323
|
findings.push({
|
|
6895
|
-
severity: "error",
|
|
6896
|
-
rule:
|
|
7324
|
+
severity: dotted ? "error" : "warning",
|
|
7325
|
+
rule: PREDICATE_RHS_PATH_SHAPED,
|
|
6897
7326
|
where,
|
|
6898
7327
|
path,
|
|
6899
|
-
message: `predicate
|
|
6900
|
-
hint: `
|
|
7328
|
+
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).`,
|
|
7329
|
+
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.`
|
|
6901
7330
|
});
|
|
6902
7331
|
}
|
|
6903
7332
|
}
|
|
@@ -6905,7 +7334,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
6905
7334
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
6906
7335
|
for (let i = 0; i < entries.length; i++) {
|
|
6907
7336
|
const entry = entries[i];
|
|
6908
|
-
if (!
|
|
7337
|
+
if (!isRec22(entry)) continue;
|
|
6909
7338
|
const path = `${base}[${i}]`;
|
|
6910
7339
|
for (const key of PREDICATE_KEYS) {
|
|
6911
7340
|
const source = predicateSource2(entry[key]);
|
|
@@ -6932,15 +7361,14 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
6932
7361
|
try {
|
|
6933
7362
|
root = resolveSchema(schemaId);
|
|
6934
7363
|
} catch {
|
|
6935
|
-
|
|
7364
|
+
root = void 0;
|
|
6936
7365
|
}
|
|
6937
|
-
if (!root) continue;
|
|
6938
7366
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
6939
7367
|
for (const bucket of ["sections", "groups"]) {
|
|
6940
7368
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6941
7369
|
for (let s = 0; s < sections.length; s++) {
|
|
6942
7370
|
const section = sections[s];
|
|
6943
|
-
if (!
|
|
7371
|
+
if (!isRec22(section)) continue;
|
|
6944
7372
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
6945
7373
|
for (const key of PREDICATE_KEYS) {
|
|
6946
7374
|
const source = predicateSource2(section[key]);
|
|
@@ -6971,6 +7399,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
|
6971
7399
|
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
6972
7400
|
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
6973
7401
|
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
7402
|
+
var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
|
|
6974
7403
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
6975
7404
|
var OWD_ALIAS_FIX = {
|
|
6976
7405
|
read: "public_read",
|
|
@@ -6983,7 +7412,7 @@ var OWD_WIDTH = {
|
|
|
6983
7412
|
public_read: 1,
|
|
6984
7413
|
public_read_write: 2
|
|
6985
7414
|
};
|
|
6986
|
-
function
|
|
7415
|
+
function asArray36(v) {
|
|
6987
7416
|
if (Array.isArray(v)) return v;
|
|
6988
7417
|
if (v && typeof v === "object") {
|
|
6989
7418
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7009,21 +7438,28 @@ function refOf(def) {
|
|
|
7009
7438
|
return typeof r === "string" && r ? r : void 0;
|
|
7010
7439
|
}
|
|
7011
7440
|
function firstMasterDetailField(obj) {
|
|
7012
|
-
for (const f of
|
|
7441
|
+
for (const f of asArray36(obj.fields)) {
|
|
7013
7442
|
if (f.type === "master_detail") {
|
|
7014
7443
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
7015
7444
|
}
|
|
7016
7445
|
}
|
|
7017
7446
|
return void 0;
|
|
7018
7447
|
}
|
|
7448
|
+
function resolveCbpRelation(obj) {
|
|
7449
|
+
const entries = asArray36(obj.fields);
|
|
7450
|
+
const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
|
|
7451
|
+
const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
|
|
7452
|
+
if (!found) return void 0;
|
|
7453
|
+
return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
|
|
7454
|
+
}
|
|
7019
7455
|
function grantsObjectAccess(p) {
|
|
7020
7456
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
7021
7457
|
}
|
|
7022
7458
|
function validateSecurityPosture(stack, opts) {
|
|
7023
7459
|
const findings = [];
|
|
7024
7460
|
if (!stack || typeof stack !== "object") return findings;
|
|
7025
|
-
const objects =
|
|
7026
|
-
const permissionSets =
|
|
7461
|
+
const objects = asArray36(stack.objects);
|
|
7462
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7027
7463
|
for (let i = 0; i < objects.length; i++) {
|
|
7028
7464
|
const obj = objects[i];
|
|
7029
7465
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -7061,6 +7497,16 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7061
7497
|
});
|
|
7062
7498
|
}
|
|
7063
7499
|
}
|
|
7500
|
+
if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
|
|
7501
|
+
findings.push({
|
|
7502
|
+
severity: "error",
|
|
7503
|
+
rule: SECURITY_CBP_NO_RELATION,
|
|
7504
|
+
where: `object "${objName}"`,
|
|
7505
|
+
path: `${objPath}.sharingModel`,
|
|
7506
|
+
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.`,
|
|
7507
|
+
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'.`
|
|
7508
|
+
});
|
|
7509
|
+
}
|
|
7064
7510
|
if (typeof external === "string") {
|
|
7065
7511
|
if (OWD_ALIAS_FIX[external]) {
|
|
7066
7512
|
findings.push({
|
|
@@ -7126,57 +7572,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7126
7572
|
}
|
|
7127
7573
|
}
|
|
7128
7574
|
}
|
|
7129
|
-
const flagRole = (kind, name, label2, where, path) => {
|
|
7130
|
-
if (identifierHasRoleToken(name)) {
|
|
7131
|
-
findings.push({
|
|
7132
|
-
severity: "error",
|
|
7133
|
-
rule: SECURITY_ROLE_WORD,
|
|
7134
|
-
where,
|
|
7135
|
-
path,
|
|
7136
|
-
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).`,
|
|
7137
|
-
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7138
|
-
});
|
|
7139
|
-
} else if (labelHasRoleWord(label2)) {
|
|
7140
|
-
findings.push({
|
|
7141
|
-
severity: "error",
|
|
7142
|
-
rule: SECURITY_ROLE_WORD,
|
|
7143
|
-
where,
|
|
7144
|
-
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7145
|
-
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7146
|
-
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7147
|
-
});
|
|
7148
|
-
}
|
|
7149
|
-
};
|
|
7150
|
-
for (let i = 0; i < objects.length; i++) {
|
|
7151
|
-
const obj = objects[i];
|
|
7152
|
-
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7153
|
-
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7154
|
-
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7155
|
-
for (const f of asArray35(obj.fields)) {
|
|
7156
|
-
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7157
|
-
}
|
|
7158
|
-
for (const [ai, action] of asArray35(obj.actions).entries()) {
|
|
7159
|
-
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7160
|
-
}
|
|
7161
|
-
}
|
|
7162
|
-
for (let i = 0; i < permissionSets.length; i++) {
|
|
7163
|
-
const ps = permissionSets[i];
|
|
7164
|
-
if (!ps || typeof ps !== "object") continue;
|
|
7165
|
-
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7166
|
-
}
|
|
7167
|
-
for (const [i, pos] of asArray35(stack.positions).entries()) {
|
|
7168
|
-
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7169
|
-
}
|
|
7170
|
-
for (const [i, app] of asArray35(stack.apps).entries()) {
|
|
7171
|
-
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7172
|
-
}
|
|
7173
|
-
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
7174
|
-
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7175
|
-
}
|
|
7176
7575
|
const stackSetNames = new Set(
|
|
7177
7576
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
7178
7577
|
);
|
|
7179
|
-
for (const [i, book] of
|
|
7578
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7180
7579
|
const audience = book.audience;
|
|
7181
7580
|
if (!audience || typeof audience !== "object") continue;
|
|
7182
7581
|
const setName = audience.permissionSet;
|
|
@@ -7254,7 +7653,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7254
7653
|
}
|
|
7255
7654
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
7256
7655
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7257
|
-
for (const [i, seed] of
|
|
7656
|
+
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7258
7657
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
7259
7658
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
7260
7659
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -7293,13 +7692,67 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7293
7692
|
}
|
|
7294
7693
|
return findings;
|
|
7295
7694
|
}
|
|
7695
|
+
function validateSecurityRoleWord(stack) {
|
|
7696
|
+
const findings = [];
|
|
7697
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
7698
|
+
const objects = asArray36(stack.objects);
|
|
7699
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7700
|
+
const flagRole = (kind, name, label2, where, path) => {
|
|
7701
|
+
if (identifierHasRoleToken(name)) {
|
|
7702
|
+
findings.push({
|
|
7703
|
+
severity: "error",
|
|
7704
|
+
rule: SECURITY_ROLE_WORD,
|
|
7705
|
+
where,
|
|
7706
|
+
path,
|
|
7707
|
+
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).`,
|
|
7708
|
+
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7709
|
+
});
|
|
7710
|
+
} else if (labelHasRoleWord(label2)) {
|
|
7711
|
+
findings.push({
|
|
7712
|
+
severity: "error",
|
|
7713
|
+
rule: SECURITY_ROLE_WORD,
|
|
7714
|
+
where,
|
|
7715
|
+
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7716
|
+
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7717
|
+
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7718
|
+
});
|
|
7719
|
+
}
|
|
7720
|
+
};
|
|
7721
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7722
|
+
const obj = objects[i];
|
|
7723
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7724
|
+
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7725
|
+
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7726
|
+
for (const f of asArray36(obj.fields)) {
|
|
7727
|
+
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7728
|
+
}
|
|
7729
|
+
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
7730
|
+
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
for (let i = 0; i < permissionSets.length; i++) {
|
|
7734
|
+
const ps = permissionSets[i];
|
|
7735
|
+
if (!ps || typeof ps !== "object") continue;
|
|
7736
|
+
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7737
|
+
}
|
|
7738
|
+
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
7739
|
+
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7740
|
+
}
|
|
7741
|
+
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
7742
|
+
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7743
|
+
}
|
|
7744
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7745
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7746
|
+
}
|
|
7747
|
+
return findings;
|
|
7748
|
+
}
|
|
7296
7749
|
|
|
7297
7750
|
// src/validate-org-axis-red-lines.ts
|
|
7298
7751
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
7299
7752
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
7300
7753
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
7301
7754
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
7302
|
-
function
|
|
7755
|
+
function asArray37(v) {
|
|
7303
7756
|
if (Array.isArray(v)) return v;
|
|
7304
7757
|
if (v && typeof v === "object") {
|
|
7305
7758
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7329,9 +7782,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
7329
7782
|
function validateOrgAxisRedLines(stack) {
|
|
7330
7783
|
const findings = [];
|
|
7331
7784
|
const cfg = stack ?? {};
|
|
7332
|
-
const permissionSets =
|
|
7785
|
+
const permissionSets = asArray37(cfg.permissions);
|
|
7333
7786
|
permissionSets.forEach((ps, psIndex) => {
|
|
7334
|
-
|
|
7787
|
+
asArray37(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
7335
7788
|
for (const clause of ["using", "check"]) {
|
|
7336
7789
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
7337
7790
|
findings.push({
|
|
@@ -7345,7 +7798,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7345
7798
|
}
|
|
7346
7799
|
});
|
|
7347
7800
|
});
|
|
7348
|
-
|
|
7801
|
+
asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
7349
7802
|
const slots = [
|
|
7350
7803
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
7351
7804
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -7363,9 +7816,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7363
7816
|
}
|
|
7364
7817
|
});
|
|
7365
7818
|
const tenancyDisabledObjects = new Set(
|
|
7366
|
-
|
|
7819
|
+
asArray37(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
7367
7820
|
);
|
|
7368
|
-
|
|
7821
|
+
asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
7369
7822
|
const target = str(rule.object);
|
|
7370
7823
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
7371
7824
|
const sharedWith = rule.sharedWith;
|
|
@@ -7388,7 +7841,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7388
7841
|
var import_formula6 = require("@objectstack/formula");
|
|
7389
7842
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7390
7843
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
7391
|
-
function
|
|
7844
|
+
function asArray38(v) {
|
|
7392
7845
|
if (Array.isArray(v)) return v;
|
|
7393
7846
|
if (v && typeof v === "object") {
|
|
7394
7847
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7415,7 +7868,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
7415
7868
|
function validateSharingRuleEnforceability(stack) {
|
|
7416
7869
|
const findings = [];
|
|
7417
7870
|
const cfg = stack ?? {};
|
|
7418
|
-
|
|
7871
|
+
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
7419
7872
|
const input = toCompilerInput(rule.condition);
|
|
7420
7873
|
if (input === null) return;
|
|
7421
7874
|
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
@@ -7455,7 +7908,7 @@ var import_formula7 = require("@objectstack/formula");
|
|
|
7455
7908
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
7456
7909
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
7457
7910
|
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
7458
|
-
function
|
|
7911
|
+
function asArray39(v) {
|
|
7459
7912
|
if (Array.isArray(v)) return v;
|
|
7460
7913
|
if (v && typeof v === "object") {
|
|
7461
7914
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7480,8 +7933,8 @@ function consequence(clause) {
|
|
|
7480
7933
|
function validateRlsPredicateEnforceability(stack) {
|
|
7481
7934
|
const findings = [];
|
|
7482
7935
|
const cfg = stack ?? {};
|
|
7483
|
-
|
|
7484
|
-
|
|
7936
|
+
asArray39(cfg.permissions).forEach((ps, psIndex) => {
|
|
7937
|
+
asArray39(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
7485
7938
|
for (const clause of ["using", "check"]) {
|
|
7486
7939
|
const source = str3(policy[clause]);
|
|
7487
7940
|
if (!source.trim()) continue;
|
|
@@ -7541,11 +7994,11 @@ var import_meta4 = {};
|
|
|
7541
7994
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7542
7995
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7543
7996
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7544
|
-
var
|
|
7545
|
-
function
|
|
7546
|
-
if (Array.isArray(v)) return v.filter(
|
|
7547
|
-
if (
|
|
7548
|
-
return Object.entries(v).filter(([, def]) =>
|
|
7997
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7998
|
+
function asArray40(v) {
|
|
7999
|
+
if (Array.isArray(v)) return v.filter(isRec23);
|
|
8000
|
+
if (isRec23(v)) {
|
|
8001
|
+
return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
|
|
7549
8002
|
}
|
|
7550
8003
|
return [];
|
|
7551
8004
|
}
|
|
@@ -7562,7 +8015,7 @@ function loadAjv() {
|
|
|
7562
8015
|
`@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.`
|
|
7563
8016
|
);
|
|
7564
8017
|
}
|
|
7565
|
-
const ctor =
|
|
8018
|
+
const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
7566
8019
|
cachedAjv = ctor;
|
|
7567
8020
|
return ctor;
|
|
7568
8021
|
}
|
|
@@ -7577,7 +8030,7 @@ function loadAddFormats() {
|
|
|
7577
8030
|
`@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.`
|
|
7578
8031
|
);
|
|
7579
8032
|
}
|
|
7580
|
-
const plugin =
|
|
8033
|
+
const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
7581
8034
|
cachedAddFormats = plugin;
|
|
7582
8035
|
return plugin;
|
|
7583
8036
|
}
|
|
@@ -7607,17 +8060,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
7607
8060
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
7608
8061
|
for (const branch of ["then", "otherwise"]) {
|
|
7609
8062
|
const nested = rule[branch];
|
|
7610
|
-
if (
|
|
8063
|
+
if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
7611
8064
|
}
|
|
7612
8065
|
return out;
|
|
7613
8066
|
}
|
|
7614
8067
|
function walkObjectValidationRules(stack) {
|
|
7615
8068
|
const walked = [];
|
|
7616
|
-
if (!
|
|
7617
|
-
for (const obj of
|
|
8069
|
+
if (!isRec23(stack)) return walked;
|
|
8070
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7618
8071
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7619
8072
|
const validations = obj.validations;
|
|
7620
|
-
for (const authored of
|
|
8073
|
+
for (const authored of asArray40(validations)) {
|
|
7621
8074
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
7622
8075
|
walked.push({
|
|
7623
8076
|
rule,
|
|
@@ -7648,7 +8101,7 @@ function validateRuleCompilability(stack) {
|
|
|
7648
8101
|
});
|
|
7649
8102
|
}
|
|
7650
8103
|
}
|
|
7651
|
-
if (rule.type === "json_schema" &&
|
|
8104
|
+
if (rule.type === "json_schema" && isRec23(rule.schema)) {
|
|
7652
8105
|
try {
|
|
7653
8106
|
createRuntimeAjv().compile(rule.schema);
|
|
7654
8107
|
} catch (err) {
|
|
@@ -7668,7 +8121,7 @@ function validateRuleCompilability(stack) {
|
|
|
7668
8121
|
|
|
7669
8122
|
// src/validate-rule-schema-formats.ts
|
|
7670
8123
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7671
|
-
var
|
|
8124
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7672
8125
|
var SUBSCHEMA_KEYS = [
|
|
7673
8126
|
"additionalItems",
|
|
7674
8127
|
"additionalProperties",
|
|
@@ -7692,7 +8145,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7692
8145
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7693
8146
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7694
8147
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7695
|
-
if (!
|
|
8148
|
+
if (!isRec24(schema)) return;
|
|
7696
8149
|
if (typeof schema.format === "string") {
|
|
7697
8150
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7698
8151
|
}
|
|
@@ -7711,7 +8164,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7711
8164
|
}
|
|
7712
8165
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7713
8166
|
const value = schema[key];
|
|
7714
|
-
if (!
|
|
8167
|
+
if (!isRec24(value)) continue;
|
|
7715
8168
|
for (const [name, entry] of Object.entries(value)) {
|
|
7716
8169
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7717
8170
|
}
|
|
@@ -7719,13 +8172,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7719
8172
|
const items = schema.items;
|
|
7720
8173
|
if (Array.isArray(items)) {
|
|
7721
8174
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7722
|
-
} else if (
|
|
8175
|
+
} else if (isRec24(items)) {
|
|
7723
8176
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7724
8177
|
}
|
|
7725
8178
|
const dependencies = schema.dependencies;
|
|
7726
|
-
if (
|
|
8179
|
+
if (isRec24(dependencies)) {
|
|
7727
8180
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7728
|
-
if (!
|
|
8181
|
+
if (!isRec24(entry)) continue;
|
|
7729
8182
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7730
8183
|
}
|
|
7731
8184
|
}
|
|
@@ -7764,7 +8217,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7764
8217
|
const findings = [];
|
|
7765
8218
|
const pending = [];
|
|
7766
8219
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7767
|
-
if (rule.type !== "json_schema" || !
|
|
8220
|
+
if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
|
|
7768
8221
|
const uses = [];
|
|
7769
8222
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7770
8223
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7790,14 +8243,14 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7790
8243
|
|
|
7791
8244
|
// src/validate-action-locations.ts
|
|
7792
8245
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
7793
|
-
function
|
|
8246
|
+
function asArray41(v) {
|
|
7794
8247
|
if (Array.isArray(v)) return v;
|
|
7795
8248
|
if (v && typeof v === "object") {
|
|
7796
8249
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7797
8250
|
}
|
|
7798
8251
|
return [];
|
|
7799
8252
|
}
|
|
7800
|
-
function
|
|
8253
|
+
function strName20(v) {
|
|
7801
8254
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7802
8255
|
}
|
|
7803
8256
|
function strList3(v) {
|
|
@@ -7811,8 +8264,8 @@ function collectNamePlacedActions(stack) {
|
|
|
7811
8264
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
7812
8265
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7813
8266
|
}
|
|
7814
|
-
for (const def of
|
|
7815
|
-
const n =
|
|
8267
|
+
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8268
|
+
const n = strName20(def?.name);
|
|
7816
8269
|
if (n) placed.add(n);
|
|
7817
8270
|
}
|
|
7818
8271
|
};
|
|
@@ -7820,12 +8273,12 @@ function collectNamePlacedActions(stack) {
|
|
|
7820
8273
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
7821
8274
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
7822
8275
|
};
|
|
7823
|
-
for (const view of
|
|
8276
|
+
for (const view of asArray41(stack.views)) {
|
|
7824
8277
|
if (!view || typeof view !== "object") continue;
|
|
7825
8278
|
harvest(view.list);
|
|
7826
8279
|
harvestListViews(view.listViews);
|
|
7827
8280
|
}
|
|
7828
|
-
for (const obj of
|
|
8281
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7829
8282
|
if (!obj || typeof obj !== "object") continue;
|
|
7830
8283
|
harvestListViews(obj.listViews);
|
|
7831
8284
|
}
|
|
@@ -7838,7 +8291,7 @@ function validateActionLocations(stack) {
|
|
|
7838
8291
|
const check = (action, path) => {
|
|
7839
8292
|
if (!action || typeof action !== "object") return;
|
|
7840
8293
|
if ("locations" in action) return;
|
|
7841
|
-
const name =
|
|
8294
|
+
const name = strName20(action.name);
|
|
7842
8295
|
if (!name) return;
|
|
7843
8296
|
if (namePlaced.has(name)) return;
|
|
7844
8297
|
findings.push({
|
|
@@ -7850,13 +8303,13 @@ function validateActionLocations(stack) {
|
|
|
7850
8303
|
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."
|
|
7851
8304
|
});
|
|
7852
8305
|
};
|
|
7853
|
-
const actions =
|
|
8306
|
+
const actions = asArray41(stack.actions);
|
|
7854
8307
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
7855
|
-
const objects =
|
|
8308
|
+
const objects = asArray41(stack.objects);
|
|
7856
8309
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7857
8310
|
const obj = objects[oi];
|
|
7858
8311
|
if (!obj || typeof obj !== "object") continue;
|
|
7859
|
-
const own =
|
|
8312
|
+
const own = asArray41(obj.actions);
|
|
7860
8313
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
7861
8314
|
}
|
|
7862
8315
|
return findings;
|
|
@@ -7864,8 +8317,8 @@ function validateActionLocations(stack) {
|
|
|
7864
8317
|
|
|
7865
8318
|
// src/lint-flow-patterns.ts
|
|
7866
8319
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
7867
|
-
var
|
|
7868
|
-
function
|
|
8320
|
+
var import_data10 = require("@objectstack/spec/data");
|
|
8321
|
+
function asArray42(v) {
|
|
7869
8322
|
if (Array.isArray(v)) return v;
|
|
7870
8323
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7871
8324
|
return [];
|
|
@@ -8139,7 +8592,7 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
8139
8592
|
function filterCarriesNoCondition(filter) {
|
|
8140
8593
|
if (filter === void 0 || filter === null) return true;
|
|
8141
8594
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
8142
|
-
return (0,
|
|
8595
|
+
return (0, import_data10.reduceFilterVerdict)(filter) === "true";
|
|
8143
8596
|
}
|
|
8144
8597
|
function describeUnboundedFilter(filter) {
|
|
8145
8598
|
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
@@ -8249,7 +8702,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
8249
8702
|
}
|
|
8250
8703
|
function lintFlowPatterns(stack) {
|
|
8251
8704
|
const findings = [];
|
|
8252
|
-
for (const flow of
|
|
8705
|
+
for (const flow of asArray42(stack.flows)) {
|
|
8253
8706
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
8254
8707
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
8255
8708
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -8347,7 +8800,7 @@ var import_node_fs = require("fs");
|
|
|
8347
8800
|
var import_meta5 = {};
|
|
8348
8801
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
8349
8802
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
8350
|
-
function
|
|
8803
|
+
function asArray43(v) {
|
|
8351
8804
|
if (Array.isArray(v)) return v;
|
|
8352
8805
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8353
8806
|
return [];
|
|
@@ -8484,11 +8937,11 @@ function lintLivenessProperties(stack) {
|
|
|
8484
8937
|
const findings = [];
|
|
8485
8938
|
const objectWarn = loadWarnMap(dir, "object");
|
|
8486
8939
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
8487
|
-
for (const obj of
|
|
8940
|
+
for (const obj of asArray43(stack.objects)) {
|
|
8488
8941
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8489
8942
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
8490
8943
|
if (fieldWarn.size > 0) {
|
|
8491
|
-
for (const field of
|
|
8944
|
+
for (const field of asArray43(obj.fields)) {
|
|
8492
8945
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
8493
8946
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
8494
8947
|
}
|
|
@@ -8497,7 +8950,7 @@ function lintLivenessProperties(stack) {
|
|
|
8497
8950
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
8498
8951
|
const warnMap = loadWarnMap(dir, type);
|
|
8499
8952
|
if (warnMap.size === 0) continue;
|
|
8500
|
-
for (const item of
|
|
8953
|
+
for (const item of asArray43(stack[key])) {
|
|
8501
8954
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
8502
8955
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
8503
8956
|
}
|
|
@@ -8506,12 +8959,12 @@ function lintLivenessProperties(stack) {
|
|
|
8506
8959
|
}
|
|
8507
8960
|
|
|
8508
8961
|
// src/lint-autonumber-formats.ts
|
|
8509
|
-
var
|
|
8962
|
+
var import_data11 = require("@objectstack/spec/data");
|
|
8510
8963
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
8511
8964
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
8512
8965
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
8513
8966
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
8514
|
-
function
|
|
8967
|
+
function asArray44(v) {
|
|
8515
8968
|
if (Array.isArray(v)) return v;
|
|
8516
8969
|
if (v && typeof v === "object") {
|
|
8517
8970
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -8520,9 +8973,9 @@ function asArray43(v) {
|
|
|
8520
8973
|
}
|
|
8521
8974
|
function lintAutonumberFormats(stack) {
|
|
8522
8975
|
const findings = [];
|
|
8523
|
-
for (const obj of
|
|
8976
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8524
8977
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8525
|
-
const fields =
|
|
8978
|
+
const fields = asArray44(obj.fields);
|
|
8526
8979
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
8527
8980
|
for (const f of fields) {
|
|
8528
8981
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -8532,8 +8985,8 @@ function lintAutonumberFormats(stack) {
|
|
|
8532
8985
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
8533
8986
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
8534
8987
|
if (!fmt) continue;
|
|
8535
|
-
const tokens = (0,
|
|
8536
|
-
const refs = (0,
|
|
8988
|
+
const tokens = (0, import_data11.parseAutonumberFormat)(fmt);
|
|
8989
|
+
const refs = (0, import_data11.referencedFields)(tokens);
|
|
8537
8990
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
8538
8991
|
for (const t of tokens) {
|
|
8539
8992
|
if (t.kind !== "literal") continue;
|
|
@@ -8588,7 +9041,7 @@ function lintAutonumberFormats(stack) {
|
|
|
8588
9041
|
|
|
8589
9042
|
// src/lint-view-refs.ts
|
|
8590
9043
|
var import_spec5 = require("@objectstack/spec");
|
|
8591
|
-
function
|
|
9044
|
+
function asArray45(v) {
|
|
8592
9045
|
if (Array.isArray(v)) return v;
|
|
8593
9046
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8594
9047
|
return [];
|
|
@@ -8616,7 +9069,7 @@ function lintViewRefs(stack) {
|
|
|
8616
9069
|
s.add(kind);
|
|
8617
9070
|
};
|
|
8618
9071
|
const containers = [];
|
|
8619
|
-
for (const v of
|
|
9072
|
+
for (const v of asArray45(stack.views)) {
|
|
8620
9073
|
if (v.viewKind) {
|
|
8621
9074
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
8622
9075
|
continue;
|
|
@@ -8625,7 +9078,7 @@ function lintViewRefs(stack) {
|
|
|
8625
9078
|
const object = viewContainerObjectName(v);
|
|
8626
9079
|
if (object) containers.push({ object, container: v });
|
|
8627
9080
|
}
|
|
8628
|
-
for (const obj of
|
|
9081
|
+
for (const obj of asArray45(stack.objects)) {
|
|
8629
9082
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8630
9083
|
if (!object) continue;
|
|
8631
9084
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -8679,11 +9132,11 @@ function lintViewRefs(stack) {
|
|
|
8679
9132
|
});
|
|
8680
9133
|
}
|
|
8681
9134
|
};
|
|
8682
|
-
for (const obj of
|
|
9135
|
+
for (const obj of asArray45(stack.objects)) {
|
|
8683
9136
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8684
|
-
for (const action of
|
|
9137
|
+
for (const action of asArray45(obj.actions)) checkAction(action, object);
|
|
8685
9138
|
}
|
|
8686
|
-
for (const action of
|
|
9139
|
+
for (const action of asArray45(stack.actions)) checkAction(action);
|
|
8687
9140
|
return findings;
|
|
8688
9141
|
}
|
|
8689
9142
|
|
|
@@ -8818,7 +9271,6 @@ var CLI_ONLY = ["cli"];
|
|
|
8818
9271
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8819
9272
|
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.";
|
|
8820
9273
|
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.";
|
|
8821
|
-
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.";
|
|
8822
9274
|
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.";
|
|
8823
9275
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8824
9276
|
var AUTHORING_RULES = [
|
|
@@ -8884,6 +9336,31 @@ var AUTHORING_RULES = [
|
|
|
8884
9336
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8885
9337
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8886
9338
|
},
|
|
9339
|
+
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
9340
|
+
// in `enable.apiMethods` that its own resolved affordances refuse. Every key
|
|
9341
|
+
// is one we know and each is individually valid, so #4001's unknown-key
|
|
9342
|
+
// rejection and the Zod parse both pass it; the contradiction is only visible
|
|
9343
|
+
// when the two keys are read TOGETHER, which nothing did at authoring time.
|
|
9344
|
+
//
|
|
9345
|
+
// `gating` because the declaration is already false when it ships: objectql's
|
|
9346
|
+
// registry strips the verb at registration, so the metadata advertises an API
|
|
9347
|
+
// the product does not serve. That strip has been correct and silent — a
|
|
9348
|
+
// `console.warn` on every control-plane boot that went unread for the life of
|
|
9349
|
+
// a real divergence (`sys_environment`/`sys_package`). This entry is the
|
|
9350
|
+
// ruling's "close it where the author is"; boot stays warn-and-strip.
|
|
9351
|
+
//
|
|
9352
|
+
// Pre-parse: the predicate reads only authored keys, and the finding must
|
|
9353
|
+
// survive an unrelated schema error elsewhere in the stack.
|
|
9354
|
+
{
|
|
9355
|
+
name: "validateManagedApiMethods",
|
|
9356
|
+
tier: "gating",
|
|
9357
|
+
input: "normalized",
|
|
9358
|
+
commands: ALL,
|
|
9359
|
+
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9360
|
+
surfaces: CLI_ONLY,
|
|
9361
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9362
|
+
run: (stack) => validateManagedApiMethods(stack)
|
|
9363
|
+
},
|
|
8887
9364
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
8888
9365
|
// in the Console, and the schema step cannot tell it from an intentionally
|
|
8889
9366
|
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
@@ -9263,14 +9740,47 @@ var AUTHORING_RULES = [
|
|
|
9263
9740
|
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9264
9741
|
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9265
9742
|
// that gains an `error` finding moves its registry tier in the same edit.
|
|
9743
|
+
//
|
|
9744
|
+
// ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
|
|
9745
|
+
//
|
|
9746
|
+
// This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
|
|
9747
|
+
// in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
|
|
9748
|
+
// `advisories` channel landed (PR #7435). Before that move a `view` written
|
|
9749
|
+
// through Studio / REST `/meta` / MCP — the only door most tenants have, and
|
|
9750
|
+
// the door AI authors use — was judged by NONE of the family's rule ids (six
|
|
9751
|
+
// at the time of the move; seven since #7659 added
|
|
9752
|
+
// `predicate-rhs-path-shaped` inside the second entry).
|
|
9753
|
+
//
|
|
9754
|
+
// They move together on purpose, and the two entries carry one comment because
|
|
9755
|
+
// they are one wall: #7214's implementer wired its own rule here alone and then
|
|
9756
|
+
// REVERTED it, because a `view` refused for an unresolvable predicate PATH
|
|
9757
|
+
// while a predicate that does not parse at all walks through the same door is
|
|
9758
|
+
// less predictable than refusing neither. A half-wired wall is worse than an
|
|
9759
|
+
// unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
|
|
9760
|
+
// directly: every id on this surface is gated at the runtime door, or none is.
|
|
9761
|
+
//
|
|
9762
|
+
// The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
|
|
9763
|
+
// and re-measuring it at move time found it false: both rule functions read
|
|
9764
|
+
// `stack.views` and `stack.pages` and NO other collection — never `objects` —
|
|
9765
|
+
// so the per-write snapshot the gate builds is not partial for them, it is
|
|
9766
|
+
// complete. (`pages` is simply absent on a `view` write, so the page half
|
|
9767
|
+
// contributes zero findings to both differential passes rather than inventing
|
|
9768
|
+
// any.) The reason was not describing this rule; it was the default a rule got
|
|
9769
|
+
// when nobody measured, which is the #4409/#4463 defect one layer in.
|
|
9770
|
+
//
|
|
9771
|
+
// Runtime input tier: the gate hands the rules the body as persisted, without
|
|
9772
|
+
// `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
|
|
9773
|
+
// door. That costs the family nothing — `validateVisibilityPredicates` reads
|
|
9774
|
+
// `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
|
|
9775
|
+
// so a caller handing it a raw authored object still gets a verdict.
|
|
9266
9776
|
{
|
|
9267
9777
|
name: "validateVisibilityPredicates",
|
|
9268
9778
|
tier: "gating",
|
|
9269
9779
|
input: "normalized",
|
|
9270
9780
|
commands: ALL,
|
|
9271
9781
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
9272
|
-
surfaces:
|
|
9273
|
-
|
|
9782
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9783
|
+
runtimeTypes: ["view"],
|
|
9274
9784
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
9275
9785
|
},
|
|
9276
9786
|
// #7010 — the same predicate surface, one question further in. The three
|
|
@@ -9287,14 +9797,28 @@ var AUTHORING_RULES = [
|
|
|
9287
9797
|
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9288
9798
|
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9289
9799
|
// false build errors. See the rule's module note.
|
|
9800
|
+
//
|
|
9801
|
+
// #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
|
|
9802
|
+
// resolution question at all: the metadata-admin renderer resolves paths only
|
|
9803
|
+
// on the LEFT of `==` / `!=` and hands the right side to its literal parser,
|
|
9804
|
+
// so `data.a == data.b` resolves both sides cleanly, passes the two rules
|
|
9805
|
+
// above, and still compares against the string "data.b" — a constant verdict.
|
|
9806
|
+
// It carries `error` on a dotted chain (no reading under which it worked) and
|
|
9807
|
+
// `warning` on a bare word (`status == active` compares as the text today, so
|
|
9808
|
+
// refusing it would fail a build over metadata that renders correctly). The
|
|
9809
|
+
// per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
|
|
9810
|
+
// since #3760; the entry's `gating` tier is unchanged because it already was.
|
|
9290
9811
|
{
|
|
9291
9812
|
name: "validatePredicatePathRefs",
|
|
9292
9813
|
tier: "gating",
|
|
9293
9814
|
input: "normalized",
|
|
9294
9815
|
commands: ALL,
|
|
9295
9816
|
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9296
|
-
|
|
9297
|
-
|
|
9817
|
+
// The second half of the #7220 family move — see the block above the
|
|
9818
|
+
// `validateVisibilityPredicates` entry. This is the rule whose solo wiring
|
|
9819
|
+
// was reverted; it is wired now because its siblings are.
|
|
9820
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9821
|
+
runtimeTypes: ["view"],
|
|
9298
9822
|
run: (stack) => validatePredicatePathRefs(stack)
|
|
9299
9823
|
},
|
|
9300
9824
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
@@ -9454,16 +9978,110 @@ var AUTHORING_RULES = [
|
|
|
9454
9978
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
9455
9979
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
9456
9980
|
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
9981
|
+
//
|
|
9982
|
+
// [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
|
|
9983
|
+
// was false in both halves — it read: "Already gated at this surface by a
|
|
9984
|
+
// DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
|
|
9985
|
+
// `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
|
|
9986
|
+
// on every runtime write. Running the linter here as well would double-report
|
|
9987
|
+
// one refusal in two vocabularies."
|
|
9988
|
+
//
|
|
9989
|
+
// - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
|
|
9990
|
+
// `externalSharingModel` through a local `OWD_WIDTH`, and never touches
|
|
9991
|
+
// `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
|
|
9992
|
+
// block carries it covers ONE — `security-external-wider-than-internal`
|
|
9993
|
+
// (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
|
|
9994
|
+
// corresponds to no lint rule at all, so it is not coverage in the other
|
|
9995
|
+
// direction either. Twelve rules were enforced at no runtime door while
|
|
9996
|
+
// this field said they were.
|
|
9997
|
+
// - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
|
|
9998
|
+
// `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
|
|
9999
|
+
// `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
|
|
10000
|
+
// both refuse by THROWING. The first to fire ends the write, so an author
|
|
10001
|
+
// sees one refusal, never two. The stated cost of moving was imaginary; the
|
|
10002
|
+
// reason it has not moved is the measured one below.
|
|
10003
|
+
//
|
|
10004
|
+
// The move IS taken now — the #7891 programme's three slices, in order:
|
|
10005
|
+
//
|
|
10006
|
+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
|
|
10007
|
+
// the isolation proof that the differential cancels every finding this
|
|
10008
|
+
// function derives from the sibling collections.
|
|
10009
|
+
// - #8309: the snapshot repair. The gate used to carry `objects` and
|
|
10010
|
+
// nothing else, so the three cross-collection rules judged a universe
|
|
10011
|
+
// missing the collection they compare against (measured: 38 phantom
|
|
10012
|
+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
|
|
10013
|
+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
|
|
10014
|
+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
|
|
10015
|
+
// - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
|
|
10016
|
+
// `object` measured DIRTY on that tree and was escalated, not forced.
|
|
10017
|
+
// - #8310 slice 2 (this state): `object` crosses under the maintainer
|
|
10018
|
+
// ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
|
|
10019
|
+
// authored OWD is REQUIRED at the runtime object door — an object
|
|
10020
|
+
// publish with no authored `sharingModel` is refused with the 422 lint
|
|
10021
|
+
// envelope (`security-owd-unset`); absence is not a decision. The ~16
|
|
10022
|
+
// objectql/rest suite files that relied on OWD-less publishes were
|
|
10023
|
+
// repaired honestly (fixtures author their posture), and
|
|
10024
|
+
// `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
|
|
10025
|
+
// answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
|
|
10026
|
+
// ADR-0094-seam 403 doors answer for what passes lint. The same ruling
|
|
10027
|
+
// retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
|
|
10028
|
+
// of this door (R1 env-tighten-only STAYS — no lint rule covers it);
|
|
10029
|
+
// see `object-posture-gate.ts` and the ADR-0094 amendment.
|
|
10030
|
+
//
|
|
10031
|
+
// `security-role-word` is NOT in this entry any more — that is what the
|
|
10032
|
+
// `validateSecurityRoleWord` entry below records. It judges six collections
|
|
10033
|
+
// (objects, fields, actions, permission sets, positions, apps — plus books),
|
|
10034
|
+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
|
|
10035
|
+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
|
|
10036
|
+
// function that still contained it would have enforced ONE rule id for a
|
|
10037
|
+
// strict subset of its collections: a door where a permission set named
|
|
10038
|
+
// `role_manager` is refused and a position named `sales_role` walks through
|
|
10039
|
+
// — the #7220 failure this table refuses to build, in either direction. The
|
|
10040
|
+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
|
|
10041
|
+
// entry.
|
|
10042
|
+
//
|
|
10043
|
+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
|
|
10044
|
+
// registration, not a per-rule split: the baseline/candidate differential is
|
|
10045
|
+
// what keeps a write of one declared type from leaking the other rules'
|
|
10046
|
+
// whole-stack findings — every finding derived from a sibling collection is
|
|
10047
|
+
// produced byte-identically in both passes and cancels in the diff. Only
|
|
10048
|
+
// findings the written item itself adds are attributed to the write.
|
|
9457
10049
|
{
|
|
9458
10050
|
name: "validateSecurityPosture",
|
|
9459
10051
|
tier: "gating",
|
|
9460
10052
|
input: "parsed",
|
|
9461
10053
|
commands: ALL,
|
|
9462
10054
|
source: "packages/lint/src/validate-security-posture.ts",
|
|
9463
|
-
surfaces:
|
|
9464
|
-
|
|
10055
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10056
|
+
runtimeTypes: ["seed", "permission", "book", "object"],
|
|
9465
10057
|
run: (stack) => validateSecurityPosture(stack)
|
|
9466
10058
|
},
|
|
10059
|
+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
|
|
10060
|
+
// `validateSecurityPosture` the day the rest of that block crossed the
|
|
10061
|
+
// runtime wall — so that it could stay behind WHOLE rather than cross for
|
|
10062
|
+
// three of the six collections it judges (#7220: one rule id must sit on ONE
|
|
10063
|
+
// side of the wall). The split is a surface boundary, not taste: the rule's
|
|
10064
|
+
// verdict and findings are byte-identical to before on every CLI command
|
|
10065
|
+
// (both entries run on all three), and the runtime door does not run it for
|
|
10066
|
+
// ANY type.
|
|
10067
|
+
//
|
|
10068
|
+
// The road to crossing is concrete and short, recorded here so the next
|
|
10069
|
+
// seat prices it correctly: carry `positions`/`apps` in
|
|
10070
|
+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
|
|
10071
|
+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
|
|
10072
|
+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
|
|
10073
|
+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
|
|
10074
|
+
// #7220 discipline satisfied.
|
|
10075
|
+
{
|
|
10076
|
+
name: "validateSecurityRoleWord",
|
|
10077
|
+
tier: "gating",
|
|
10078
|
+
input: "parsed",
|
|
10079
|
+
commands: ALL,
|
|
10080
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
10081
|
+
surfaces: CLI_ONLY,
|
|
10082
|
+
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.",
|
|
10083
|
+
run: (stack) => validateSecurityRoleWord(stack)
|
|
10084
|
+
},
|
|
9467
10085
|
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
9468
10086
|
// sharing rule that walks it builds a second permission hierarchy (the
|
|
9469
10087
|
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
@@ -9514,7 +10132,7 @@ var AUTHORING_RULES = [
|
|
|
9514
10132
|
commands: ALL,
|
|
9515
10133
|
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
9516
10134
|
surfaces: CLI_ONLY,
|
|
9517
|
-
surfaceReason: "
|
|
10135
|
+
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.",
|
|
9518
10136
|
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
9519
10137
|
},
|
|
9520
10138
|
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
@@ -9572,8 +10190,34 @@ var TYPE_TO_STACK_KEY = {
|
|
|
9572
10190
|
dashboard: "dashboards",
|
|
9573
10191
|
agent: "agents",
|
|
9574
10192
|
hook: "hooks",
|
|
9575
|
-
seed
|
|
10193
|
+
// [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
|
|
10194
|
+
// holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
|
|
10195
|
+
// — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
|
|
10196
|
+
// mapping onto one either.
|
|
10197
|
+
//
|
|
10198
|
+
// The wrong spelling was INERT rather than harmless, and it is the #4449 shape
|
|
10199
|
+
// one surface over: the wiring guard asks only that a declared type HAS a
|
|
10200
|
+
// mapping, never that the mapping names a key some rule reads. So it would
|
|
10201
|
+
// have stayed green while the gate built `{ objects, seeds: [item] }` for
|
|
10202
|
+
// every seed write and every rule reading `stack.data` saw nothing — wired,
|
|
10203
|
+
// and running on nothing, with `rulesRun` reporting the rules as having run.
|
|
10204
|
+
// Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
|
|
10205
|
+
// changed no behaviour then; it was corrected here, with the measurement
|
|
10206
|
+
// that found it (#7576), rather than left for the rollout card to trip
|
|
10207
|
+
// over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
|
|
10208
|
+
// mapping is load-bearing today, not merely inert-and-correct.
|
|
10209
|
+
seed: "data",
|
|
10210
|
+
// [#8309] `permission`/`book` map ahead of their registration (#8310), the
|
|
10211
|
+
// same order `seed` arrived in: the mapping plus the enriched snapshot below
|
|
10212
|
+
// are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
|
|
10213
|
+
// a mapping without a declaring rule is inert by construction (the gate
|
|
10214
|
+
// filters by `runtimeTypes` before it ever consults this table), while a
|
|
10215
|
+
// declaration without the mapping is the wired-onto-nothing state the wiring
|
|
10216
|
+
// guard refuses. Landing the mapping first keeps #8310 a registry data edit.
|
|
10217
|
+
permission: "permissions",
|
|
10218
|
+
book: "books"
|
|
9576
10219
|
};
|
|
10220
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
|
|
9577
10221
|
function runtimeAuthoringRulesFor(type) {
|
|
9578
10222
|
return AUTHORING_RULES.filter(
|
|
9579
10223
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -9591,6 +10235,23 @@ function stackKeyForType(type) {
|
|
|
9591
10235
|
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
9592
10236
|
}
|
|
9593
10237
|
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
10238
|
+
function buildRuntimeWriteSnapshots(args) {
|
|
10239
|
+
const stackKey = stackKeyForType(args.type);
|
|
10240
|
+
if (!stackKey) return null;
|
|
10241
|
+
if (!args.item || typeof args.item !== "object") return null;
|
|
10242
|
+
const item = args.item;
|
|
10243
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10244
|
+
const baseline = {};
|
|
10245
|
+
for (const key of CONTEXT_STACK_KEYS) {
|
|
10246
|
+
const collection = args.context?.[key] ?? [];
|
|
10247
|
+
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10248
|
+
}
|
|
10249
|
+
const candidate = {
|
|
10250
|
+
...baseline,
|
|
10251
|
+
[stackKey]: [...baseline[stackKey] ?? [], item]
|
|
10252
|
+
};
|
|
10253
|
+
return { baseline, candidate };
|
|
10254
|
+
}
|
|
9594
10255
|
function runRules(rules, stack, ctx) {
|
|
9595
10256
|
const findings = [];
|
|
9596
10257
|
for (const rule of rules) {
|
|
@@ -9613,19 +10274,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9613
10274
|
const rules = runtimeAuthoringRulesFor(args.type);
|
|
9614
10275
|
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
9615
10276
|
if (rules.length === 0) return empty;
|
|
9616
|
-
const
|
|
9617
|
-
|
|
9618
|
-
|
|
9619
|
-
|
|
9620
|
-
|
|
9621
|
-
|
|
10277
|
+
const snapshots = buildRuntimeWriteSnapshots({
|
|
10278
|
+
type: args.type,
|
|
10279
|
+
item: args.item,
|
|
10280
|
+
...args.context !== void 0 ? { context: args.context } : {}
|
|
10281
|
+
});
|
|
10282
|
+
if (!snapshots) return empty;
|
|
9622
10283
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
9623
|
-
const
|
|
9624
|
-
const
|
|
9625
|
-
const baseline = { objects: baselineObjects };
|
|
9626
|
-
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
9627
|
-
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
9628
|
-
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
10284
|
+
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10285
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
9629
10286
|
return {
|
|
9630
10287
|
errors: added.filter((f) => f.severity === "error"),
|
|
9631
10288
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -9634,6 +10291,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9634
10291
|
}
|
|
9635
10292
|
// Annotate the CommonJS export names for ESM import in node:
|
|
9636
10293
|
0 && (module.exports = {
|
|
10294
|
+
buildRuntimeWriteSnapshots,
|
|
9637
10295
|
runRuntimeAuthoringRules,
|
|
9638
10296
|
runtimeAuthoringRulesFor,
|
|
9639
10297
|
runtimeGatedTypes,
|