@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.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// src/validate-expressions.ts
|
|
2
|
-
import { validateExpression, collectCelRootIdentifiers, SCOPE_ROOTS } from "@objectstack/formula";
|
|
2
|
+
import { validateExpression, collectCelRootIdentifiers, parseCelToAst as parseCelToAst2, SCOPE_ROOTS } from "@objectstack/formula";
|
|
3
3
|
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
4
4
|
|
|
5
5
|
// src/system-fields.ts
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
FIELD_GROUP_SYSTEM_FIELDS,
|
|
8
|
+
resolveInjectedSystemColumns,
|
|
9
|
+
unprovisionedInjectedColumns
|
|
10
|
+
} from "@objectstack/spec/data";
|
|
7
11
|
import { SystemFieldName } from "@objectstack/spec/system";
|
|
8
12
|
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
9
13
|
...FIELD_GROUP_SYSTEM_FIELDS,
|
|
@@ -12,6 +16,34 @@ var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
|
12
16
|
function injectedColumnsFor(objectDef) {
|
|
13
17
|
return resolveInjectedSystemColumns(objectDef).names;
|
|
14
18
|
}
|
|
19
|
+
function unprovisionedInjectedColumnsFor(objectDef) {
|
|
20
|
+
return new Set(unprovisionedInjectedColumns(objectDef));
|
|
21
|
+
}
|
|
22
|
+
function objectDefsOf(stack) {
|
|
23
|
+
if (!stack || typeof stack !== "object") return [];
|
|
24
|
+
const objects = stack.objects;
|
|
25
|
+
if (Array.isArray(objects)) return objects.filter((o) => !!o && typeof o === "object");
|
|
26
|
+
if (objects && typeof objects === "object") {
|
|
27
|
+
return Object.entries(objects).filter(([, def]) => !!def && typeof def === "object").map(([name, def]) => ({ name, ...def }));
|
|
28
|
+
}
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
function indexUnprovisionedAnchors(stack) {
|
|
32
|
+
const index = /* @__PURE__ */ new Map();
|
|
33
|
+
for (const obj of objectDefsOf(stack)) {
|
|
34
|
+
const name = typeof obj.name === "string" && obj.name.length > 0 ? obj.name : void 0;
|
|
35
|
+
if (!name) continue;
|
|
36
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
37
|
+
if (anchors.size > 0) index.set(name, anchors);
|
|
38
|
+
}
|
|
39
|
+
return index;
|
|
40
|
+
}
|
|
41
|
+
function unprovisionedAnchorCause(objectName, field) {
|
|
42
|
+
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`;
|
|
43
|
+
}
|
|
44
|
+
function unprovisionedAnchorHint(objectName, field) {
|
|
45
|
+
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).`;
|
|
46
|
+
}
|
|
15
47
|
|
|
16
48
|
// src/validate-null-guards.ts
|
|
17
49
|
import { parseCelToAst } from "@objectstack/formula";
|
|
@@ -245,6 +277,37 @@ function buildFieldIndex(objects) {
|
|
|
245
277
|
}
|
|
246
278
|
return idx;
|
|
247
279
|
}
|
|
280
|
+
var BOUND_RECORD_ROOTS = ["record", "previous"];
|
|
281
|
+
function isCelNode(v) {
|
|
282
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
283
|
+
}
|
|
284
|
+
function collectBoundRecordReads(source) {
|
|
285
|
+
const out = /* @__PURE__ */ new Map();
|
|
286
|
+
const ast = parseCelToAst2(source);
|
|
287
|
+
if (!ast) return out;
|
|
288
|
+
const pending = [ast];
|
|
289
|
+
while (pending.length > 0) {
|
|
290
|
+
const celNode = pending.pop();
|
|
291
|
+
if (!isCelNode(celNode)) continue;
|
|
292
|
+
if ((celNode.op === "." || celNode.op === ".?") && Array.isArray(celNode.args) && celNode.args.length >= 2) {
|
|
293
|
+
const [celRecv, seg] = celNode.args;
|
|
294
|
+
if (typeof seg === "string" && isCelNode(celRecv) && celRecv.op === "id" && typeof celRecv.args === "string" && BOUND_RECORD_ROOTS.includes(celRecv.args)) {
|
|
295
|
+
if (!out.has(seg)) out.set(seg, `${celRecv.args}.${seg}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const celArgs = celNode.args;
|
|
299
|
+
if (isCelNode(celArgs)) pending.push(celArgs);
|
|
300
|
+
else if (Array.isArray(celArgs)) {
|
|
301
|
+
for (const a of celArgs) {
|
|
302
|
+
if (isCelNode(a)) pending.push(a);
|
|
303
|
+
else if (Array.isArray(a)) {
|
|
304
|
+
for (const b of a) if (isCelNode(b)) pending.push(b);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
248
311
|
function buildFieldTypeIndex(objects) {
|
|
249
312
|
const idx = /* @__PURE__ */ new Map();
|
|
250
313
|
for (const obj of objects) {
|
|
@@ -344,6 +407,29 @@ function validateStackExpressions(stack) {
|
|
|
344
407
|
const fieldIndex = buildFieldIndex(objects);
|
|
345
408
|
const fieldTypeIndex = buildFieldTypeIndex(objects);
|
|
346
409
|
const nullableIndex = buildNullableFieldIndex(objects);
|
|
410
|
+
const unprovisionedIndex = /* @__PURE__ */ new Map();
|
|
411
|
+
for (const obj of objects) {
|
|
412
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
413
|
+
if (!name) continue;
|
|
414
|
+
const anchors = unprovisionedInjectedColumnsFor(obj);
|
|
415
|
+
if (anchors.size > 0) unprovisionedIndex.set(name, anchors);
|
|
416
|
+
}
|
|
417
|
+
const warnUnprovisionedAnchors = (where, raw, objectName) => {
|
|
418
|
+
if (!objectName) return;
|
|
419
|
+
const anchors = unprovisionedIndex.get(objectName);
|
|
420
|
+
if (!anchors) return;
|
|
421
|
+
const source = celSourceOf(raw);
|
|
422
|
+
if (!source) return;
|
|
423
|
+
for (const [field, operand] of collectBoundRecordReads(source)) {
|
|
424
|
+
if (!anchors.has(field)) continue;
|
|
425
|
+
issues.push({
|
|
426
|
+
where,
|
|
427
|
+
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).`,
|
|
428
|
+
source,
|
|
429
|
+
severity: "warning"
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
};
|
|
347
433
|
const checkNullGuards = (where, subject, raw, objectName, outcome = "fail-closed") => {
|
|
348
434
|
if (!objectName) return;
|
|
349
435
|
const nullableFields = nullableIndex.get(objectName);
|
|
@@ -370,6 +456,7 @@ function validateStackExpressions(stack) {
|
|
|
370
456
|
);
|
|
371
457
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
372
458
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
459
|
+
warnUnprovisionedAnchors(where, raw, objectName);
|
|
373
460
|
};
|
|
374
461
|
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
375
462
|
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
@@ -516,6 +603,7 @@ function validateStackExpressions(stack) {
|
|
|
516
603
|
const fieldWhere = `object '${objectName}' \xB7 field '${fname}' expression`;
|
|
517
604
|
for (const e of res.errors) issues.push({ where: fieldWhere, message: e.message, source: e.source, severity: "error" });
|
|
518
605
|
for (const w of res.warnings) issues.push({ where: fieldWhere, message: w.message, source: w.source, severity: "warning" });
|
|
606
|
+
warnUnprovisionedAnchors(fieldWhere, f.expression, objectName);
|
|
519
607
|
}
|
|
520
608
|
}
|
|
521
609
|
}
|
|
@@ -725,6 +813,46 @@ function validateFunctionalCompleteness(stack) {
|
|
|
725
813
|
return out;
|
|
726
814
|
}
|
|
727
815
|
|
|
816
|
+
// src/validate-managed-api-methods.ts
|
|
817
|
+
import {
|
|
818
|
+
checkManagedApiMethodAffordances,
|
|
819
|
+
describeManagedApiMethodConflicts
|
|
820
|
+
} from "@objectstack/spec/data";
|
|
821
|
+
var MANAGED_API_METHOD_UNAFFORDABLE = "object/managed-api-method-unaffordable";
|
|
822
|
+
var isRec2 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
823
|
+
function entriesOf2(v) {
|
|
824
|
+
if (Array.isArray(v)) {
|
|
825
|
+
return v.flatMap(
|
|
826
|
+
(def, i) => isRec2(def) ? [{ name: String(def.name ?? i), def, key: `[${i}]` }] : []
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
if (isRec2(v)) {
|
|
830
|
+
return Object.entries(v).flatMap(
|
|
831
|
+
([name, def]) => isRec2(def) ? [{ name, def: { name, ...def }, key: `.${name}` }] : []
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
return [];
|
|
835
|
+
}
|
|
836
|
+
function validateManagedApiMethods(stack) {
|
|
837
|
+
const out = [];
|
|
838
|
+
if (!isRec2(stack)) return out;
|
|
839
|
+
for (const [oi, obj] of entriesOf2(stack.objects).entries()) {
|
|
840
|
+
const conflicts = checkManagedApiMethodAffordances(obj.def);
|
|
841
|
+
if (conflicts.length === 0) continue;
|
|
842
|
+
const verbs = conflicts.map((c) => c.verb).join(", ");
|
|
843
|
+
const flags = [...new Set(conflicts.map((c) => c.needs))];
|
|
844
|
+
out.push({
|
|
845
|
+
severity: "error",
|
|
846
|
+
rule: MANAGED_API_METHOD_UNAFFORDABLE,
|
|
847
|
+
where: `object "${obj.name}"`,
|
|
848
|
+
path: `objects[${oi}].enable.apiMethods`,
|
|
849
|
+
message: `\`managedBy: '${String(obj.def.managedBy)}'\` object "${obj.name}" ` + 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.`,
|
|
850
|
+
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.`
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
|
|
728
856
|
// src/validate-view-containers.ts
|
|
729
857
|
var VIEW_CONTAINER_SHAPE = "view-container-shape";
|
|
730
858
|
var CONTAINER_SLOT_KEYS = ["list", "form", "listViews", "formViews"];
|
|
@@ -742,10 +870,32 @@ function containerViewCount(rec) {
|
|
|
742
870
|
function validateViewContainers(stack) {
|
|
743
871
|
const out = [];
|
|
744
872
|
if (!stack || typeof stack !== "object") return out;
|
|
873
|
+
const viewItems = stack.viewItems;
|
|
874
|
+
if (viewItems != null && asEntries(viewItems).length > 0) {
|
|
875
|
+
out.push({
|
|
876
|
+
severity: "error",
|
|
877
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
878
|
+
where: "viewItems",
|
|
879
|
+
path: "viewItems",
|
|
880
|
+
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.",
|
|
881
|
+
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."
|
|
882
|
+
});
|
|
883
|
+
}
|
|
745
884
|
for (const { key, value } of asEntries(stack.views)) {
|
|
746
885
|
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
747
886
|
const rec = value;
|
|
748
|
-
if (rec.viewKind != null)
|
|
887
|
+
if (rec.viewKind != null) {
|
|
888
|
+
const label3 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
889
|
+
out.push({
|
|
890
|
+
severity: "error",
|
|
891
|
+
rule: VIEW_CONTAINER_SHAPE,
|
|
892
|
+
where: `views${key}${label3}`,
|
|
893
|
+
path: `views${key}`,
|
|
894
|
+
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).",
|
|
895
|
+
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:`."
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
749
899
|
if (containerViewCount(rec) > 0) continue;
|
|
750
900
|
const label2 = typeof rec.name === "string" ? ` ("${rec.name}")` : "";
|
|
751
901
|
const hasContainerSlot = CONTAINER_SLOT_KEYS.some((k) => k in rec);
|
|
@@ -775,6 +925,7 @@ var MEASURE_AGGREGATE_INCOHERENT = "measure-aggregate-incoherent";
|
|
|
775
925
|
var WIDGET_LEGACY_ANALYTICS_SHAPE = "widget-legacy-analytics-shape";
|
|
776
926
|
var WIDGET_LEGACY_ANALYTICS_UNRENDERABLE = "widget-legacy-analytics-unrenderable";
|
|
777
927
|
var DASHBOARD_FILTER_FIELD_UNKNOWN = "dashboard-filter-field-unknown";
|
|
928
|
+
var DASHBOARD_FILTER_FIELD_UNPROVISIONED = "dashboard-filter-field-unprovisioned";
|
|
778
929
|
var LEGACY_ANALYTICS_KEYS = [
|
|
779
930
|
"categoryField",
|
|
780
931
|
"valueField",
|
|
@@ -894,6 +1045,7 @@ function validateWidgetBindings(stack) {
|
|
|
894
1045
|
}
|
|
895
1046
|
objectFieldTypes.set(o.name, fm);
|
|
896
1047
|
}
|
|
1048
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
897
1049
|
const datasetList = asArray3(stack.datasets);
|
|
898
1050
|
for (let i = 0; i < datasetList.length; i++) {
|
|
899
1051
|
const ds = datasetList[i];
|
|
@@ -980,13 +1132,24 @@ function validateWidgetBindings(stack) {
|
|
|
980
1132
|
if (dashFilterDefs.length > 0) {
|
|
981
1133
|
const datasetObject = typeof dataset.object === "string" ? dataset.object : void 0;
|
|
982
1134
|
const objectFields = datasetObject ? objectFieldTypes.get(datasetObject) : void 0;
|
|
983
|
-
|
|
1135
|
+
const anchors = datasetObject ? unprovisionedAnchors.get(datasetObject) : void 0;
|
|
1136
|
+
if (objectFields && datasetObject) {
|
|
984
1137
|
for (const def of dashFilterDefs) {
|
|
985
1138
|
const eff = effectiveFilterField(w, def);
|
|
986
1139
|
if (!eff) continue;
|
|
987
1140
|
const field = eff.field;
|
|
988
1141
|
if (field.includes(".")) continue;
|
|
989
|
-
if (objectFields.has(field) || SYSTEM_FIELDS.has(field))
|
|
1142
|
+
if (objectFields.has(field) || SYSTEM_FIELDS.has(field)) {
|
|
1143
|
+
if (anchors?.has(field)) {
|
|
1144
|
+
push2({
|
|
1145
|
+
severity: "warning",
|
|
1146
|
+
rule: DASHBOARD_FILTER_FIELD_UNPROVISIONED,
|
|
1147
|
+
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).`,
|
|
1148
|
+
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.`
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
990
1153
|
push2({
|
|
991
1154
|
severity: "error",
|
|
992
1155
|
rule: DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
@@ -1660,7 +1823,7 @@ function asArray7(v) {
|
|
|
1660
1823
|
}
|
|
1661
1824
|
return [];
|
|
1662
1825
|
}
|
|
1663
|
-
function
|
|
1826
|
+
function isRec3(v) {
|
|
1664
1827
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1665
1828
|
}
|
|
1666
1829
|
function strName3(v) {
|
|
@@ -1736,7 +1899,7 @@ function distance2(a, b) {
|
|
|
1736
1899
|
}
|
|
1737
1900
|
function indexObjectSearchTargets(stack) {
|
|
1738
1901
|
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1739
|
-
if (!
|
|
1902
|
+
if (!isRec3(stack)) return fieldsByObject;
|
|
1740
1903
|
for (const obj of asArray7(stack.objects)) {
|
|
1741
1904
|
const name = strName3(obj.name);
|
|
1742
1905
|
if (name) fieldsByObject.set(name, declaredFieldTarget(obj));
|
|
@@ -1818,7 +1981,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
1818
1981
|
}
|
|
1819
1982
|
function validateSearchableFields(stack) {
|
|
1820
1983
|
const findings = [];
|
|
1821
|
-
if (!
|
|
1984
|
+
if (!isRec3(stack)) return findings;
|
|
1822
1985
|
const objects = asArray7(stack.objects);
|
|
1823
1986
|
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1824
1987
|
const check = (declared, objectName, where, path, subject, role) => {
|
|
@@ -1828,7 +1991,7 @@ function validateSearchableFields(stack) {
|
|
|
1828
1991
|
};
|
|
1829
1992
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
1830
1993
|
const obj = objects[oi];
|
|
1831
|
-
if (!
|
|
1994
|
+
if (!isRec3(obj)) continue;
|
|
1832
1995
|
const objName = strName3(obj.name);
|
|
1833
1996
|
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1834
1997
|
check(
|
|
@@ -1839,9 +2002,9 @@ function validateSearchableFields(stack) {
|
|
|
1839
2002
|
"searchableFields",
|
|
1840
2003
|
"canonical"
|
|
1841
2004
|
);
|
|
1842
|
-
if (
|
|
2005
|
+
if (isRec3(obj.listViews)) {
|
|
1843
2006
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1844
|
-
if (!
|
|
2007
|
+
if (!isRec3(lv)) continue;
|
|
1845
2008
|
check(
|
|
1846
2009
|
lv.searchableFields,
|
|
1847
2010
|
// A built-in list view belongs to its object; an inline `data.object`
|
|
@@ -1858,10 +2021,10 @@ function validateSearchableFields(stack) {
|
|
|
1858
2021
|
const views = asArray7(stack.views);
|
|
1859
2022
|
for (let vi = 0; vi < views.length; vi++) {
|
|
1860
2023
|
const view = views[vi];
|
|
1861
|
-
if (!
|
|
2024
|
+
if (!isRec3(view)) continue;
|
|
1862
2025
|
const viewLabel2 = strName3(view.name) ?? strName3(view.objectName) ?? `#${vi}`;
|
|
1863
2026
|
const viewObject = strName3(view.objectName) ?? strName3(view.object);
|
|
1864
|
-
if (
|
|
2027
|
+
if (isRec3(view.list)) {
|
|
1865
2028
|
check(
|
|
1866
2029
|
view.list.searchableFields,
|
|
1867
2030
|
listViewObject(view.list) ?? viewObject,
|
|
@@ -1871,9 +2034,9 @@ function validateSearchableFields(stack) {
|
|
|
1871
2034
|
"narrowing"
|
|
1872
2035
|
);
|
|
1873
2036
|
}
|
|
1874
|
-
if (
|
|
2037
|
+
if (isRec3(view.listViews)) {
|
|
1875
2038
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1876
|
-
if (!
|
|
2039
|
+
if (!isRec3(lv)) continue;
|
|
1877
2040
|
check(
|
|
1878
2041
|
lv.searchableFields,
|
|
1879
2042
|
listViewObject(lv) ?? viewObject,
|
|
@@ -1889,11 +2052,11 @@ function validateSearchableFields(stack) {
|
|
|
1889
2052
|
}
|
|
1890
2053
|
function listViewObject(listView) {
|
|
1891
2054
|
const data = listView.data;
|
|
1892
|
-
return
|
|
2055
|
+
return isRec3(data) ? strName3(data.object) : void 0;
|
|
1893
2056
|
}
|
|
1894
2057
|
|
|
1895
2058
|
// src/page-walk.ts
|
|
1896
|
-
function
|
|
2059
|
+
function isRec4(v) {
|
|
1897
2060
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1898
2061
|
}
|
|
1899
2062
|
function strName4(v) {
|
|
@@ -1906,19 +2069,19 @@ function isSourceAuthoredPage(page) {
|
|
|
1906
2069
|
}
|
|
1907
2070
|
function walkPageComponents(page, pagePath) {
|
|
1908
2071
|
const out = [];
|
|
1909
|
-
if (!
|
|
2072
|
+
if (!isRec4(page) || isSourceAuthoredPage(page)) return out;
|
|
1910
2073
|
const pageObject = strName4(page.object);
|
|
1911
2074
|
const visit = (node, path, inheritedObject) => {
|
|
1912
|
-
if (!
|
|
1913
|
-
const props =
|
|
1914
|
-
const dataSource =
|
|
2075
|
+
if (!isRec4(node)) return;
|
|
2076
|
+
const props = isRec4(node.properties) ? node.properties : void 0;
|
|
2077
|
+
const dataSource = isRec4(node.dataSource) ? node.dataSource : void 0;
|
|
1915
2078
|
const objectName = strName4(dataSource?.object) ?? strName4(props?.object) ?? inheritedObject;
|
|
1916
2079
|
out.push({ component: node, path, objectName });
|
|
1917
2080
|
if (!props) return;
|
|
1918
2081
|
if (Array.isArray(props.items)) {
|
|
1919
2082
|
for (let i = 0; i < props.items.length; i++) {
|
|
1920
2083
|
const item = props.items[i];
|
|
1921
|
-
if (!
|
|
2084
|
+
if (!isRec4(item) || !Array.isArray(item.children)) continue;
|
|
1922
2085
|
for (let c = 0; c < item.children.length; c++) {
|
|
1923
2086
|
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1924
2087
|
}
|
|
@@ -1940,12 +2103,12 @@ function walkPageComponents(page, pagePath) {
|
|
|
1940
2103
|
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1941
2104
|
for (let r = 0; r < regions.length; r++) {
|
|
1942
2105
|
const region = regions[r];
|
|
1943
|
-
if (!
|
|
2106
|
+
if (!isRec4(region) || !Array.isArray(region.components)) continue;
|
|
1944
2107
|
for (let c = 0; c < region.components.length; c++) {
|
|
1945
2108
|
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1946
2109
|
}
|
|
1947
2110
|
}
|
|
1948
|
-
const slots =
|
|
2111
|
+
const slots = isRec4(page.slots) ? page.slots : void 0;
|
|
1949
2112
|
if (slots) {
|
|
1950
2113
|
for (const [slot, value] of Object.entries(slots)) {
|
|
1951
2114
|
const list3 = Array.isArray(value) ? value : [value];
|
|
@@ -2153,6 +2316,7 @@ function validateActionNameRefs(stack) {
|
|
|
2153
2316
|
|
|
2154
2317
|
// src/validate-page-field-bindings.ts
|
|
2155
2318
|
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
2319
|
+
var PAGE_FIELD_UNPROVISIONED = "page-field-unprovisioned";
|
|
2156
2320
|
function asArray9(v) {
|
|
2157
2321
|
if (Array.isArray(v)) return v;
|
|
2158
2322
|
if (v && typeof v === "object") {
|
|
@@ -2163,7 +2327,7 @@ function asArray9(v) {
|
|
|
2163
2327
|
function strName6(v) {
|
|
2164
2328
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2165
2329
|
}
|
|
2166
|
-
function
|
|
2330
|
+
function isRec5(v) {
|
|
2167
2331
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2168
2332
|
}
|
|
2169
2333
|
function fieldRefsFrom(value, basePath) {
|
|
@@ -2174,7 +2338,7 @@ function fieldRefsFrom(value, basePath) {
|
|
|
2174
2338
|
out.push({ name: bare, path });
|
|
2175
2339
|
return;
|
|
2176
2340
|
}
|
|
2177
|
-
if (!
|
|
2341
|
+
if (!isRec5(v)) return;
|
|
2178
2342
|
const named = strName6(v.field) ?? strName6(v.name);
|
|
2179
2343
|
if (named) out.push({ name: named, path: `${path}.${strName6(v.field) ? "field" : "name"}` });
|
|
2180
2344
|
};
|
|
@@ -2232,15 +2396,15 @@ function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
|
2232
2396
|
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
2233
2397
|
for (let si = 0; si < sections.length; si++) {
|
|
2234
2398
|
const section = sections[si];
|
|
2235
|
-
if (!
|
|
2399
|
+
if (!isRec5(section)) continue;
|
|
2236
2400
|
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
2237
2401
|
}
|
|
2238
2402
|
}
|
|
2239
2403
|
return refs;
|
|
2240
2404
|
}
|
|
2241
2405
|
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
2242
|
-
const add =
|
|
2243
|
-
const picker = add &&
|
|
2406
|
+
const add = isRec5(props.add) ? props.add : void 0;
|
|
2407
|
+
const picker = add && isRec5(add.picker) ? add.picker : void 0;
|
|
2244
2408
|
const at = (key) => `${basePath}${sep}${key}`;
|
|
2245
2409
|
return {
|
|
2246
2410
|
relatedObject: strName6(props.objectName),
|
|
@@ -2261,7 +2425,7 @@ function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
|
2261
2425
|
}
|
|
2262
2426
|
function indexObjectFields(stack) {
|
|
2263
2427
|
const objectFields = /* @__PURE__ */ new Map();
|
|
2264
|
-
if (!
|
|
2428
|
+
if (!isRec5(stack)) return objectFields;
|
|
2265
2429
|
for (const obj of asArray9(stack.objects)) {
|
|
2266
2430
|
const name = strName6(obj.name);
|
|
2267
2431
|
if (!name) continue;
|
|
@@ -2274,14 +2438,27 @@ function indexObjectFields(stack) {
|
|
|
2274
2438
|
}
|
|
2275
2439
|
return objectFields;
|
|
2276
2440
|
}
|
|
2277
|
-
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped") {
|
|
2441
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence2 = "skipped", unprovisionedAnchors) {
|
|
2278
2442
|
const findings = [];
|
|
2279
2443
|
if (!objectName) return findings;
|
|
2280
2444
|
const known = objectFields.get(objectName);
|
|
2281
2445
|
if (!known) return findings;
|
|
2446
|
+
const anchors = unprovisionedAnchors?.get(objectName);
|
|
2282
2447
|
for (const ref of refs) {
|
|
2283
2448
|
if (ref.name.includes(".")) continue;
|
|
2284
|
-
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name))
|
|
2449
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) {
|
|
2450
|
+
if (anchors?.has(ref.name)) {
|
|
2451
|
+
findings.push({
|
|
2452
|
+
severity: "warning",
|
|
2453
|
+
rule: PAGE_FIELD_UNPROVISIONED,
|
|
2454
|
+
where,
|
|
2455
|
+
path: ref.path,
|
|
2456
|
+
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."),
|
|
2457
|
+
hint: unprovisionedAnchorHint(objectName, ref.name)
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
continue;
|
|
2461
|
+
}
|
|
2285
2462
|
findings.push({
|
|
2286
2463
|
severity: consequence2 === "queried" ? "error" : "warning",
|
|
2287
2464
|
rule: PAGE_FIELD_UNKNOWN,
|
|
@@ -2297,17 +2474,20 @@ function validatePageFieldBindings(stack) {
|
|
|
2297
2474
|
const findings = [];
|
|
2298
2475
|
if (!stack || typeof stack !== "object") return findings;
|
|
2299
2476
|
const objectFields = indexObjectFields(stack);
|
|
2477
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
2300
2478
|
const pages = asArray9(stack.pages);
|
|
2301
2479
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2302
2480
|
const page = pages[pi];
|
|
2303
2481
|
if (!page || typeof page !== "object") continue;
|
|
2304
2482
|
const pageName = strName6(page.name) ?? `#${pi}`;
|
|
2305
2483
|
const checkRefs = (refs, objectName, where) => {
|
|
2306
|
-
findings.push(
|
|
2484
|
+
findings.push(
|
|
2485
|
+
...checkFieldRefs(refs, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
2486
|
+
);
|
|
2307
2487
|
};
|
|
2308
2488
|
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2309
2489
|
const type = strName6(component.type);
|
|
2310
|
-
const props =
|
|
2490
|
+
const props = isRec5(component.properties) ? component.properties : void 0;
|
|
2311
2491
|
if (!type || !props) continue;
|
|
2312
2492
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
2313
2493
|
const base = `${path}.properties`;
|
|
@@ -2322,7 +2502,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2322
2502
|
if (!refs) continue;
|
|
2323
2503
|
checkRefs(refs, objectName, where);
|
|
2324
2504
|
}
|
|
2325
|
-
const cfg =
|
|
2505
|
+
const cfg = isRec5(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
2326
2506
|
if (cfg) {
|
|
2327
2507
|
const cfgObject = strName6(cfg.source) ?? strName6(page.object);
|
|
2328
2508
|
const base = `pages[${pi}].interfaceConfig`;
|
|
@@ -2331,7 +2511,7 @@ function validatePageFieldBindings(stack) {
|
|
|
2331
2511
|
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
2332
2512
|
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
2333
2513
|
];
|
|
2334
|
-
const userFilters =
|
|
2514
|
+
const userFilters = isRec5(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
2335
2515
|
if (userFilters) {
|
|
2336
2516
|
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
2337
2517
|
}
|
|
@@ -2359,7 +2539,7 @@ function strName7(v) {
|
|
|
2359
2539
|
function strList2(v) {
|
|
2360
2540
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
2361
2541
|
}
|
|
2362
|
-
function
|
|
2542
|
+
function isRec6(v) {
|
|
2363
2543
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2364
2544
|
}
|
|
2365
2545
|
function distance4(a, b) {
|
|
@@ -2488,10 +2668,10 @@ function validateChartBindings(stack) {
|
|
|
2488
2668
|
const reports = asArray10(stack.reports);
|
|
2489
2669
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
2490
2670
|
const report = reports[ri];
|
|
2491
|
-
if (!
|
|
2671
|
+
if (!isRec6(report)) continue;
|
|
2492
2672
|
const reportName = strName7(report.name) ?? `#${ri}`;
|
|
2493
2673
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
2494
|
-
if (!
|
|
2674
|
+
if (!isRec6(chart)) return;
|
|
2495
2675
|
check({
|
|
2496
2676
|
dataset,
|
|
2497
2677
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
@@ -2515,7 +2695,7 @@ function validateChartBindings(stack) {
|
|
|
2515
2695
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
2516
2696
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
2517
2697
|
const block = blocks[bi];
|
|
2518
|
-
if (!
|
|
2698
|
+
if (!isRec6(block)) continue;
|
|
2519
2699
|
checkReportChart(
|
|
2520
2700
|
block.chart,
|
|
2521
2701
|
strName7(block.dataset),
|
|
@@ -2526,9 +2706,9 @@ function validateChartBindings(stack) {
|
|
|
2526
2706
|
}
|
|
2527
2707
|
}
|
|
2528
2708
|
const checkListChart = (container, where, path) => {
|
|
2529
|
-
if (!
|
|
2709
|
+
if (!isRec6(container)) return;
|
|
2530
2710
|
const chart = container.chart;
|
|
2531
|
-
if (!
|
|
2711
|
+
if (!isRec6(chart)) return;
|
|
2532
2712
|
check({
|
|
2533
2713
|
dataset: strName7(chart.dataset),
|
|
2534
2714
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
@@ -2540,10 +2720,10 @@ function validateChartBindings(stack) {
|
|
|
2540
2720
|
const views = asArray10(stack.views);
|
|
2541
2721
|
for (let vi = 0; vi < views.length; vi++) {
|
|
2542
2722
|
const view = views[vi];
|
|
2543
|
-
if (!
|
|
2723
|
+
if (!isRec6(view)) continue;
|
|
2544
2724
|
const viewName = strName7(view.name) ?? strName7(view.objectName) ?? `#${vi}`;
|
|
2545
2725
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
2546
|
-
if (
|
|
2726
|
+
if (isRec6(view.listViews)) {
|
|
2547
2727
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
2548
2728
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
2549
2729
|
}
|
|
@@ -2552,7 +2732,7 @@ function validateChartBindings(stack) {
|
|
|
2552
2732
|
const objects = asArray10(stack.objects);
|
|
2553
2733
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
2554
2734
|
const obj = objects[oi];
|
|
2555
|
-
if (!
|
|
2735
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
2556
2736
|
const objName = strName7(obj.name) ?? `#${oi}`;
|
|
2557
2737
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
2558
2738
|
checkListChart(
|
|
@@ -2565,10 +2745,10 @@ function validateChartBindings(stack) {
|
|
|
2565
2745
|
const pages = asArray10(stack.pages);
|
|
2566
2746
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
2567
2747
|
const page = pages[pi];
|
|
2568
|
-
if (!
|
|
2748
|
+
if (!isRec6(page)) continue;
|
|
2569
2749
|
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
2570
2750
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
2571
|
-
const props =
|
|
2751
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
2572
2752
|
if (!props || !strName7(props.dataset)) continue;
|
|
2573
2753
|
const axisRefs = asArray10(props.yAxis).map((a, ai) => ({ name: strName7(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
2574
2754
|
const seriesRefs = asArray10(props.series).map((s, si) => ({ name: strName7(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
@@ -2718,10 +2898,10 @@ function validateNavAccess(stack) {
|
|
|
2718
2898
|
|
|
2719
2899
|
// src/validate-nav-target-refs.ts
|
|
2720
2900
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
2721
|
-
var
|
|
2901
|
+
var isRec7 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2722
2902
|
function asArray13(v) {
|
|
2723
|
-
if (Array.isArray(v)) return v.filter(
|
|
2724
|
-
if (
|
|
2903
|
+
if (Array.isArray(v)) return v.filter(isRec7);
|
|
2904
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => isRec7(def) ? { name, ...def } : { name });
|
|
2725
2905
|
return [];
|
|
2726
2906
|
}
|
|
2727
2907
|
function strName9(v) {
|
|
@@ -2743,7 +2923,7 @@ function namesOf(collection) {
|
|
|
2743
2923
|
}
|
|
2744
2924
|
function validateNavTargetRefs(stack) {
|
|
2745
2925
|
const findings = [];
|
|
2746
|
-
if (!
|
|
2926
|
+
if (!isRec7(stack)) return findings;
|
|
2747
2927
|
const apps = asArray13(stack.apps);
|
|
2748
2928
|
if (apps.length === 0) return findings;
|
|
2749
2929
|
const declared = /* @__PURE__ */ new Map();
|
|
@@ -2755,7 +2935,7 @@ function validateNavTargetRefs(stack) {
|
|
|
2755
2935
|
const walk = (items, basePath) => {
|
|
2756
2936
|
if (!Array.isArray(items)) return;
|
|
2757
2937
|
for (const [ni, raw] of items.entries()) {
|
|
2758
|
-
if (!
|
|
2938
|
+
if (!isRec7(raw)) continue;
|
|
2759
2939
|
const nav = raw;
|
|
2760
2940
|
const navPath = `${basePath}[${ni}]`;
|
|
2761
2941
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
@@ -2786,32 +2966,100 @@ function validateNavTargetRefs(stack) {
|
|
|
2786
2966
|
return findings;
|
|
2787
2967
|
}
|
|
2788
2968
|
|
|
2969
|
+
// src/validate-nav-object-servability.ts
|
|
2970
|
+
import { canServeApiOperation } from "@objectstack/spec/data";
|
|
2971
|
+
var NAV_OBJECT_UNSERVABLE = "nav-object-unservable";
|
|
2972
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2973
|
+
function asArray14(v) {
|
|
2974
|
+
if (Array.isArray(v)) return v.filter(isRec8);
|
|
2975
|
+
if (isRec8(v)) return Object.entries(v).map(([name, def]) => isRec8(def) ? { name, ...def } : { name });
|
|
2976
|
+
return [];
|
|
2977
|
+
}
|
|
2978
|
+
function strName10(v) {
|
|
2979
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2980
|
+
}
|
|
2981
|
+
var isInterpolated3 = (s) => s.includes("${") || s.includes("{");
|
|
2982
|
+
function validateNavObjectServability(stack) {
|
|
2983
|
+
const findings = [];
|
|
2984
|
+
if (!isRec8(stack)) return findings;
|
|
2985
|
+
const apps = asArray14(stack.apps);
|
|
2986
|
+
if (apps.length === 0) return findings;
|
|
2987
|
+
const ownEnable = /* @__PURE__ */ new Map();
|
|
2988
|
+
const objects = asArray14(stack.objects);
|
|
2989
|
+
for (const [oi, obj] of objects.entries()) {
|
|
2990
|
+
const n = strName10(obj.name);
|
|
2991
|
+
if (!n) continue;
|
|
2992
|
+
ownEnable.set(n, { enable: obj.enable, path: `objects[${oi}].enable` });
|
|
2993
|
+
}
|
|
2994
|
+
if (ownEnable.size === 0) return findings;
|
|
2995
|
+
for (const [ai, app] of apps.entries()) {
|
|
2996
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
2997
|
+
const walk = (items, basePath) => {
|
|
2998
|
+
if (!Array.isArray(items)) return;
|
|
2999
|
+
for (const [ni, raw] of items.entries()) {
|
|
3000
|
+
if (!isRec8(raw)) continue;
|
|
3001
|
+
const nav = raw;
|
|
3002
|
+
const navPath = `${basePath}[${ni}]`;
|
|
3003
|
+
if (nav.type === "object") {
|
|
3004
|
+
const target = strName10(nav.objectName);
|
|
3005
|
+
const declared = target && !isInterpolated3(target) ? ownEnable.get(target) : void 0;
|
|
3006
|
+
if (target && declared && !canServeApiOperation(declared.enable, "list")) {
|
|
3007
|
+
const enable = isRec8(declared.enable) ? declared.enable : {};
|
|
3008
|
+
const apiDisabled = enable.apiEnabled === false;
|
|
3009
|
+
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(", ")})` : "");
|
|
3010
|
+
const answer = apiDisabled ? "404 `OBJECT_API_DISABLED`" : "405 `OBJECT_API_METHOD_NOT_ALLOWED`";
|
|
3011
|
+
const offendingKey = apiDisabled ? `${declared.path}.apiEnabled` : `${declared.path}.apiMethods`;
|
|
3012
|
+
findings.push({
|
|
3013
|
+
severity: "error",
|
|
3014
|
+
rule: NAV_OBJECT_UNSERVABLE,
|
|
3015
|
+
where: `app "${appName}" \xB7 nav "${strName10(nav.id) ?? strName10(nav.label) ?? `#${ni}`}"`,
|
|
3016
|
+
// The nav entry is where the dead row is authored; the `enable`
|
|
3017
|
+
// key that condemns it is named in the message, because the fix
|
|
3018
|
+
// may belong at either end.
|
|
3019
|
+
path: `${navPath}.objectName`,
|
|
3020
|
+
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.`,
|
|
3021
|
+
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.`
|
|
3022
|
+
});
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
if (Array.isArray(nav.children)) walk(nav.children, `${navPath}.children`);
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
walk(app.navigation, `apps[${ai}].navigation`);
|
|
3029
|
+
for (const [ari, area] of asArray14(app.areas).entries()) {
|
|
3030
|
+
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
3031
|
+
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
3032
|
+
}
|
|
3033
|
+
}
|
|
3034
|
+
return findings;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
2789
3037
|
// src/validate-translation-references.ts
|
|
2790
3038
|
import { expandViewContainer } from "@objectstack/spec";
|
|
2791
3039
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
2792
3040
|
|
|
2793
3041
|
// src/view-walk.ts
|
|
2794
|
-
function
|
|
3042
|
+
function isRec9(v) {
|
|
2795
3043
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2796
3044
|
}
|
|
2797
|
-
function
|
|
3045
|
+
function strName11(v) {
|
|
2798
3046
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2799
3047
|
}
|
|
2800
3048
|
function viewObjectName(view) {
|
|
2801
|
-
return
|
|
3049
|
+
return strName11(view.objectName) ?? strName11(view.object) ?? (isRec9(view.data) ? strName11(view.data.object) : void 0);
|
|
2802
3050
|
}
|
|
2803
3051
|
function viewContainerSites(view, basePath) {
|
|
2804
|
-
if (!
|
|
3052
|
+
if (!isRec9(view)) return [];
|
|
2805
3053
|
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
2806
|
-
if (
|
|
3054
|
+
if (isRec9(view.form)) {
|
|
2807
3055
|
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
2808
3056
|
}
|
|
2809
3057
|
for (const key of ["listViews", "formViews"]) {
|
|
2810
3058
|
const container = view[key];
|
|
2811
|
-
if (!
|
|
3059
|
+
if (!isRec9(container)) continue;
|
|
2812
3060
|
const kind = key === "listViews" ? "listView" : "formView";
|
|
2813
3061
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
2814
|
-
if (!
|
|
3062
|
+
if (!isRec9(sub)) continue;
|
|
2815
3063
|
sites.push({
|
|
2816
3064
|
view: sub,
|
|
2817
3065
|
path: `${basePath}.${key}.${subKey}`,
|
|
@@ -2829,15 +3077,15 @@ function formViewSites(view, basePath) {
|
|
|
2829
3077
|
// src/validate-translation-references.ts
|
|
2830
3078
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
2831
3079
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
2832
|
-
function
|
|
3080
|
+
function isRec10(v) {
|
|
2833
3081
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
2834
3082
|
}
|
|
2835
|
-
function
|
|
3083
|
+
function asArray15(v) {
|
|
2836
3084
|
if (Array.isArray(v)) return v;
|
|
2837
|
-
if (
|
|
3085
|
+
if (isRec10(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec10(def) ? def : {} }));
|
|
2838
3086
|
return [];
|
|
2839
3087
|
}
|
|
2840
|
-
function
|
|
3088
|
+
function strName12(v) {
|
|
2841
3089
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2842
3090
|
}
|
|
2843
3091
|
function distance5(a, b) {
|
|
@@ -2897,34 +3145,34 @@ function collectViewRecord(view, factsFor) {
|
|
|
2897
3145
|
};
|
|
2898
3146
|
const addSections = (container, binding) => {
|
|
2899
3147
|
if (!binding) return;
|
|
2900
|
-
for (const section of
|
|
2901
|
-
const sectionName =
|
|
3148
|
+
for (const section of asArray15(container.sections)) {
|
|
3149
|
+
const sectionName = strName12(section.name);
|
|
2902
3150
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
2903
3151
|
}
|
|
2904
3152
|
};
|
|
2905
|
-
const listBinding =
|
|
2906
|
-
if (
|
|
2907
|
-
addView(recordObject ?? listBinding,
|
|
3153
|
+
const listBinding = isRec10(view.list) ? bindingOf(view.list) : void 0;
|
|
3154
|
+
if (isRec10(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
3155
|
+
addView(recordObject ?? listBinding, strName12(view.name));
|
|
2908
3156
|
const named = namedViewKeys(view);
|
|
2909
3157
|
for (const family of ["listViews", "formViews"]) {
|
|
2910
3158
|
const container = view[family];
|
|
2911
|
-
if (!
|
|
3159
|
+
if (!isRec10(container)) continue;
|
|
2912
3160
|
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
2913
3161
|
let at = 0;
|
|
2914
3162
|
for (const sub of Object.values(container)) {
|
|
2915
3163
|
if (!sub || typeof sub !== "object") continue;
|
|
2916
3164
|
const registryKey = registryKeys[at++];
|
|
2917
|
-
if (!
|
|
3165
|
+
if (!isRec10(sub)) continue;
|
|
2918
3166
|
const binding = bindingOf(sub) ?? listBinding;
|
|
2919
3167
|
addView(binding, registryKey);
|
|
2920
3168
|
addSections(sub, binding);
|
|
2921
3169
|
}
|
|
2922
3170
|
}
|
|
2923
|
-
if (
|
|
3171
|
+
if (isRec10(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
2924
3172
|
addSections(view, recordObject ?? listBinding);
|
|
2925
3173
|
}
|
|
2926
3174
|
function defaultListViewKey(object, container) {
|
|
2927
|
-
if (!object || !
|
|
3175
|
+
if (!object || !isRec10(container.list)) return void 0;
|
|
2928
3176
|
const item = expandViewContainer(object, container).find(
|
|
2929
3177
|
(i) => i.viewKind === "list" && i.isDefault
|
|
2930
3178
|
);
|
|
@@ -2936,7 +3184,7 @@ function namedViewKeys(container) {
|
|
|
2936
3184
|
const object = "probe";
|
|
2937
3185
|
const prefix = `${object}.`;
|
|
2938
3186
|
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
2939
|
-
const countEntries = (v) =>
|
|
3187
|
+
const countEntries = (v) => isRec10(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
2940
3188
|
const listCount = countEntries(container.listViews);
|
|
2941
3189
|
const formCount = countEntries(container.formViews);
|
|
2942
3190
|
if (!listCount && !formCount) return { list: [], form: [] };
|
|
@@ -2954,14 +3202,14 @@ function readOptions(field) {
|
|
|
2954
3202
|
values.add(opt);
|
|
2955
3203
|
continue;
|
|
2956
3204
|
}
|
|
2957
|
-
if (!
|
|
2958
|
-
const value =
|
|
3205
|
+
if (!isRec10(opt)) continue;
|
|
3206
|
+
const value = strName12(opt.value);
|
|
2959
3207
|
if (!value) continue;
|
|
2960
3208
|
values.add(value);
|
|
2961
|
-
const label2 =
|
|
3209
|
+
const label2 = strName12(opt.label);
|
|
2962
3210
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
2963
3211
|
}
|
|
2964
|
-
} else if (
|
|
3212
|
+
} else if (isRec10(raw)) {
|
|
2965
3213
|
for (const [value, label2] of Object.entries(raw)) {
|
|
2966
3214
|
values.add(value);
|
|
2967
3215
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -2981,48 +3229,48 @@ function buildUniverse(stack) {
|
|
|
2981
3229
|
}
|
|
2982
3230
|
return facts;
|
|
2983
3231
|
};
|
|
2984
|
-
for (const obj of
|
|
2985
|
-
const objectName =
|
|
3232
|
+
for (const obj of asArray15(stack.objects)) {
|
|
3233
|
+
const objectName = strName12(obj.name);
|
|
2986
3234
|
if (!objectName) continue;
|
|
2987
3235
|
const facts = factsFor(objectName);
|
|
2988
|
-
for (const field of
|
|
2989
|
-
const fieldName =
|
|
3236
|
+
for (const field of asArray15(obj.fields)) {
|
|
3237
|
+
const fieldName = strName12(field.name);
|
|
2990
3238
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
2991
3239
|
}
|
|
2992
|
-
for (const action of
|
|
2993
|
-
const actionName =
|
|
3240
|
+
for (const action of asArray15(obj.actions)) {
|
|
3241
|
+
const actionName = strName12(action.name);
|
|
2994
3242
|
if (actionName) facts.actions.set(actionName, action);
|
|
2995
3243
|
}
|
|
2996
|
-
for (const view of
|
|
2997
|
-
collectViewRecord({ ...view, object:
|
|
3244
|
+
for (const view of asArray15(obj.views)) {
|
|
3245
|
+
collectViewRecord({ ...view, object: strName12(view.object) ?? objectName }, factsFor);
|
|
2998
3246
|
}
|
|
2999
3247
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
3000
|
-
for (const group of
|
|
3001
|
-
const key =
|
|
3248
|
+
for (const group of asArray15(obj.fieldGroups)) {
|
|
3249
|
+
const key = strName12(group.key) ?? strName12(group.name);
|
|
3002
3250
|
if (key) facts.sections.add(key);
|
|
3003
3251
|
}
|
|
3004
3252
|
}
|
|
3005
|
-
for (const view of
|
|
3253
|
+
for (const view of asArray15(stack.views)) {
|
|
3006
3254
|
collectViewRecord(view, factsFor);
|
|
3007
3255
|
}
|
|
3008
|
-
const pages =
|
|
3256
|
+
const pages = asArray15(stack.pages);
|
|
3009
3257
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3010
3258
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
3011
3259
|
if (!walked.objectName) continue;
|
|
3012
|
-
const props =
|
|
3260
|
+
const props = isRec10(walked.component.properties) ? walked.component.properties : void 0;
|
|
3013
3261
|
if (!props) continue;
|
|
3014
|
-
for (const section of
|
|
3015
|
-
const sectionName =
|
|
3262
|
+
for (const section of asArray15(props.sections)) {
|
|
3263
|
+
const sectionName = strName12(section.name);
|
|
3016
3264
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
3017
3265
|
}
|
|
3018
3266
|
}
|
|
3019
3267
|
}
|
|
3020
3268
|
const globalActions = /* @__PURE__ */ new Map();
|
|
3021
3269
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
3022
|
-
for (const action of
|
|
3023
|
-
const actionName =
|
|
3270
|
+
for (const action of asArray15(stack.actions)) {
|
|
3271
|
+
const actionName = strName12(action.name);
|
|
3024
3272
|
if (!actionName) continue;
|
|
3025
|
-
const owner =
|
|
3273
|
+
const owner = strName12(action.objectName) ?? strName12(action.object);
|
|
3026
3274
|
if (owner) {
|
|
3027
3275
|
factsFor(owner).actions.set(actionName, action);
|
|
3028
3276
|
actionOwners.set(actionName, owner);
|
|
@@ -3036,41 +3284,41 @@ function buildUniverse(stack) {
|
|
|
3036
3284
|
}
|
|
3037
3285
|
}
|
|
3038
3286
|
const apps = /* @__PURE__ */ new Map();
|
|
3039
|
-
for (const app of
|
|
3040
|
-
const appName =
|
|
3287
|
+
for (const app of asArray15(stack.apps)) {
|
|
3288
|
+
const appName = strName12(app.name);
|
|
3041
3289
|
if (!appName) continue;
|
|
3042
3290
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
3043
3291
|
const walkNav = (items) => {
|
|
3044
|
-
for (const item of
|
|
3045
|
-
const id =
|
|
3292
|
+
for (const item of asArray15(items)) {
|
|
3293
|
+
const id = strName12(item.id);
|
|
3046
3294
|
if (id) navIds.add(id);
|
|
3047
3295
|
if (item.children) walkNav(item.children);
|
|
3048
3296
|
}
|
|
3049
3297
|
};
|
|
3050
3298
|
walkNav(app.navigation);
|
|
3051
|
-
for (const area of
|
|
3052
|
-
const areaId =
|
|
3299
|
+
for (const area of asArray15(app.areas)) {
|
|
3300
|
+
const areaId = strName12(area.id);
|
|
3053
3301
|
if (areaId) navIds.add(areaId);
|
|
3054
3302
|
walkNav(area.navigation);
|
|
3055
3303
|
}
|
|
3056
3304
|
apps.set(appName, navIds);
|
|
3057
3305
|
}
|
|
3058
3306
|
const dashboards = /* @__PURE__ */ new Map();
|
|
3059
|
-
for (const dash of
|
|
3060
|
-
const dashName =
|
|
3307
|
+
for (const dash of asArray15(stack.dashboards)) {
|
|
3308
|
+
const dashName = strName12(dash.name);
|
|
3061
3309
|
if (!dashName) continue;
|
|
3062
3310
|
const widgets = /* @__PURE__ */ new Set();
|
|
3063
|
-
for (const widget of
|
|
3064
|
-
const id =
|
|
3311
|
+
for (const widget of asArray15(dash.widgets)) {
|
|
3312
|
+
const id = strName12(widget.id) ?? strName12(widget.name);
|
|
3065
3313
|
if (id) widgets.add(id);
|
|
3066
3314
|
}
|
|
3067
3315
|
const actions = /* @__PURE__ */ new Set();
|
|
3068
3316
|
const headerActions = [
|
|
3069
|
-
...
|
|
3070
|
-
...
|
|
3317
|
+
...asArray15(isRec10(dash.header) ? dash.header.actions : void 0),
|
|
3318
|
+
...asArray15(dash.actions)
|
|
3071
3319
|
];
|
|
3072
3320
|
for (const action of headerActions) {
|
|
3073
|
-
const key =
|
|
3321
|
+
const key = strName12(action.actionUrl) ?? strName12(action.url) ?? strName12(action.name);
|
|
3074
3322
|
if (key) actions.add(key);
|
|
3075
3323
|
}
|
|
3076
3324
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -3082,7 +3330,7 @@ function localePath(bundleIndex, locale) {
|
|
|
3082
3330
|
}
|
|
3083
3331
|
function validateTranslationReferences(stack) {
|
|
3084
3332
|
const findings = [];
|
|
3085
|
-
if (!
|
|
3333
|
+
if (!isRec10(stack)) return findings;
|
|
3086
3334
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3087
3335
|
if (bundles.length === 0) return findings;
|
|
3088
3336
|
const universe = buildUniverse(stack);
|
|
@@ -3091,13 +3339,13 @@ function validateTranslationReferences(stack) {
|
|
|
3091
3339
|
};
|
|
3092
3340
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
3093
3341
|
const bundle = bundles[bi];
|
|
3094
|
-
if (!
|
|
3342
|
+
if (!isRec10(bundle)) continue;
|
|
3095
3343
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
3096
|
-
if (!
|
|
3344
|
+
if (!isRec10(rawData)) continue;
|
|
3097
3345
|
const base = localePath(bi, locale);
|
|
3098
3346
|
const inLocale = `locale "${locale}"`;
|
|
3099
3347
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
3100
|
-
if (!
|
|
3348
|
+
if (!isRec10(rawNode)) continue;
|
|
3101
3349
|
const objPath = `${base}.objects.${objectName}`;
|
|
3102
3350
|
const facts = universe.objects.get(objectName);
|
|
3103
3351
|
if (!facts) {
|
|
@@ -3123,7 +3371,7 @@ function validateTranslationReferences(stack) {
|
|
|
3123
3371
|
);
|
|
3124
3372
|
continue;
|
|
3125
3373
|
}
|
|
3126
|
-
if (!
|
|
3374
|
+
if (!isRec10(rawField)) continue;
|
|
3127
3375
|
checkOptionKeys(findings, {
|
|
3128
3376
|
optionMap: rawField.options,
|
|
3129
3377
|
field,
|
|
@@ -3205,7 +3453,7 @@ function validateTranslationReferences(stack) {
|
|
|
3205
3453
|
);
|
|
3206
3454
|
continue;
|
|
3207
3455
|
}
|
|
3208
|
-
if (!
|
|
3456
|
+
if (!isRec10(rawApp)) continue;
|
|
3209
3457
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
3210
3458
|
if (navIds.has(navId)) continue;
|
|
3211
3459
|
orphan(
|
|
@@ -3228,7 +3476,7 @@ function validateTranslationReferences(stack) {
|
|
|
3228
3476
|
);
|
|
3229
3477
|
continue;
|
|
3230
3478
|
}
|
|
3231
|
-
if (!
|
|
3479
|
+
if (!isRec10(rawDash)) continue;
|
|
3232
3480
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
3233
3481
|
if (dash.widgets.has(widgetId)) continue;
|
|
3234
3482
|
orphan(
|
|
@@ -3253,7 +3501,7 @@ function validateTranslationReferences(stack) {
|
|
|
3253
3501
|
return findings;
|
|
3254
3502
|
}
|
|
3255
3503
|
function asRecord(v) {
|
|
3256
|
-
return
|
|
3504
|
+
return isRec10(v) ? v : {};
|
|
3257
3505
|
}
|
|
3258
3506
|
function checkOptionKeys(findings, ctx) {
|
|
3259
3507
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -3265,7 +3513,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3265
3513
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
3266
3514
|
where: ctx.where,
|
|
3267
3515
|
path: ctx.path,
|
|
3268
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
3516
|
+
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.`,
|
|
3269
3517
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
3270
3518
|
});
|
|
3271
3519
|
return;
|
|
@@ -3284,11 +3532,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
3284
3532
|
}
|
|
3285
3533
|
}
|
|
3286
3534
|
function checkActionParams(findings, ctx) {
|
|
3287
|
-
const rawParams = Object.keys(asRecord(
|
|
3535
|
+
const rawParams = Object.keys(asRecord(isRec10(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
3288
3536
|
if (rawParams.length === 0) return;
|
|
3289
3537
|
const declared = /* @__PURE__ */ new Set();
|
|
3290
|
-
for (const param of
|
|
3291
|
-
const name =
|
|
3538
|
+
for (const param of asArray15(ctx.action.params)) {
|
|
3539
|
+
const name = strName12(param.name) ?? strName12(param.field);
|
|
3292
3540
|
if (name) declared.add(name);
|
|
3293
3541
|
}
|
|
3294
3542
|
for (const paramName of rawParams) {
|
|
@@ -3305,33 +3553,33 @@ function checkActionParams(findings, ctx) {
|
|
|
3305
3553
|
}
|
|
3306
3554
|
|
|
3307
3555
|
// src/collection-entries.ts
|
|
3308
|
-
function
|
|
3556
|
+
function isRec11(v) {
|
|
3309
3557
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3310
3558
|
}
|
|
3311
3559
|
function collectionEntries(v, base) {
|
|
3312
3560
|
if (Array.isArray(v)) {
|
|
3313
3561
|
const out = [];
|
|
3314
3562
|
for (let i = 0; i < v.length; i++) {
|
|
3315
|
-
if (
|
|
3563
|
+
if (isRec11(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3316
3564
|
}
|
|
3317
3565
|
return out;
|
|
3318
3566
|
}
|
|
3319
|
-
if (
|
|
3320
|
-
return Object.entries(v).filter(([, def]) =>
|
|
3567
|
+
if (isRec11(v)) {
|
|
3568
|
+
return Object.entries(v).filter(([, def]) => isRec11(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3321
3569
|
}
|
|
3322
3570
|
return [];
|
|
3323
3571
|
}
|
|
3324
3572
|
|
|
3325
3573
|
// src/validate-translatable-sections.ts
|
|
3326
3574
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
3327
|
-
function
|
|
3575
|
+
function isRec12(v) {
|
|
3328
3576
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3329
3577
|
}
|
|
3330
|
-
function
|
|
3578
|
+
function strName13(v) {
|
|
3331
3579
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3332
3580
|
}
|
|
3333
3581
|
function viewLabel(view) {
|
|
3334
|
-
const name =
|
|
3582
|
+
const name = strName13(view.name);
|
|
3335
3583
|
return name ? `view "${name}"` : "";
|
|
3336
3584
|
}
|
|
3337
3585
|
function joinWhere(...parts) {
|
|
@@ -3339,7 +3587,7 @@ function joinWhere(...parts) {
|
|
|
3339
3587
|
}
|
|
3340
3588
|
function collectViewSites(view, basePath, label2, sites) {
|
|
3341
3589
|
const recordObject = viewObjectName(view);
|
|
3342
|
-
const listBinding =
|
|
3590
|
+
const listBinding = isRec12(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
3343
3591
|
for (const site of viewContainerSites(view, basePath)) {
|
|
3344
3592
|
sites.push({
|
|
3345
3593
|
path: `${site.path}.sections`,
|
|
@@ -3353,11 +3601,11 @@ function translatedObjectNames(stack) {
|
|
|
3353
3601
|
const out = /* @__PURE__ */ new Set();
|
|
3354
3602
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
3355
3603
|
for (const bundle of bundles) {
|
|
3356
|
-
if (!
|
|
3604
|
+
if (!isRec12(bundle)) continue;
|
|
3357
3605
|
for (const data of Object.values(bundle)) {
|
|
3358
|
-
if (!
|
|
3606
|
+
if (!isRec12(data) || !isRec12(data.objects)) continue;
|
|
3359
3607
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
3360
|
-
if (
|
|
3608
|
+
if (isRec12(node)) out.add(objectName);
|
|
3361
3609
|
}
|
|
3362
3610
|
}
|
|
3363
3611
|
}
|
|
@@ -3369,22 +3617,22 @@ function suggestedName(label2) {
|
|
|
3369
3617
|
}
|
|
3370
3618
|
function validateTranslatableSections(stack) {
|
|
3371
3619
|
const findings = [];
|
|
3372
|
-
if (!
|
|
3620
|
+
if (!isRec12(stack)) return findings;
|
|
3373
3621
|
const translated = translatedObjectNames(stack);
|
|
3374
3622
|
if (translated.size === 0) return findings;
|
|
3375
3623
|
const sites = [];
|
|
3376
3624
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
3377
|
-
const objectName =
|
|
3625
|
+
const objectName = strName13(obj.name);
|
|
3378
3626
|
if (!objectName) continue;
|
|
3379
3627
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
3380
3628
|
collectViewSites(
|
|
3381
|
-
{ ...view, object:
|
|
3629
|
+
{ ...view, object: strName13(view.object) ?? objectName },
|
|
3382
3630
|
path,
|
|
3383
3631
|
viewLabel(view),
|
|
3384
3632
|
sites
|
|
3385
3633
|
);
|
|
3386
3634
|
}
|
|
3387
|
-
if (
|
|
3635
|
+
if (isRec12(obj.listViews)) {
|
|
3388
3636
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
3389
3637
|
}
|
|
3390
3638
|
}
|
|
@@ -3392,13 +3640,13 @@ function validateTranslatableSections(stack) {
|
|
|
3392
3640
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
3393
3641
|
}
|
|
3394
3642
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
3395
|
-
const pageName =
|
|
3643
|
+
const pageName = strName13(page.name);
|
|
3396
3644
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
3397
3645
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
3398
3646
|
if (!walked.objectName) continue;
|
|
3399
|
-
const props =
|
|
3647
|
+
const props = isRec12(walked.component.properties) ? walked.component.properties : void 0;
|
|
3400
3648
|
if (!props) continue;
|
|
3401
|
-
const type =
|
|
3649
|
+
const type = strName13(walked.component.type) ?? "component";
|
|
3402
3650
|
sites.push({
|
|
3403
3651
|
path: `${walked.path}.properties.sections`,
|
|
3404
3652
|
surface: joinWhere(pageLabel, type),
|
|
@@ -3413,9 +3661,9 @@ function validateTranslatableSections(stack) {
|
|
|
3413
3661
|
if (!Array.isArray(site.sections)) continue;
|
|
3414
3662
|
for (let i = 0; i < site.sections.length; i++) {
|
|
3415
3663
|
const section = site.sections[i];
|
|
3416
|
-
if (!
|
|
3417
|
-
if (
|
|
3418
|
-
const heading =
|
|
3664
|
+
if (!isRec12(section)) continue;
|
|
3665
|
+
if (strName13(section.name)) continue;
|
|
3666
|
+
const heading = strName13(section.label);
|
|
3419
3667
|
if (!heading) continue;
|
|
3420
3668
|
const slug = suggestedName(heading);
|
|
3421
3669
|
findings.push({
|
|
@@ -3433,10 +3681,10 @@ function validateTranslatableSections(stack) {
|
|
|
3433
3681
|
|
|
3434
3682
|
// src/flow-walk.ts
|
|
3435
3683
|
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
3436
|
-
function
|
|
3684
|
+
function isRec13(v) {
|
|
3437
3685
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3438
3686
|
}
|
|
3439
|
-
function
|
|
3687
|
+
function strName14(v) {
|
|
3440
3688
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3441
3689
|
}
|
|
3442
3690
|
var REGION_SLOTS = new Map(
|
|
@@ -3445,10 +3693,10 @@ var REGION_SLOTS = new Map(
|
|
|
3445
3693
|
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
3446
3694
|
var MAX_REGION_DEPTH = 16;
|
|
3447
3695
|
function flowNodeLabel(node, index) {
|
|
3448
|
-
return
|
|
3696
|
+
return strName14(node.label) ?? strName14(node.id) ?? `#${index}`;
|
|
3449
3697
|
}
|
|
3450
3698
|
function stripRegions(config) {
|
|
3451
|
-
if (!
|
|
3699
|
+
if (!isRec13(config)) return void 0;
|
|
3452
3700
|
let out;
|
|
3453
3701
|
for (const key of Object.keys(config)) {
|
|
3454
3702
|
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
@@ -3459,11 +3707,11 @@ function stripRegions(config) {
|
|
|
3459
3707
|
}
|
|
3460
3708
|
function walkFlowNodes(flow, flowPath) {
|
|
3461
3709
|
const out = [];
|
|
3462
|
-
if (!
|
|
3710
|
+
if (!isRec13(flow)) return out;
|
|
3463
3711
|
const visitList = (nodes, basePath, trail, depth) => {
|
|
3464
3712
|
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
3465
3713
|
nodes.forEach((raw, index) => {
|
|
3466
|
-
if (!
|
|
3714
|
+
if (!isRec13(raw)) return;
|
|
3467
3715
|
const path = `${basePath}[${index}]`;
|
|
3468
3716
|
out.push({
|
|
3469
3717
|
node: raw,
|
|
@@ -3472,9 +3720,9 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3472
3720
|
regionTrail: trail,
|
|
3473
3721
|
depth
|
|
3474
3722
|
});
|
|
3475
|
-
const type =
|
|
3723
|
+
const type = strName14(raw.type);
|
|
3476
3724
|
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
3477
|
-
if (!slots || !
|
|
3725
|
+
if (!slots || !isRec13(raw.config)) return;
|
|
3478
3726
|
const config = raw.config;
|
|
3479
3727
|
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
3480
3728
|
for (const slot of slots) {
|
|
@@ -3482,8 +3730,8 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3482
3730
|
if (slot === "branches") {
|
|
3483
3731
|
if (!Array.isArray(value)) continue;
|
|
3484
3732
|
value.forEach((branch, b) => {
|
|
3485
|
-
if (!
|
|
3486
|
-
const branchName =
|
|
3733
|
+
if (!isRec13(branch)) return;
|
|
3734
|
+
const branchName = strName14(branch.name) ?? `#${b}`;
|
|
3487
3735
|
visitList(
|
|
3488
3736
|
branch.nodes,
|
|
3489
3737
|
`${path}.config.branches[${b}].nodes`,
|
|
@@ -3493,7 +3741,7 @@ function walkFlowNodes(flow, flowPath) {
|
|
|
3493
3741
|
});
|
|
3494
3742
|
continue;
|
|
3495
3743
|
}
|
|
3496
|
-
if (!
|
|
3744
|
+
if (!isRec13(value)) continue;
|
|
3497
3745
|
visitList(
|
|
3498
3746
|
value.nodes,
|
|
3499
3747
|
`${path}.config.${slot}.nodes`,
|
|
@@ -3513,7 +3761,8 @@ function joinTrail(trail, segment) {
|
|
|
3513
3761
|
// src/validate-flow-template-paths.ts
|
|
3514
3762
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
3515
3763
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
3516
|
-
|
|
3764
|
+
var FLOW_TEMPLATE_FIELD_UNPROVISIONED = "flow-template-field-unprovisioned";
|
|
3765
|
+
function asArray16(v) {
|
|
3517
3766
|
if (Array.isArray(v)) return v;
|
|
3518
3767
|
if (v && typeof v === "object") {
|
|
3519
3768
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -3542,7 +3791,7 @@ var FILTER_GUARDED_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
|
3542
3791
|
]);
|
|
3543
3792
|
function fieldTypesOf(obj) {
|
|
3544
3793
|
const types = /* @__PURE__ */ new Map();
|
|
3545
|
-
for (const f of
|
|
3794
|
+
for (const f of asArray16(obj.fields)) {
|
|
3546
3795
|
if (typeof f.name === "string") {
|
|
3547
3796
|
types.set(f.name, typeof f.type === "string" ? f.type : "");
|
|
3548
3797
|
}
|
|
@@ -3639,10 +3888,10 @@ function declaredExpandOf(flow) {
|
|
|
3639
3888
|
}
|
|
3640
3889
|
function validateFlowTemplatePaths(stack) {
|
|
3641
3890
|
const findings = [];
|
|
3642
|
-
const flows =
|
|
3891
|
+
const flows = asArray16(stack.flows);
|
|
3643
3892
|
if (flows.length === 0) return findings;
|
|
3644
3893
|
const objectsByName = /* @__PURE__ */ new Map();
|
|
3645
|
-
for (const obj of
|
|
3894
|
+
for (const obj of asArray16(stack.objects)) {
|
|
3646
3895
|
if (typeof obj.name === "string") objectsByName.set(obj.name, obj);
|
|
3647
3896
|
}
|
|
3648
3897
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -3655,6 +3904,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3655
3904
|
const obj = objectsByName.get(objectName);
|
|
3656
3905
|
if (!obj) return;
|
|
3657
3906
|
const fieldTypes = fieldTypesOf(obj);
|
|
3907
|
+
const unprovisionedAnchors = unprovisionedInjectedColumnsFor(obj);
|
|
3658
3908
|
const expandSet = declaredExpandOf(flow);
|
|
3659
3909
|
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
3660
3910
|
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
@@ -3666,6 +3916,7 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3666
3916
|
if (leaves.length === 0) return;
|
|
3667
3917
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
3668
3918
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
3919
|
+
const seenUnprovisioned = /* @__PURE__ */ new Set();
|
|
3669
3920
|
for (const leaf of leaves) {
|
|
3670
3921
|
const inFilter = leaf.inFilter;
|
|
3671
3922
|
for (const rest of recordRefsIn(leaf.text)) {
|
|
@@ -3673,6 +3924,19 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3673
3924
|
const hasSubPath = rest.length > 1;
|
|
3674
3925
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
3675
3926
|
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
3927
|
+
if (unprovisionedAnchors.has(head)) {
|
|
3928
|
+
if (!seenUnprovisioned.has(head)) {
|
|
3929
|
+
seenUnprovisioned.add(head);
|
|
3930
|
+
findings.push({
|
|
3931
|
+
severity: "warning",
|
|
3932
|
+
rule: FLOW_TEMPLATE_FIELD_UNPROVISIONED,
|
|
3933
|
+
where,
|
|
3934
|
+
path: nodePath,
|
|
3935
|
+
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).`),
|
|
3936
|
+
hint: unprovisionedAnchorHint(objectName, head)
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3676
3940
|
if (!isKnown) {
|
|
3677
3941
|
if (seenUnknown.has(head)) continue;
|
|
3678
3942
|
seenUnknown.add(head);
|
|
@@ -3711,14 +3975,14 @@ function validateFlowTemplatePaths(stack) {
|
|
|
3711
3975
|
|
|
3712
3976
|
// src/validate-ai-surface-affinity.ts
|
|
3713
3977
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
3714
|
-
function
|
|
3978
|
+
function asArray17(v) {
|
|
3715
3979
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3716
3980
|
if (v && typeof v === "object") {
|
|
3717
3981
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3718
3982
|
}
|
|
3719
3983
|
return [];
|
|
3720
3984
|
}
|
|
3721
|
-
function
|
|
3985
|
+
function strName15(v) {
|
|
3722
3986
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3723
3987
|
}
|
|
3724
3988
|
function surfaceOf(v) {
|
|
@@ -3728,18 +3992,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3728
3992
|
const findings = [];
|
|
3729
3993
|
if (!stack || typeof stack !== "object") return findings;
|
|
3730
3994
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
3731
|
-
for (const skill of
|
|
3732
|
-
const n =
|
|
3995
|
+
for (const skill of asArray17(stack.skills)) {
|
|
3996
|
+
const n = strName15(skill.name);
|
|
3733
3997
|
if (n) skillsByName.set(n, skill);
|
|
3734
3998
|
}
|
|
3735
|
-
const agents =
|
|
3999
|
+
const agents = asArray17(stack.agents);
|
|
3736
4000
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3737
4001
|
const agent = agents[ai];
|
|
3738
|
-
const agentName =
|
|
4002
|
+
const agentName = strName15(agent.name) ?? `#${ai}`;
|
|
3739
4003
|
const agentSurface = surfaceOf(agent.surface);
|
|
3740
4004
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
3741
4005
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
3742
|
-
const ref =
|
|
4006
|
+
const ref = strName15(skillRefs[si]);
|
|
3743
4007
|
if (!ref) continue;
|
|
3744
4008
|
const skill = skillsByName.get(ref);
|
|
3745
4009
|
if (!skill) continue;
|
|
@@ -3761,14 +4025,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
3761
4025
|
// src/validate-ai-tool-references.ts
|
|
3762
4026
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
3763
4027
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
3764
|
-
function
|
|
4028
|
+
function asArray18(v) {
|
|
3765
4029
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3766
4030
|
if (v && typeof v === "object") {
|
|
3767
4031
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3768
4032
|
}
|
|
3769
4033
|
return [];
|
|
3770
4034
|
}
|
|
3771
|
-
function
|
|
4035
|
+
function strName16(v) {
|
|
3772
4036
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3773
4037
|
}
|
|
3774
4038
|
function distance6(a, b) {
|
|
@@ -3809,26 +4073,26 @@ function materialisesAsTool(action) {
|
|
|
3809
4073
|
if (!ai || typeof ai !== "object") return false;
|
|
3810
4074
|
const aiRec = ai;
|
|
3811
4075
|
if (aiRec.exposed !== true) return false;
|
|
3812
|
-
if (!
|
|
3813
|
-
const type =
|
|
4076
|
+
if (!strName16(aiRec.description)) return false;
|
|
4077
|
+
const type = strName16(action.type);
|
|
3814
4078
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
3815
4079
|
if (type === "script") return Boolean(action.target || action.body);
|
|
3816
4080
|
return Boolean(action.target);
|
|
3817
4081
|
}
|
|
3818
4082
|
function collectToolUniverse(stack) {
|
|
3819
4083
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
3820
|
-
for (const tool of
|
|
3821
|
-
const n =
|
|
4084
|
+
for (const tool of asArray18(stack.tools)) {
|
|
4085
|
+
const n = strName16(tool.name);
|
|
3822
4086
|
if (n) universe.add(n);
|
|
3823
4087
|
}
|
|
3824
4088
|
const addActionFamily = (actions) => {
|
|
3825
|
-
for (const action of
|
|
3826
|
-
const n =
|
|
4089
|
+
for (const action of asArray18(actions)) {
|
|
4090
|
+
const n = strName16(action.name);
|
|
3827
4091
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
3828
4092
|
}
|
|
3829
4093
|
};
|
|
3830
4094
|
addActionFamily(stack.actions);
|
|
3831
|
-
for (const obj of
|
|
4095
|
+
for (const obj of asArray18(stack.objects)) {
|
|
3832
4096
|
addActionFamily(obj.actions);
|
|
3833
4097
|
}
|
|
3834
4098
|
return universe;
|
|
@@ -3836,13 +4100,13 @@ function collectToolUniverse(stack) {
|
|
|
3836
4100
|
function collectUnexposedActionNames(stack) {
|
|
3837
4101
|
const names = /* @__PURE__ */ new Set();
|
|
3838
4102
|
const scan = (actions) => {
|
|
3839
|
-
for (const action of
|
|
3840
|
-
const n =
|
|
4103
|
+
for (const action of asArray18(actions)) {
|
|
4104
|
+
const n = strName16(action.name);
|
|
3841
4105
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
3842
4106
|
}
|
|
3843
4107
|
};
|
|
3844
4108
|
scan(stack.actions);
|
|
3845
|
-
for (const obj of
|
|
4109
|
+
for (const obj of asArray18(stack.objects)) scan(obj.actions);
|
|
3846
4110
|
return names;
|
|
3847
4111
|
}
|
|
3848
4112
|
function validateAiToolReferences(stack) {
|
|
@@ -3860,13 +4124,13 @@ function validateAiToolReferences(stack) {
|
|
|
3860
4124
|
}
|
|
3861
4125
|
return universe.has(ref);
|
|
3862
4126
|
};
|
|
3863
|
-
const skills =
|
|
4127
|
+
const skills = asArray18(stack.skills);
|
|
3864
4128
|
for (let si = 0; si < skills.length; si++) {
|
|
3865
4129
|
const skill = skills[si];
|
|
3866
|
-
const skillName =
|
|
4130
|
+
const skillName = strName16(skill.name) ?? `#${si}`;
|
|
3867
4131
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
3868
4132
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
3869
|
-
const ref =
|
|
4133
|
+
const ref = strName16(refs[ti]);
|
|
3870
4134
|
if (!ref || resolves(ref)) continue;
|
|
3871
4135
|
const isPattern = ref.endsWith("*");
|
|
3872
4136
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -3886,24 +4150,24 @@ function validateAiToolReferences(stack) {
|
|
|
3886
4150
|
// src/validate-ai-agent-authoring.ts
|
|
3887
4151
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
3888
4152
|
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
3889
|
-
function
|
|
4153
|
+
function asArray19(v) {
|
|
3890
4154
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
3891
4155
|
if (v && typeof v === "object") {
|
|
3892
4156
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3893
4157
|
}
|
|
3894
4158
|
return [];
|
|
3895
4159
|
}
|
|
3896
|
-
function
|
|
4160
|
+
function strName17(v) {
|
|
3897
4161
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3898
4162
|
}
|
|
3899
4163
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
3900
4164
|
function validateAiAgentAuthoring(stack) {
|
|
3901
4165
|
const findings = [];
|
|
3902
4166
|
if (!stack || typeof stack !== "object") return findings;
|
|
3903
|
-
const agents =
|
|
4167
|
+
const agents = asArray19(stack.agents);
|
|
3904
4168
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
3905
4169
|
const agent = agents[ai];
|
|
3906
|
-
const name =
|
|
4170
|
+
const name = strName17(agent.name) ?? `#${ai}`;
|
|
3907
4171
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
3908
4172
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
3909
4173
|
findings.push({
|
|
@@ -3916,12 +4180,12 @@ function validateAiAgentAuthoring(stack) {
|
|
|
3916
4180
|
});
|
|
3917
4181
|
}
|
|
3918
4182
|
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
3919
|
-
const apps =
|
|
4183
|
+
const apps = asArray19(stack.apps);
|
|
3920
4184
|
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
3921
4185
|
const app = apps[appIdx];
|
|
3922
|
-
const defaultAgent =
|
|
4186
|
+
const defaultAgent = strName17(app.defaultAgent);
|
|
3923
4187
|
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
3924
|
-
const appName =
|
|
4188
|
+
const appName = strName17(app.name) ?? `#${appIdx}`;
|
|
3925
4189
|
findings.push({
|
|
3926
4190
|
severity: "warning",
|
|
3927
4191
|
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
@@ -4017,24 +4281,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
4017
4281
|
"owner",
|
|
4018
4282
|
"record_type"
|
|
4019
4283
|
]);
|
|
4020
|
-
var
|
|
4021
|
-
function
|
|
4022
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4023
|
-
if (
|
|
4284
|
+
var isRec14 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4285
|
+
function asArray20(v) {
|
|
4286
|
+
if (Array.isArray(v)) return v.filter((x) => isRec14(x));
|
|
4287
|
+
if (isRec14(v)) {
|
|
4024
4288
|
return Object.entries(v).map(([name, def]) => ({
|
|
4025
4289
|
name,
|
|
4026
|
-
...
|
|
4290
|
+
...isRec14(def) ? def : {}
|
|
4027
4291
|
}));
|
|
4028
4292
|
}
|
|
4029
4293
|
return [];
|
|
4030
4294
|
}
|
|
4031
4295
|
function indexObjectFields2(stack) {
|
|
4032
4296
|
const out = /* @__PURE__ */ new Map();
|
|
4033
|
-
for (const obj of
|
|
4297
|
+
for (const obj of asArray20(stack.objects)) {
|
|
4034
4298
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
4035
4299
|
if (!name) continue;
|
|
4036
4300
|
const names = /* @__PURE__ */ new Set();
|
|
4037
|
-
for (const f of
|
|
4301
|
+
for (const f of asArray20(obj.fields)) {
|
|
4038
4302
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
4039
4303
|
}
|
|
4040
4304
|
out.set(name, names);
|
|
@@ -4164,12 +4428,12 @@ ${source}
|
|
|
4164
4428
|
}
|
|
4165
4429
|
function validateHookBodyWrites(stack) {
|
|
4166
4430
|
const findings = [];
|
|
4167
|
-
const hooks =
|
|
4431
|
+
const hooks = asArray20(stack.hooks);
|
|
4168
4432
|
if (hooks.length === 0) return findings;
|
|
4169
4433
|
let objectFields = null;
|
|
4170
4434
|
hooks.forEach((hook, hookIndex) => {
|
|
4171
4435
|
const body = hook.body;
|
|
4172
|
-
if (!
|
|
4436
|
+
if (!isRec14(body) || body.language !== "js") return;
|
|
4173
4437
|
const source = body.source;
|
|
4174
4438
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4175
4439
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -4240,13 +4504,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
4240
4504
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
4241
4505
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
4242
4506
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
4243
|
-
var
|
|
4244
|
-
function
|
|
4245
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4246
|
-
if (
|
|
4507
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4508
|
+
function asArray21(v) {
|
|
4509
|
+
if (Array.isArray(v)) return v.filter((x) => isRec15(x));
|
|
4510
|
+
if (isRec15(v)) {
|
|
4247
4511
|
return Object.entries(v).map(([name, def]) => ({
|
|
4248
4512
|
name,
|
|
4249
|
-
...
|
|
4513
|
+
...isRec15(def) ? def : {}
|
|
4250
4514
|
}));
|
|
4251
4515
|
}
|
|
4252
4516
|
return [];
|
|
@@ -4260,11 +4524,11 @@ function collectActionBodies(stack) {
|
|
|
4260
4524
|
const sites = [];
|
|
4261
4525
|
const seen = /* @__PURE__ */ new Set();
|
|
4262
4526
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
4263
|
-
|
|
4527
|
+
asArray21(actions).forEach((action, index) => {
|
|
4264
4528
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
4265
4529
|
if (type !== "script") return;
|
|
4266
4530
|
const body = action.body;
|
|
4267
|
-
if (!
|
|
4531
|
+
if (!isRec15(body) || body.language !== "js") return;
|
|
4268
4532
|
const source = body.source;
|
|
4269
4533
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
4270
4534
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -4275,7 +4539,7 @@ function collectActionBodies(stack) {
|
|
|
4275
4539
|
});
|
|
4276
4540
|
};
|
|
4277
4541
|
collect(stack.actions, "actions");
|
|
4278
|
-
|
|
4542
|
+
asArray21(stack.objects).forEach((obj, objIndex) => {
|
|
4279
4543
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
4280
4544
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
4281
4545
|
});
|
|
@@ -4283,7 +4547,7 @@ function collectActionBodies(stack) {
|
|
|
4283
4547
|
}
|
|
4284
4548
|
function validateActionBodyWrites(stack) {
|
|
4285
4549
|
const findings = [];
|
|
4286
|
-
if (!
|
|
4550
|
+
if (!isRec15(stack)) return findings;
|
|
4287
4551
|
const sites = collectActionBodies(stack);
|
|
4288
4552
|
if (sites.length === 0) return findings;
|
|
4289
4553
|
let objectFields = null;
|
|
@@ -4341,13 +4605,13 @@ function fixHint2(field, declared) {
|
|
|
4341
4605
|
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
4342
4606
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
4343
4607
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
4344
|
-
var
|
|
4345
|
-
function
|
|
4346
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
4347
|
-
if (
|
|
4608
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4609
|
+
function asArray22(v) {
|
|
4610
|
+
if (Array.isArray(v)) return v.filter((x) => isRec16(x));
|
|
4611
|
+
if (isRec16(v)) {
|
|
4348
4612
|
return Object.entries(v).map(([name, def]) => ({
|
|
4349
4613
|
name,
|
|
4350
|
-
...
|
|
4614
|
+
...isRec16(def) ? def : {}
|
|
4351
4615
|
}));
|
|
4352
4616
|
}
|
|
4353
4617
|
return [];
|
|
@@ -4360,8 +4624,8 @@ function readLiteralObjectName(config) {
|
|
|
4360
4624
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
4361
4625
|
function validateFlowNodeWrites(stack) {
|
|
4362
4626
|
const findings = [];
|
|
4363
|
-
if (!
|
|
4364
|
-
const flows =
|
|
4627
|
+
if (!isRec16(stack)) return findings;
|
|
4628
|
+
const flows = asArray22(stack.flows);
|
|
4365
4629
|
if (flows.length === 0) return findings;
|
|
4366
4630
|
let objectFields = null;
|
|
4367
4631
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -4369,10 +4633,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
4369
4633
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
4370
4634
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
4371
4635
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
4372
|
-
const config =
|
|
4636
|
+
const config = isRec16(node.config) ? node.config : void 0;
|
|
4373
4637
|
if (!config) return;
|
|
4374
4638
|
const fields = config.fields;
|
|
4375
|
-
if (!
|
|
4639
|
+
if (!isRec16(fields)) return;
|
|
4376
4640
|
const written = Object.keys(fields);
|
|
4377
4641
|
if (written.length === 0) return;
|
|
4378
4642
|
const objectName = readLiteralObjectName(config);
|
|
@@ -4406,7 +4670,7 @@ function fixHint3(field, declared) {
|
|
|
4406
4670
|
// src/validate-readonly-flow-writes.ts
|
|
4407
4671
|
var FLOW_UPDATE_READONLY_FIELD = "flow-update-readonly-field";
|
|
4408
4672
|
var FLOW_UPDATE_READONLY_WHEN_FIELD = "flow-update-readonly-when-field";
|
|
4409
|
-
function
|
|
4673
|
+
function asArray23(v) {
|
|
4410
4674
|
if (Array.isArray(v)) return v;
|
|
4411
4675
|
if (v && typeof v === "object") {
|
|
4412
4676
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -4447,9 +4711,9 @@ function readLiteralObjectName2(config) {
|
|
|
4447
4711
|
}
|
|
4448
4712
|
function validateReadonlyFlowWrites(stack) {
|
|
4449
4713
|
const findings = [];
|
|
4450
|
-
const flows =
|
|
4714
|
+
const flows = asArray23(stack.flows);
|
|
4451
4715
|
if (flows.length === 0) return findings;
|
|
4452
|
-
const roIndex = buildReadonlyIndex(
|
|
4716
|
+
const roIndex = buildReadonlyIndex(asArray23(stack.objects));
|
|
4453
4717
|
flows.forEach((flow, flowIndex) => {
|
|
4454
4718
|
if (flow.runAs === "system") return;
|
|
4455
4719
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
@@ -4508,11 +4772,11 @@ import {
|
|
|
4508
4772
|
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
4509
4773
|
|
|
4510
4774
|
// src/zod-issue-format.ts
|
|
4511
|
-
var
|
|
4775
|
+
var isRec17 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4512
4776
|
var valueAtPath = (root, path) => {
|
|
4513
4777
|
let cur = root;
|
|
4514
4778
|
for (const key of path) {
|
|
4515
|
-
if (!
|
|
4779
|
+
if (!isRec17(cur) && !Array.isArray(cur)) return void 0;
|
|
4516
4780
|
cur = cur[key];
|
|
4517
4781
|
}
|
|
4518
4782
|
return cur;
|
|
@@ -4557,7 +4821,7 @@ function loadTypeScript2() {
|
|
|
4557
4821
|
}
|
|
4558
4822
|
return cachedTs2;
|
|
4559
4823
|
}
|
|
4560
|
-
var
|
|
4824
|
+
var asArray24 = (v) => Array.isArray(v) ? v : [];
|
|
4561
4825
|
var BLOCKS = new Map(
|
|
4562
4826
|
REACT_BLOCKS.map((b) => [
|
|
4563
4827
|
b.tag,
|
|
@@ -4645,12 +4909,13 @@ function filterAttrValue(tsc, sf, attr) {
|
|
|
4645
4909
|
return perPosition(init.expression);
|
|
4646
4910
|
}
|
|
4647
4911
|
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
4912
|
+
var REACT_CHART_FIELD_UNPROVISIONED = "react-chart-field-unprovisioned";
|
|
4648
4913
|
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
4649
4914
|
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
4650
4915
|
var REACT_CHART_DRILLDOWN_INVALID = "react-chart-drilldown-invalid";
|
|
4651
4916
|
function checkChartDrillDown(raw, push2) {
|
|
4652
4917
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4653
|
-
if (!
|
|
4918
|
+
if (!isRec18(raw)) {
|
|
4654
4919
|
push2(
|
|
4655
4920
|
"error",
|
|
4656
4921
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
@@ -4673,7 +4938,7 @@ function checkChartDrillDown(raw, push2) {
|
|
|
4673
4938
|
}
|
|
4674
4939
|
function checkChartAggregate(raw, push2) {
|
|
4675
4940
|
if (raw === void 0 || raw === NOT_STATIC) return;
|
|
4676
|
-
if (!
|
|
4941
|
+
if (!isRec18(raw)) {
|
|
4677
4942
|
push2(
|
|
4678
4943
|
"error",
|
|
4679
4944
|
REACT_CHART_AGGREGATE_INVALID,
|
|
@@ -4704,9 +4969,9 @@ function checkChartAggregate(raw, push2) {
|
|
|
4704
4969
|
);
|
|
4705
4970
|
}
|
|
4706
4971
|
}
|
|
4707
|
-
var
|
|
4972
|
+
var isRec18 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
4708
4973
|
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4709
|
-
function checkObjectChart(attrs, objectFields, findings) {
|
|
4974
|
+
function checkObjectChart(attrs, objectFields, findings, unprovisionedAnchors = /* @__PURE__ */ new Map()) {
|
|
4710
4975
|
const { values, where, path } = attrs;
|
|
4711
4976
|
const push2 = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
4712
4977
|
checkChartDrillDown(values.get("drillDown"), push2);
|
|
@@ -4714,18 +4979,29 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4714
4979
|
const aggregate = values.get("aggregate");
|
|
4715
4980
|
checkChartAggregate(aggregate, push2);
|
|
4716
4981
|
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
4717
|
-
if (!
|
|
4982
|
+
if (!isRec18(aggregate)) return;
|
|
4718
4983
|
const fn = strOf(aggregate.function);
|
|
4719
4984
|
const field = strOf(aggregate.field);
|
|
4720
4985
|
const groupBy = aggregate.groupBy;
|
|
4721
|
-
const groupByField = strOf(groupBy) ?? (
|
|
4986
|
+
const groupByField = strOf(groupBy) ?? (isRec18(groupBy) ? strOf(groupBy.field) : void 0);
|
|
4722
4987
|
const objectName = strOf(values.get("objectName"));
|
|
4723
4988
|
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
4724
4989
|
if (objectName && known) {
|
|
4990
|
+
const anchors = unprovisionedAnchors.get(objectName);
|
|
4725
4991
|
const fieldRef = (name, prop) => {
|
|
4726
4992
|
if (!name) return;
|
|
4727
4993
|
if (name.includes(".")) return;
|
|
4728
|
-
if (known.has(name) || SYSTEM_FIELDS.has(name))
|
|
4994
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) {
|
|
4995
|
+
if (anchors?.has(name)) {
|
|
4996
|
+
push2(
|
|
4997
|
+
"warning",
|
|
4998
|
+
REACT_CHART_FIELD_UNPROVISIONED,
|
|
4999
|
+
`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.`,
|
|
5000
|
+
unprovisionedAnchorHint(objectName, name)
|
|
5001
|
+
);
|
|
5002
|
+
}
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
4729
5005
|
push2(
|
|
4730
5006
|
"error",
|
|
4731
5007
|
REACT_CHART_FIELD_UNKNOWN,
|
|
@@ -4751,18 +5027,18 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
4751
5027
|
);
|
|
4752
5028
|
};
|
|
4753
5029
|
const xAxisRaw = values.get("xAxis");
|
|
4754
|
-
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (
|
|
5030
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec18(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
4755
5031
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
4756
5032
|
axisRef(categoryAxis, categoryProp);
|
|
4757
5033
|
const yAxisRaw = values.get("yAxis");
|
|
4758
5034
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
4759
5035
|
for (const a of yAxisList) {
|
|
4760
|
-
axisRef(strOf(a) ?? (
|
|
5036
|
+
axisRef(strOf(a) ?? (isRec18(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
4761
5037
|
}
|
|
4762
5038
|
const series = values.get("series");
|
|
4763
5039
|
if (Array.isArray(series)) {
|
|
4764
5040
|
for (const s of series) {
|
|
4765
|
-
if (!
|
|
5041
|
+
if (!isRec18(s)) continue;
|
|
4766
5042
|
const dataKey = strOf(s.dataKey);
|
|
4767
5043
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
4768
5044
|
}
|
|
@@ -4818,7 +5094,7 @@ function subformFieldRefs(value, basePath) {
|
|
|
4818
5094
|
if (!Array.isArray(value)) return { child, parent };
|
|
4819
5095
|
for (let i = 0; i < value.length; i++) {
|
|
4820
5096
|
const sub = value[i];
|
|
4821
|
-
if (!
|
|
5097
|
+
if (!isRec18(sub)) continue;
|
|
4822
5098
|
const at = (key) => `${basePath}[${i}].${key}`;
|
|
4823
5099
|
child.push({
|
|
4824
5100
|
objectName: strOf(sub.childObject),
|
|
@@ -4863,20 +5139,20 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4863
5139
|
}
|
|
4864
5140
|
for (const key of spec.nestedFields ?? []) {
|
|
4865
5141
|
const v = readable(key);
|
|
4866
|
-
if (
|
|
5142
|
+
if (isRec18(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
4867
5143
|
}
|
|
4868
5144
|
for (const key of spec.sections ?? []) {
|
|
4869
5145
|
const v = readable(key);
|
|
4870
5146
|
if (!Array.isArray(v)) continue;
|
|
4871
5147
|
for (let i = 0; i < v.length; i++) {
|
|
4872
5148
|
const section = v[i];
|
|
4873
|
-
if (!
|
|
5149
|
+
if (!isRec18(section)) continue;
|
|
4874
5150
|
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
4875
5151
|
}
|
|
4876
5152
|
}
|
|
4877
5153
|
for (const key of spec.keyedByField ?? []) {
|
|
4878
5154
|
const v = readable(key);
|
|
4879
|
-
if (!
|
|
5155
|
+
if (!isRec18(v)) continue;
|
|
4880
5156
|
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
4881
5157
|
}
|
|
4882
5158
|
for (const key of spec.filterArrays ?? []) {
|
|
@@ -4884,22 +5160,30 @@ function reactFieldRefs(spec, values, basePath) {
|
|
|
4884
5160
|
}
|
|
4885
5161
|
return { own, queried };
|
|
4886
5162
|
}
|
|
4887
|
-
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
5163
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors) {
|
|
4888
5164
|
const objectName = strOf(values.get("objectName"));
|
|
4889
5165
|
const out = [];
|
|
4890
5166
|
const spec = REACT_FIELD_SPECS[tag];
|
|
4891
5167
|
if (spec) {
|
|
4892
5168
|
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
4893
|
-
out.push(
|
|
4894
|
-
|
|
5169
|
+
out.push(
|
|
5170
|
+
...checkFieldRefs(own, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5171
|
+
);
|
|
5172
|
+
out.push(
|
|
5173
|
+
...checkFieldRefs(queried, objectName, objectFields, where, "queried", unprovisionedAnchors)
|
|
5174
|
+
);
|
|
4895
5175
|
}
|
|
4896
5176
|
if (tag === "ObjectForm") {
|
|
4897
5177
|
const raw = values.get("subforms");
|
|
4898
5178
|
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
4899
5179
|
for (const sub of subs.child) {
|
|
4900
|
-
out.push(
|
|
5180
|
+
out.push(
|
|
5181
|
+
...checkFieldRefs(sub.refs, sub.objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5182
|
+
);
|
|
4901
5183
|
}
|
|
4902
|
-
out.push(
|
|
5184
|
+
out.push(
|
|
5185
|
+
...checkFieldRefs(subs.parent, objectName, objectFields, where, "skipped", unprovisionedAnchors)
|
|
5186
|
+
);
|
|
4903
5187
|
}
|
|
4904
5188
|
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
4905
5189
|
if (schemaType && COMPONENT_FIELD_SPECS[schemaType]) {
|
|
@@ -4908,7 +5192,9 @@ function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
|
4908
5192
|
componentFieldRefs(schemaType, readableProps(values), path, PATH_SEP) ?? [],
|
|
4909
5193
|
objectName,
|
|
4910
5194
|
objectFields,
|
|
4911
|
-
where
|
|
5195
|
+
where,
|
|
5196
|
+
"skipped",
|
|
5197
|
+
unprovisionedAnchors
|
|
4912
5198
|
)
|
|
4913
5199
|
);
|
|
4914
5200
|
}
|
|
@@ -4940,8 +5226,9 @@ function localComponentNames(tsc, sf) {
|
|
|
4940
5226
|
function validateReactPageProps(stack) {
|
|
4941
5227
|
const findings = [];
|
|
4942
5228
|
const objectFields = indexObjectFields(stack);
|
|
5229
|
+
const unprovisionedAnchors = indexUnprovisionedAnchors(stack);
|
|
4943
5230
|
const searchTargets = indexObjectSearchTargets(stack);
|
|
4944
|
-
const pages =
|
|
5231
|
+
const pages = asArray24(stack.pages);
|
|
4945
5232
|
for (let p = 0; p < pages.length; p++) {
|
|
4946
5233
|
const page = pages[p];
|
|
4947
5234
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5022,7 +5309,7 @@ function validateReactPageProps(stack) {
|
|
|
5022
5309
|
}
|
|
5023
5310
|
}
|
|
5024
5311
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
5025
|
-
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
5312
|
+
checkObjectChart({ values, where, path }, objectFields, findings, unprovisionedAnchors);
|
|
5026
5313
|
}
|
|
5027
5314
|
if (tag === "ListView" && !hasSpread) {
|
|
5028
5315
|
findings.push(
|
|
@@ -5038,7 +5325,7 @@ function validateReactPageProps(stack) {
|
|
|
5038
5325
|
}
|
|
5039
5326
|
if (!hasSpread) {
|
|
5040
5327
|
findings.push(
|
|
5041
|
-
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
5328
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path, unprovisionedAnchors)
|
|
5042
5329
|
);
|
|
5043
5330
|
}
|
|
5044
5331
|
}
|
|
@@ -5065,6 +5352,15 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
5065
5352
|
// `action` is deliberately absent (validateActionNameRefs owns it) and so is
|
|
5066
5353
|
// `component` (an unregistered ref renders a named diagnostic, not silence).
|
|
5067
5354
|
{ name: "validateNavTargetRefs", run: validateNavTargetRefs },
|
|
5355
|
+
// [#7912] The THIRD question about a nav entry, after "does the target
|
|
5356
|
+
// resolve?" (above) and "is it granted?" (`validateNavAccess`): can the
|
|
5357
|
+
// destination serve at all? An object's own `enable` block can make its list
|
|
5358
|
+
// answer 404/405 for every persona, and no gate authorable on the entry
|
|
5359
|
+
// expresses that — which is how #7544's dead row survived review for a year.
|
|
5360
|
+
// The server now prunes such an entry from the `/meta` payload; the
|
|
5361
|
+
// maintainer ruling of 2026-08-12 makes THIS the mandatory companion, so the
|
|
5362
|
+
// prune is never silent to the author who wrote the row.
|
|
5363
|
+
{ name: "validateNavObjectServability", run: validateNavObjectServability },
|
|
5068
5364
|
{ name: "validateTranslationReferences", run: validateTranslationReferences },
|
|
5069
5365
|
// The same family from the other end (#5417). Its sibling above asks "does
|
|
5070
5366
|
// this bundle key resolve?"; this one asks "is there a key at all?" — a form
|
|
@@ -5163,13 +5459,13 @@ import { ComponentPropsMap } from "@objectstack/spec/ui";
|
|
|
5163
5459
|
import { lintUnknownKeysAgainstSchema } from "@objectstack/spec";
|
|
5164
5460
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5165
5461
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5166
|
-
function
|
|
5462
|
+
function isRec19(v) {
|
|
5167
5463
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5168
5464
|
}
|
|
5169
|
-
function
|
|
5465
|
+
function strName18(v) {
|
|
5170
5466
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5171
5467
|
}
|
|
5172
|
-
function
|
|
5468
|
+
function asArray25(v) {
|
|
5173
5469
|
if (Array.isArray(v)) return v;
|
|
5174
5470
|
if (v && typeof v === "object") {
|
|
5175
5471
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5180,23 +5476,36 @@ var PROPS_SCHEMAS = ComponentPropsMap;
|
|
|
5180
5476
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5181
5477
|
function suppliedByDataSource(issue, component) {
|
|
5182
5478
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5183
|
-
const dataSource =
|
|
5184
|
-
return
|
|
5479
|
+
const dataSource = isRec19(component.dataSource) ? component.dataSource : void 0;
|
|
5480
|
+
return strName18(dataSource?.object) !== void 0;
|
|
5481
|
+
}
|
|
5482
|
+
function unrecognizedKeysFromUnionArm(issue) {
|
|
5483
|
+
if (issue.code !== "invalid_union") return void 0;
|
|
5484
|
+
const arms = issue.errors;
|
|
5485
|
+
if (!arms || arms.length === 0) return void 0;
|
|
5486
|
+
let found;
|
|
5487
|
+
for (const arm of arms) {
|
|
5488
|
+
const keyIssues = arm.filter((inner) => inner.code === "unrecognized_keys");
|
|
5489
|
+
if (keyIssues.length === 0) continue;
|
|
5490
|
+
if (keyIssues.length !== arm.length || found) return void 0;
|
|
5491
|
+
found = keyIssues[0];
|
|
5492
|
+
}
|
|
5493
|
+
return found;
|
|
5185
5494
|
}
|
|
5186
5495
|
function validateComponentProps(stack) {
|
|
5187
5496
|
const findings = [];
|
|
5188
|
-
if (!
|
|
5189
|
-
const pages =
|
|
5497
|
+
if (!isRec19(stack)) return findings;
|
|
5498
|
+
const pages = asArray25(stack.pages);
|
|
5190
5499
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5191
5500
|
const page = pages[pi];
|
|
5192
|
-
if (!
|
|
5193
|
-
const pageName =
|
|
5501
|
+
if (!isRec19(page)) continue;
|
|
5502
|
+
const pageName = strName18(page.name) ?? `#${pi}`;
|
|
5194
5503
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5195
|
-
const type =
|
|
5504
|
+
const type = strName18(component.type);
|
|
5196
5505
|
if (!type) continue;
|
|
5197
5506
|
const schema = PROPS_SCHEMAS[type];
|
|
5198
5507
|
if (!schema) continue;
|
|
5199
|
-
const props =
|
|
5508
|
+
const props = isRec19(component.properties) ? component.properties : void 0;
|
|
5200
5509
|
if (!props) continue;
|
|
5201
5510
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5202
5511
|
const base = `${path}.properties`;
|
|
@@ -5215,6 +5524,20 @@ function validateComponentProps(stack) {
|
|
|
5215
5524
|
for (const issue of parsed.error?.issues ?? []) {
|
|
5216
5525
|
if (suppliedByDataSource(issue, component)) continue;
|
|
5217
5526
|
const at = issue.path.length ? `${base}.${issue.path.join(".")}` : base;
|
|
5527
|
+
const armIssue = unrecognizedKeysFromUnionArm(issue);
|
|
5528
|
+
if (armIssue) {
|
|
5529
|
+
for (const key of armIssue.keys ?? []) {
|
|
5530
|
+
findings.push({
|
|
5531
|
+
severity: "warning",
|
|
5532
|
+
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
5533
|
+
where,
|
|
5534
|
+
path: `${at}.${key}`,
|
|
5535
|
+
message: `\`${key}\` is not a prop \`${type}\` declares (ComponentPropsMap, @objectstack/spec/ui): ${armIssue.message}`,
|
|
5536
|
+
hint: `Remove \`${key}\`, or declare it on \`${type}\`'s props schema if the component honours it.`
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
5539
|
+
continue;
|
|
5540
|
+
}
|
|
5218
5541
|
if (issue.code === "unrecognized_keys") {
|
|
5219
5542
|
for (const key of issue.keys ?? []) {
|
|
5220
5543
|
findings.push({
|
|
@@ -5445,7 +5768,7 @@ function looksLikeTailwind(className) {
|
|
|
5445
5768
|
return false;
|
|
5446
5769
|
});
|
|
5447
5770
|
}
|
|
5448
|
-
function
|
|
5771
|
+
function asArray26(v) {
|
|
5449
5772
|
if (Array.isArray(v)) return v;
|
|
5450
5773
|
if (v && typeof v === "object") {
|
|
5451
5774
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5538,13 +5861,13 @@ function checkNode(node, pageName, path, findings) {
|
|
|
5538
5861
|
}
|
|
5539
5862
|
function validateResponsiveStyles(stack) {
|
|
5540
5863
|
const findings = [];
|
|
5541
|
-
const pages =
|
|
5864
|
+
const pages = asArray26(stack.pages);
|
|
5542
5865
|
for (let p = 0; p < pages.length; p++) {
|
|
5543
5866
|
const page = pages[p];
|
|
5544
5867
|
const pageName = typeof page.name === "string" ? page.name : `pages[${p}]`;
|
|
5545
|
-
const regions =
|
|
5868
|
+
const regions = asArray26(page.regions);
|
|
5546
5869
|
for (let r = 0; r < regions.length; r++) {
|
|
5547
|
-
const components =
|
|
5870
|
+
const components = asArray26(regions[r].components);
|
|
5548
5871
|
for (let c = 0; c < components.length; c++) {
|
|
5549
5872
|
checkNode(components[c], pageName, `pages[${p}].regions[${r}].components[${c}]`, findings);
|
|
5550
5873
|
}
|
|
@@ -5555,10 +5878,10 @@ function validateResponsiveStyles(stack) {
|
|
|
5555
5878
|
|
|
5556
5879
|
// src/validate-jsx-pages.ts
|
|
5557
5880
|
import { parseJsx, compile } from "@objectstack/sdui-parser";
|
|
5558
|
-
var
|
|
5881
|
+
var asArray27 = (v) => Array.isArray(v) ? v : [];
|
|
5559
5882
|
function validateJsxPages(stack, opts = {}) {
|
|
5560
5883
|
const findings = [];
|
|
5561
|
-
const pages =
|
|
5884
|
+
const pages = asArray27(stack.pages);
|
|
5562
5885
|
for (let p = 0; p < pages.length; p++) {
|
|
5563
5886
|
const page = pages[p];
|
|
5564
5887
|
if (!page || page.kind !== "html" && page.kind !== "jsx") continue;
|
|
@@ -5605,10 +5928,10 @@ function loadSucraseTransform() {
|
|
|
5605
5928
|
}
|
|
5606
5929
|
return cachedTransform;
|
|
5607
5930
|
}
|
|
5608
|
-
var
|
|
5931
|
+
var asArray28 = (v) => Array.isArray(v) ? v : [];
|
|
5609
5932
|
function validateReactPages(stack) {
|
|
5610
5933
|
const findings = [];
|
|
5611
|
-
const pages =
|
|
5934
|
+
const pages = asArray28(stack.pages);
|
|
5612
5935
|
for (let p = 0; p < pages.length; p++) {
|
|
5613
5936
|
const page = pages[p];
|
|
5614
5937
|
if (!page || page.kind !== "react") continue;
|
|
@@ -5645,11 +5968,11 @@ function validateReactPages(stack) {
|
|
|
5645
5968
|
|
|
5646
5969
|
// src/validate-page-source-styling.ts
|
|
5647
5970
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
5648
|
-
var
|
|
5971
|
+
var asArray29 = (v) => Array.isArray(v) ? v : [];
|
|
5649
5972
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
5650
5973
|
function validatePageSourceStyling(stack) {
|
|
5651
5974
|
const findings = [];
|
|
5652
|
-
const pages =
|
|
5975
|
+
const pages = asArray29(stack.pages);
|
|
5653
5976
|
for (let p = 0; p < pages.length; p++) {
|
|
5654
5977
|
const page = pages[p];
|
|
5655
5978
|
if (!page) continue;
|
|
@@ -5677,7 +6000,7 @@ function validatePageSourceStyling(stack) {
|
|
|
5677
6000
|
// src/validate-capability-references.ts
|
|
5678
6001
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
5679
6002
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
5680
|
-
function
|
|
6003
|
+
function asArray30(v) {
|
|
5681
6004
|
if (Array.isArray(v)) return v;
|
|
5682
6005
|
if (v && typeof v === "object") {
|
|
5683
6006
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5702,13 +6025,13 @@ function validateCapabilityReferences(stack) {
|
|
|
5702
6025
|
const findings = [];
|
|
5703
6026
|
if (!stack || typeof stack !== "object") return findings;
|
|
5704
6027
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
5705
|
-
for (const cap of
|
|
6028
|
+
for (const cap of asArray30(stack.capabilities)) {
|
|
5706
6029
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
5707
6030
|
}
|
|
5708
|
-
for (const ps of
|
|
6031
|
+
for (const ps of asArray30(stack.permissions)) {
|
|
5709
6032
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
5710
6033
|
}
|
|
5711
|
-
for (const seed of
|
|
6034
|
+
for (const seed of asArray30(stack.data)) {
|
|
5712
6035
|
if (seed.object !== "sys_capability") continue;
|
|
5713
6036
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
5714
6037
|
const name = rec?.name;
|
|
@@ -5727,7 +6050,7 @@ function validateCapabilityReferences(stack) {
|
|
|
5727
6050
|
hint
|
|
5728
6051
|
});
|
|
5729
6052
|
};
|
|
5730
|
-
const objects =
|
|
6053
|
+
const objects = asArray30(stack.objects);
|
|
5731
6054
|
for (let i = 0; i < objects.length; i++) {
|
|
5732
6055
|
const obj = objects[i];
|
|
5733
6056
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -5736,27 +6059,27 @@ function validateCapabilityReferences(stack) {
|
|
|
5736
6059
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
5737
6060
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
5738
6061
|
}
|
|
5739
|
-
const fields =
|
|
6062
|
+
const fields = asArray30(obj.fields);
|
|
5740
6063
|
for (const f of fields) {
|
|
5741
6064
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
5742
6065
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
5743
6066
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
5744
6067
|
}
|
|
5745
6068
|
}
|
|
5746
|
-
for (const [ai, action] of
|
|
6069
|
+
for (const [ai, action] of asArray30(obj.actions).entries()) {
|
|
5747
6070
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
5748
6071
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5749
6072
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
5750
6073
|
}
|
|
5751
6074
|
}
|
|
5752
6075
|
}
|
|
5753
|
-
for (const [i, action] of
|
|
6076
|
+
for (const [i, action] of asArray30(stack.actions).entries()) {
|
|
5754
6077
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
5755
6078
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
5756
6079
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
5757
6080
|
}
|
|
5758
6081
|
}
|
|
5759
|
-
const apps =
|
|
6082
|
+
const apps = asArray30(stack.apps);
|
|
5760
6083
|
for (let i = 0; i < apps.length; i++) {
|
|
5761
6084
|
const app = apps[i];
|
|
5762
6085
|
if (!app || typeof app !== "object") continue;
|
|
@@ -5791,7 +6114,7 @@ var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-inval
|
|
|
5791
6114
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
5792
6115
|
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
5793
6116
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
5794
|
-
function
|
|
6117
|
+
function asArray31(v) {
|
|
5795
6118
|
if (Array.isArray(v)) return v;
|
|
5796
6119
|
if (v && typeof v === "object") {
|
|
5797
6120
|
return Object.entries(v).map(([name, def]) => ({
|
|
@@ -5819,10 +6142,10 @@ function startNodeOf(flow) {
|
|
|
5819
6142
|
}
|
|
5820
6143
|
function validateFlowTriggerReadiness(stack) {
|
|
5821
6144
|
const findings = [];
|
|
5822
|
-
const flows =
|
|
6145
|
+
const flows = asArray31(stack.flows);
|
|
5823
6146
|
if (flows.length === 0) return findings;
|
|
5824
6147
|
const objectNames = new Set(
|
|
5825
|
-
|
|
6148
|
+
asArray31(stack.objects).map((o) => typeof o.name === "string" ? o.name : void 0).filter((n) => !!n)
|
|
5826
6149
|
);
|
|
5827
6150
|
flows.forEach((flow, flowIndex) => {
|
|
5828
6151
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
@@ -5995,7 +6318,7 @@ var TYPE_FIX = {
|
|
|
5995
6318
|
business_unit: "department",
|
|
5996
6319
|
bu: "department"
|
|
5997
6320
|
};
|
|
5998
|
-
function
|
|
6321
|
+
function asArray32(v) {
|
|
5999
6322
|
if (Array.isArray(v)) return v;
|
|
6000
6323
|
if (v && typeof v === "object") {
|
|
6001
6324
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6005,7 +6328,7 @@ function asArray31(v) {
|
|
|
6005
6328
|
function validateApprovalApprovers(stack) {
|
|
6006
6329
|
const findings = [];
|
|
6007
6330
|
if (!stack || typeof stack !== "object") return findings;
|
|
6008
|
-
const flows =
|
|
6331
|
+
const flows = asArray32(stack.flows);
|
|
6009
6332
|
const validTypes = new Set(ApproverType.options);
|
|
6010
6333
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
6011
6334
|
const flow = flows[fi];
|
|
@@ -6188,7 +6511,7 @@ function validateApprovalApprovers(stack) {
|
|
|
6188
6511
|
import { objectTitleCompleteness } from "@objectstack/spec/data";
|
|
6189
6512
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
6190
6513
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
6191
|
-
function
|
|
6514
|
+
function asArray33(v) {
|
|
6192
6515
|
if (Array.isArray(v)) return v;
|
|
6193
6516
|
if (v && typeof v === "object") {
|
|
6194
6517
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6197,7 +6520,7 @@ function asArray32(v) {
|
|
|
6197
6520
|
}
|
|
6198
6521
|
function validateRecordTitle(stack) {
|
|
6199
6522
|
const findings = [];
|
|
6200
|
-
const objects =
|
|
6523
|
+
const objects = asArray33(stack.objects);
|
|
6201
6524
|
for (let i = 0; i < objects.length; i++) {
|
|
6202
6525
|
const obj = objects[i];
|
|
6203
6526
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -6233,7 +6556,8 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
6233
6556
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
6234
6557
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
6235
6558
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
6236
|
-
|
|
6559
|
+
var SEMANTIC_ROLE_FIELD_UNPROVISIONED = "semantic-role-field-unprovisioned";
|
|
6560
|
+
function asArray34(v) {
|
|
6237
6561
|
if (Array.isArray(v)) return v;
|
|
6238
6562
|
if (v && typeof v === "object") {
|
|
6239
6563
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -6242,7 +6566,7 @@ function asArray33(v) {
|
|
|
6242
6566
|
}
|
|
6243
6567
|
function validateSemanticRoles(stack) {
|
|
6244
6568
|
const findings = [];
|
|
6245
|
-
const objects =
|
|
6569
|
+
const objects = asArray34(stack.objects);
|
|
6246
6570
|
for (let i = 0; i < objects.length; i++) {
|
|
6247
6571
|
const obj = objects[i];
|
|
6248
6572
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -6251,6 +6575,15 @@ function validateSemanticRoles(stack) {
|
|
|
6251
6575
|
const path = `objects[${i}]`;
|
|
6252
6576
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? obj.fields : {};
|
|
6253
6577
|
const fieldNames = /* @__PURE__ */ new Set([...Object.keys(fields), ...injectedColumnsFor(obj)]);
|
|
6578
|
+
const unprovisioned = unprovisionedInjectedColumnsFor(obj);
|
|
6579
|
+
const unprovisionedPointer = (slot, entry) => ({
|
|
6580
|
+
severity: "warning",
|
|
6581
|
+
rule: SEMANTIC_ROLE_FIELD_UNPROVISIONED,
|
|
6582
|
+
where,
|
|
6583
|
+
path: `${path}.${slot}`,
|
|
6584
|
+
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.`,
|
|
6585
|
+
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.`
|
|
6586
|
+
});
|
|
6254
6587
|
const declaredGroups = new Set(
|
|
6255
6588
|
(Array.isArray(obj.fieldGroups) ? obj.fieldGroups : []).filter((g) => !!g && typeof g === "object").map((g) => g.key).filter((k) => typeof k === "string" && k.length > 0)
|
|
6256
6589
|
);
|
|
@@ -6292,10 +6625,16 @@ function validateSemanticRoles(stack) {
|
|
|
6292
6625
|
message: `${objName}: stageField "${stage}" is not a field on this object \u2014 consumers fall back to heuristic stage detection`,
|
|
6293
6626
|
hint: `Point stageField at an existing select/status field, or set stageField: false to declare the object has no linear lifecycle.`
|
|
6294
6627
|
});
|
|
6628
|
+
} else if (typeof stage === "string" && unprovisioned.has(stage)) {
|
|
6629
|
+
findings.push(unprovisionedPointer("stageField", stage));
|
|
6295
6630
|
}
|
|
6296
6631
|
const highlights = Array.isArray(obj.highlightFields) ? obj.highlightFields : Array.isArray(obj.compactLayout) ? obj.compactLayout : [];
|
|
6297
6632
|
for (const entry of highlights) {
|
|
6298
|
-
if (typeof entry !== "string" || entry.length === 0
|
|
6633
|
+
if (typeof entry !== "string" || entry.length === 0) continue;
|
|
6634
|
+
if (fieldNames.has(entry)) {
|
|
6635
|
+
if (unprovisioned.has(entry)) findings.push(unprovisionedPointer("highlightFields", entry));
|
|
6636
|
+
continue;
|
|
6637
|
+
}
|
|
6299
6638
|
findings.push({
|
|
6300
6639
|
severity: "warning",
|
|
6301
6640
|
rule: SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
@@ -6337,17 +6676,17 @@ function validateSemanticRoles(stack) {
|
|
|
6337
6676
|
// src/validate-form-layout.ts
|
|
6338
6677
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
6339
6678
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
6340
|
-
function
|
|
6679
|
+
function asArray35(v) {
|
|
6341
6680
|
if (Array.isArray(v)) return v;
|
|
6342
6681
|
if (v && typeof v === "object") {
|
|
6343
6682
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6344
6683
|
}
|
|
6345
6684
|
return [];
|
|
6346
6685
|
}
|
|
6347
|
-
function
|
|
6686
|
+
function isRec20(v) {
|
|
6348
6687
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6349
6688
|
}
|
|
6350
|
-
function
|
|
6689
|
+
function strName19(v) {
|
|
6351
6690
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6352
6691
|
}
|
|
6353
6692
|
function fieldNameOf(entry) {
|
|
@@ -6361,14 +6700,14 @@ function fieldNameOf(entry) {
|
|
|
6361
6700
|
function validateFormLayout(stack) {
|
|
6362
6701
|
const findings = [];
|
|
6363
6702
|
const objectFields = /* @__PURE__ */ new Map();
|
|
6364
|
-
for (const obj of
|
|
6703
|
+
for (const obj of asArray35(stack.objects)) {
|
|
6365
6704
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6366
6705
|
if (!name) continue;
|
|
6367
6706
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
6368
6707
|
objectFields.set(name, new Set(fields));
|
|
6369
6708
|
}
|
|
6370
6709
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6371
|
-
const viewName =
|
|
6710
|
+
const viewName = strName19(view.name) ?? strName19(view.object) ?? viewPath;
|
|
6372
6711
|
const containerObject = viewObjectName(view);
|
|
6373
6712
|
for (const site of formViewSites(view, viewPath)) {
|
|
6374
6713
|
const objName = viewObjectName(site.view) ?? containerObject;
|
|
@@ -6378,7 +6717,7 @@ function validateFormLayout(stack) {
|
|
|
6378
6717
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6379
6718
|
for (let s = 0; s < sections.length; s++) {
|
|
6380
6719
|
const sec = sections[s];
|
|
6381
|
-
const secFields =
|
|
6720
|
+
const secFields = isRec20(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
6382
6721
|
for (let f = 0; f < secFields.length; f++) {
|
|
6383
6722
|
const entry = secFields[f];
|
|
6384
6723
|
const fname = fieldNameOf(entry);
|
|
@@ -6393,7 +6732,7 @@ function validateFormLayout(stack) {
|
|
|
6393
6732
|
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
6394
6733
|
});
|
|
6395
6734
|
}
|
|
6396
|
-
const colSpan =
|
|
6735
|
+
const colSpan = isRec20(entry) ? entry.colSpan : void 0;
|
|
6397
6736
|
if (colSpan != null) {
|
|
6398
6737
|
findings.push({
|
|
6399
6738
|
severity: "warning",
|
|
@@ -6506,9 +6845,67 @@ function validateSeedStateMachine(stack) {
|
|
|
6506
6845
|
import {
|
|
6507
6846
|
collectCelRootIdentifiers as collectCelRootIdentifiers3,
|
|
6508
6847
|
firstUndeclaredReference,
|
|
6509
|
-
parseCelToAst as
|
|
6848
|
+
parseCelToAst as parseCelToAst3,
|
|
6510
6849
|
parseCelToAstWithReason
|
|
6511
6850
|
} from "@objectstack/formula";
|
|
6851
|
+
|
|
6852
|
+
// src/predicate-rhs-position.ts
|
|
6853
|
+
function isNode2(v) {
|
|
6854
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6855
|
+
}
|
|
6856
|
+
var EQUALITY_OPS = /* @__PURE__ */ new Set(["==", "!="]);
|
|
6857
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6858
|
+
function bareId(node) {
|
|
6859
|
+
if (!isNode2(node)) return null;
|
|
6860
|
+
return node.op === "id" && typeof node.args === "string" ? node.args : null;
|
|
6861
|
+
}
|
|
6862
|
+
function bareRhsOnlyIdentifiers(ast) {
|
|
6863
|
+
const rhs = /* @__PURE__ */ new Set();
|
|
6864
|
+
const elsewhere = /* @__PURE__ */ new Set();
|
|
6865
|
+
const walk = (node, suppressible) => {
|
|
6866
|
+
if (Array.isArray(node)) {
|
|
6867
|
+
for (const child of node) walk(child, suppressible);
|
|
6868
|
+
return;
|
|
6869
|
+
}
|
|
6870
|
+
if (!isNode2(node)) return;
|
|
6871
|
+
const args = node.args;
|
|
6872
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6873
|
+
walk(args[1], suppressible);
|
|
6874
|
+
walk(args[2], false);
|
|
6875
|
+
return;
|
|
6876
|
+
}
|
|
6877
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
6878
|
+
const right = suppressible ? bareId(args[1]) : null;
|
|
6879
|
+
walk(args[0], suppressible);
|
|
6880
|
+
if (right !== null) {
|
|
6881
|
+
rhs.add(right);
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6884
|
+
walk(args[1], suppressible);
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
const name = bareId(node);
|
|
6888
|
+
if (name !== null) {
|
|
6889
|
+
elsewhere.add(name);
|
|
6890
|
+
return;
|
|
6891
|
+
}
|
|
6892
|
+
walk(args, suppressible);
|
|
6893
|
+
};
|
|
6894
|
+
walk(ast, true);
|
|
6895
|
+
for (const name of elsewhere) rhs.delete(name);
|
|
6896
|
+
return rhs;
|
|
6897
|
+
}
|
|
6898
|
+
function isRec21(v) {
|
|
6899
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6900
|
+
}
|
|
6901
|
+
function schemaIdOf(view) {
|
|
6902
|
+
const data = view.data;
|
|
6903
|
+
if (!isRec21(data)) return void 0;
|
|
6904
|
+
if (data.provider !== "schema") return void 0;
|
|
6905
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6906
|
+
}
|
|
6907
|
+
|
|
6908
|
+
// src/validate-visibility-predicates.ts
|
|
6512
6909
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
6513
6910
|
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
6514
6911
|
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
@@ -6556,7 +6953,7 @@ function boundName(overrun) {
|
|
|
6556
6953
|
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
6557
6954
|
}
|
|
6558
6955
|
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
6559
|
-
function
|
|
6956
|
+
function isNode3(v) {
|
|
6560
6957
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6561
6958
|
}
|
|
6562
6959
|
function namespaceRoots(node, out) {
|
|
@@ -6564,22 +6961,27 @@ function namespaceRoots(node, out) {
|
|
|
6564
6961
|
for (const child of node) namespaceRoots(child, out);
|
|
6565
6962
|
return;
|
|
6566
6963
|
}
|
|
6567
|
-
if (!
|
|
6964
|
+
if (!isNode3(node)) return;
|
|
6568
6965
|
const args = node.args;
|
|
6569
6966
|
if (Array.isArray(args)) {
|
|
6570
6967
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6571
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
6968
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6572
6969
|
out.add(receiver.args);
|
|
6573
6970
|
}
|
|
6574
6971
|
}
|
|
6575
6972
|
namespaceRoots(args, out);
|
|
6576
6973
|
}
|
|
6577
|
-
function firstBareIdentifier(source) {
|
|
6578
|
-
const ast =
|
|
6974
|
+
function firstBareIdentifier(source, literalRhs) {
|
|
6975
|
+
const ast = parseCelToAst3(source);
|
|
6579
6976
|
if (!ast) return null;
|
|
6580
6977
|
const rooted = /* @__PURE__ */ new Set();
|
|
6581
6978
|
namespaceRoots(ast, rooted);
|
|
6582
|
-
|
|
6979
|
+
const literalSlot = literalRhs ? bareRhsOnlyIdentifiers(ast) : [];
|
|
6980
|
+
return firstUndeclaredReference(source, [
|
|
6981
|
+
...VIEW_PAGE_EXTRA_ROOTS,
|
|
6982
|
+
...rooted,
|
|
6983
|
+
...literalSlot
|
|
6984
|
+
]);
|
|
6583
6985
|
}
|
|
6584
6986
|
var CANONICAL_ROOT_BY_LAYER = {
|
|
6585
6987
|
runtime: "record",
|
|
@@ -6588,16 +6990,16 @@ var CANONICAL_ROOT_BY_LAYER = {
|
|
|
6588
6990
|
var MISLAYER_BY_LAYER = {
|
|
6589
6991
|
runtime: {
|
|
6590
6992
|
forbiddenRoot: "data",
|
|
6591
|
-
message: "visibility predicate is rooted at `data.` \u2014 that is the metadata-editing
|
|
6993
|
+
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).",
|
|
6592
6994
|
hint: "Runtime record surfaces bind `record` + `current_user` (pages also expose `page.<var>`). Use e.g. `record.status == 'open'` instead of `data.status == 'open'`."
|
|
6593
6995
|
},
|
|
6594
6996
|
metadata: {
|
|
6595
6997
|
forbiddenRoot: "record",
|
|
6596
|
-
message: "visibility predicate is rooted at `record.` \u2014 that is the
|
|
6998
|
+
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).",
|
|
6597
6999
|
hint: "Metadata-editing forms bind `data` (the row under edit). Use e.g. `data.type == 'grid'` instead of `record.type == 'grid'`."
|
|
6598
7000
|
}
|
|
6599
7001
|
};
|
|
6600
|
-
function checkElement(el, where, path, layer, findings) {
|
|
7002
|
+
function checkElement(el, where, path, layer, findings, literalRhs = false) {
|
|
6601
7003
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
6602
7004
|
const source = predicateSource(raw);
|
|
6603
7005
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -6635,7 +7037,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6635
7037
|
});
|
|
6636
7038
|
}
|
|
6637
7039
|
if (source && !refusal) {
|
|
6638
|
-
const bare = firstBareIdentifier(source);
|
|
7040
|
+
const bare = firstBareIdentifier(source, literalRhs);
|
|
6639
7041
|
if (bare) {
|
|
6640
7042
|
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
6641
7043
|
findings.push({
|
|
@@ -6644,7 +7046,7 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
6644
7046
|
where,
|
|
6645
7047
|
path,
|
|
6646
7048
|
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).`,
|
|
6647
|
-
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
|
|
7049
|
+
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`).")
|
|
6648
7050
|
});
|
|
6649
7051
|
}
|
|
6650
7052
|
}
|
|
@@ -6653,24 +7055,27 @@ function isFieldObject(entry) {
|
|
|
6653
7055
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
6654
7056
|
}
|
|
6655
7057
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
6656
|
-
const
|
|
7058
|
+
const declaredLayer = opts.layer ?? "runtime";
|
|
6657
7059
|
const findings = [];
|
|
6658
7060
|
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
6659
7061
|
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
6660
7062
|
for (const site of formViewSites(view, viewPath)) {
|
|
6661
7063
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
7064
|
+
const schemaBound = schemaIdOf(site.view) !== void 0;
|
|
7065
|
+
const literalRhs = schemaBound;
|
|
7066
|
+
const layer = schemaBound ? "metadata" : declaredLayer;
|
|
6662
7067
|
for (const bucket of ["sections", "groups"]) {
|
|
6663
7068
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6664
7069
|
for (let s = 0; s < sections.length; s++) {
|
|
6665
7070
|
const sec = sections[s];
|
|
6666
7071
|
if (!sec || typeof sec !== "object") continue;
|
|
6667
7072
|
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
6668
|
-
checkElement(sec, where, secPath, layer, findings);
|
|
7073
|
+
checkElement(sec, where, secPath, layer, findings, literalRhs);
|
|
6669
7074
|
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
6670
7075
|
for (let f = 0; f < secFields.length; f++) {
|
|
6671
7076
|
const entry = secFields[f];
|
|
6672
7077
|
if (isFieldObject(entry)) {
|
|
6673
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
7078
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings, literalRhs);
|
|
6674
7079
|
}
|
|
6675
7080
|
}
|
|
6676
7081
|
}
|
|
@@ -6681,21 +7086,21 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
6681
7086
|
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
6682
7087
|
const where = `page "${pageName ?? pagePath}"`;
|
|
6683
7088
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6684
|
-
checkElement(walked.component, where, walked.path,
|
|
7089
|
+
checkElement(walked.component, where, walked.path, declaredLayer, findings);
|
|
6685
7090
|
}
|
|
6686
7091
|
}
|
|
6687
7092
|
return findings;
|
|
6688
7093
|
}
|
|
6689
7094
|
|
|
6690
7095
|
// src/validate-predicate-path-refs.ts
|
|
6691
|
-
import { parseCelToAst as
|
|
7096
|
+
import { parseCelToAst as parseCelToAst4 } from "@objectstack/formula";
|
|
6692
7097
|
import { getMetadataTypeSchema } from "@objectstack/spec/kernel";
|
|
6693
7098
|
import { findClosestMatches as findClosestMatches4, formatSuggestion as formatSuggestion4 } from "@objectstack/spec";
|
|
6694
7099
|
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
6695
7100
|
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
7101
|
+
var PREDICATE_RHS_PATH_SHAPED = "predicate-rhs-path-shaped";
|
|
6696
7102
|
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
6697
7103
|
var ROOT = "data";
|
|
6698
|
-
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
6699
7104
|
function defOf(schema) {
|
|
6700
7105
|
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
6701
7106
|
const s = schema;
|
|
@@ -6788,11 +7193,11 @@ function stepInto(scope, segment) {
|
|
|
6788
7193
|
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
6789
7194
|
return { kind: "declared", next: propertyOf(u, segment) };
|
|
6790
7195
|
}
|
|
6791
|
-
function
|
|
7196
|
+
function isNode4(v) {
|
|
6792
7197
|
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
6793
7198
|
}
|
|
6794
7199
|
function memberChain(node) {
|
|
6795
|
-
if (!
|
|
7200
|
+
if (!isNode4(node)) return null;
|
|
6796
7201
|
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
6797
7202
|
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
6798
7203
|
const head = memberChain(node.args[0]);
|
|
@@ -6805,7 +7210,7 @@ function rootedPaths(node, out) {
|
|
|
6805
7210
|
for (const child of node) rootedPaths(child, out);
|
|
6806
7211
|
return;
|
|
6807
7212
|
}
|
|
6808
|
-
if (!
|
|
7213
|
+
if (!isNode4(node)) return;
|
|
6809
7214
|
if (node.op === ".") {
|
|
6810
7215
|
const chain = memberChain(node);
|
|
6811
7216
|
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
@@ -6815,23 +7220,40 @@ function rootedPaths(node, out) {
|
|
|
6815
7220
|
}
|
|
6816
7221
|
rootedPaths(node.args, out);
|
|
6817
7222
|
}
|
|
7223
|
+
var PATH_SHAPED_RHS = /^[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*$/;
|
|
7224
|
+
function equalitySites(node, out) {
|
|
7225
|
+
if (Array.isArray(node)) {
|
|
7226
|
+
for (const child of node) equalitySites(child, out);
|
|
7227
|
+
return;
|
|
7228
|
+
}
|
|
7229
|
+
if (!isNode4(node)) return;
|
|
7230
|
+
const args = node.args;
|
|
7231
|
+
if (node.op === "rcall" && Array.isArray(args) && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
7232
|
+
equalitySites(args[1], out);
|
|
7233
|
+
return;
|
|
7234
|
+
}
|
|
7235
|
+
if (typeof node.op === "string" && EQUALITY_OPS.has(node.op) && Array.isArray(args) && args.length === 2) {
|
|
7236
|
+
out.push({ op: node.op, right: args[1] });
|
|
7237
|
+
}
|
|
7238
|
+
equalitySites(args, out);
|
|
7239
|
+
}
|
|
6818
7240
|
function classifyIdentifiers(node, values, excluded) {
|
|
6819
7241
|
if (Array.isArray(node)) {
|
|
6820
7242
|
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
6821
7243
|
return;
|
|
6822
7244
|
}
|
|
6823
|
-
if (!
|
|
7245
|
+
if (!isNode4(node)) return;
|
|
6824
7246
|
const args = node.args;
|
|
6825
7247
|
if (Array.isArray(args)) {
|
|
6826
7248
|
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
6827
|
-
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") &&
|
|
7249
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode4(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
6828
7250
|
excluded.add(receiver.args);
|
|
6829
7251
|
}
|
|
6830
7252
|
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
6831
7253
|
const macroArgs = args[2];
|
|
6832
7254
|
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
6833
7255
|
const bound = macroArgs[0];
|
|
6834
|
-
if (
|
|
7256
|
+
if (isNode4(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
6835
7257
|
excluded.add(bound.args);
|
|
6836
7258
|
}
|
|
6837
7259
|
}
|
|
@@ -6850,17 +7272,11 @@ function predicateSource2(v) {
|
|
|
6850
7272
|
}
|
|
6851
7273
|
return void 0;
|
|
6852
7274
|
}
|
|
6853
|
-
function
|
|
7275
|
+
function isRec22(v) {
|
|
6854
7276
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6855
7277
|
}
|
|
6856
|
-
function schemaIdOf(view) {
|
|
6857
|
-
const data = view.data;
|
|
6858
|
-
if (!isRec19(data)) return void 0;
|
|
6859
|
-
if (data.provider !== "schema") return void 0;
|
|
6860
|
-
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
6861
|
-
}
|
|
6862
7278
|
function checkPredicate(source, scope, where, path, findings) {
|
|
6863
|
-
const ast =
|
|
7279
|
+
const ast = parseCelToAst4(source);
|
|
6864
7280
|
if (!ast) return;
|
|
6865
7281
|
const paths = [];
|
|
6866
7282
|
rootedPaths(ast, paths);
|
|
@@ -6888,19 +7304,38 @@ function checkPredicate(source, scope, where, path, findings) {
|
|
|
6888
7304
|
}
|
|
6889
7305
|
}
|
|
6890
7306
|
const declaredHere = keysOf(scope);
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
7307
|
+
const rhsOnly = bareRhsOnlyIdentifiers(ast);
|
|
7308
|
+
if (declaredHere) {
|
|
7309
|
+
const values = /* @__PURE__ */ new Set();
|
|
7310
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
7311
|
+
classifyIdentifiers(ast, values, excluded);
|
|
7312
|
+
for (const id of values) {
|
|
7313
|
+
if (excluded.has(id) || rhsOnly.has(id) || !declaredHere.includes(id)) continue;
|
|
7314
|
+
findings.push({
|
|
7315
|
+
severity: "error",
|
|
7316
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
7317
|
+
where,
|
|
7318
|
+
path,
|
|
7319
|
+
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).`,
|
|
7320
|
+
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).`
|
|
7321
|
+
});
|
|
7322
|
+
}
|
|
7323
|
+
}
|
|
7324
|
+
const sites = [];
|
|
7325
|
+
equalitySites(ast, sites);
|
|
7326
|
+
for (const { op, right } of sites) {
|
|
7327
|
+
const chain = memberChain(right);
|
|
7328
|
+
if (!chain) continue;
|
|
7329
|
+
const text = chain.join(".");
|
|
7330
|
+
if (!PATH_SHAPED_RHS.test(text)) continue;
|
|
7331
|
+
const dotted = chain.length > 1;
|
|
6897
7332
|
findings.push({
|
|
6898
|
-
severity: "error",
|
|
6899
|
-
rule:
|
|
7333
|
+
severity: dotted ? "error" : "warning",
|
|
7334
|
+
rule: PREDICATE_RHS_PATH_SHAPED,
|
|
6900
7335
|
where,
|
|
6901
7336
|
path,
|
|
6902
|
-
message: `predicate
|
|
6903
|
-
hint: `
|
|
7337
|
+
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).`,
|
|
7338
|
+
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.`
|
|
6904
7339
|
});
|
|
6905
7340
|
}
|
|
6906
7341
|
}
|
|
@@ -6908,7 +7343,7 @@ function walkFields(entries, scope, where, base, findings, depth) {
|
|
|
6908
7343
|
if (!Array.isArray(entries) || depth > 12) return;
|
|
6909
7344
|
for (let i = 0; i < entries.length; i++) {
|
|
6910
7345
|
const entry = entries[i];
|
|
6911
|
-
if (!
|
|
7346
|
+
if (!isRec22(entry)) continue;
|
|
6912
7347
|
const path = `${base}[${i}]`;
|
|
6913
7348
|
for (const key of PREDICATE_KEYS) {
|
|
6914
7349
|
const source = predicateSource2(entry[key]);
|
|
@@ -6935,15 +7370,14 @@ function validatePredicatePathRefs(stack, opts = {}) {
|
|
|
6935
7370
|
try {
|
|
6936
7371
|
root = resolveSchema(schemaId);
|
|
6937
7372
|
} catch {
|
|
6938
|
-
|
|
7373
|
+
root = void 0;
|
|
6939
7374
|
}
|
|
6940
|
-
if (!root) continue;
|
|
6941
7375
|
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
6942
7376
|
for (const bucket of ["sections", "groups"]) {
|
|
6943
7377
|
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
6944
7378
|
for (let s = 0; s < sections.length; s++) {
|
|
6945
7379
|
const section = sections[s];
|
|
6946
|
-
if (!
|
|
7380
|
+
if (!isRec22(section)) continue;
|
|
6947
7381
|
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
6948
7382
|
for (const key of PREDICATE_KEYS) {
|
|
6949
7383
|
const source = predicateSource2(section[key]);
|
|
@@ -6974,6 +7408,7 @@ var SECURITY_MASTER_DETAIL_UNGRANTED = "security-master-detail-ungranted";
|
|
|
6974
7408
|
var SECURITY_FLS_UNQUALIFIED_KEY = "security-fls-unqualified-key";
|
|
6975
7409
|
var SECURITY_GRANT_EXPIRED_AT_AUTHORING = "security-grant-expired-at-authoring";
|
|
6976
7410
|
var SECURITY_DELEGATION_MISSING_REASON = "security-delegation-missing-reason";
|
|
7411
|
+
var SECURITY_CBP_NO_RELATION = "security-controlled-by-parent-no-relation";
|
|
6977
7412
|
var CANONICAL_OWD = ["private", "public_read", "public_read_write", "controlled_by_parent"];
|
|
6978
7413
|
var OWD_ALIAS_FIX = {
|
|
6979
7414
|
read: "public_read",
|
|
@@ -6986,7 +7421,7 @@ var OWD_WIDTH = {
|
|
|
6986
7421
|
public_read: 1,
|
|
6987
7422
|
public_read_write: 2
|
|
6988
7423
|
};
|
|
6989
|
-
function
|
|
7424
|
+
function asArray36(v) {
|
|
6990
7425
|
if (Array.isArray(v)) return v;
|
|
6991
7426
|
if (v && typeof v === "object") {
|
|
6992
7427
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7012,21 +7447,28 @@ function refOf(def) {
|
|
|
7012
7447
|
return typeof r === "string" && r ? r : void 0;
|
|
7013
7448
|
}
|
|
7014
7449
|
function firstMasterDetailField(obj) {
|
|
7015
|
-
for (const f of
|
|
7450
|
+
for (const f of asArray36(obj.fields)) {
|
|
7016
7451
|
if (f.type === "master_detail") {
|
|
7017
7452
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
7018
7453
|
}
|
|
7019
7454
|
}
|
|
7020
7455
|
return void 0;
|
|
7021
7456
|
}
|
|
7457
|
+
function resolveCbpRelation(obj) {
|
|
7458
|
+
const entries = asArray36(obj.fields);
|
|
7459
|
+
const pick = (pred) => entries.find((f) => pred(f) && refOf(f));
|
|
7460
|
+
const found = pick((f) => f.type === "master_detail" && !!f.required) ?? pick((f) => f.type === "master_detail") ?? pick((f) => f.type === "lookup" && !!f.required);
|
|
7461
|
+
if (!found) return void 0;
|
|
7462
|
+
return { field: String(found.name ?? "?"), type: String(found.type), master: refOf(found) };
|
|
7463
|
+
}
|
|
7022
7464
|
function grantsObjectAccess(p) {
|
|
7023
7465
|
return p.allowRead === true || p.allowCreate === true || p.allowEdit === true || p.allowDelete === true || p.viewAllRecords === true || p.modifyAllRecords === true;
|
|
7024
7466
|
}
|
|
7025
7467
|
function validateSecurityPosture(stack, opts) {
|
|
7026
7468
|
const findings = [];
|
|
7027
7469
|
if (!stack || typeof stack !== "object") return findings;
|
|
7028
|
-
const objects =
|
|
7029
|
-
const permissionSets =
|
|
7470
|
+
const objects = asArray36(stack.objects);
|
|
7471
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7030
7472
|
for (let i = 0; i < objects.length; i++) {
|
|
7031
7473
|
const obj = objects[i];
|
|
7032
7474
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -7064,6 +7506,16 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7064
7506
|
});
|
|
7065
7507
|
}
|
|
7066
7508
|
}
|
|
7509
|
+
if (owd === "controlled_by_parent" && !resolveCbpRelation(obj)) {
|
|
7510
|
+
findings.push({
|
|
7511
|
+
severity: "error",
|
|
7512
|
+
rule: SECURITY_CBP_NO_RELATION,
|
|
7513
|
+
where: `object "${objName}"`,
|
|
7514
|
+
path: `${objPath}.sharingModel`,
|
|
7515
|
+
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.`,
|
|
7516
|
+
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'.`
|
|
7517
|
+
});
|
|
7518
|
+
}
|
|
7067
7519
|
if (typeof external === "string") {
|
|
7068
7520
|
if (OWD_ALIAS_FIX[external]) {
|
|
7069
7521
|
findings.push({
|
|
@@ -7129,57 +7581,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7129
7581
|
}
|
|
7130
7582
|
}
|
|
7131
7583
|
}
|
|
7132
|
-
const flagRole = (kind, name, label2, where, path) => {
|
|
7133
|
-
if (identifierHasRoleToken(name)) {
|
|
7134
|
-
findings.push({
|
|
7135
|
-
severity: "error",
|
|
7136
|
-
rule: SECURITY_ROLE_WORD,
|
|
7137
|
-
where,
|
|
7138
|
-
path,
|
|
7139
|
-
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).`,
|
|
7140
|
-
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7141
|
-
});
|
|
7142
|
-
} else if (labelHasRoleWord(label2)) {
|
|
7143
|
-
findings.push({
|
|
7144
|
-
severity: "error",
|
|
7145
|
-
rule: SECURITY_ROLE_WORD,
|
|
7146
|
-
where,
|
|
7147
|
-
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7148
|
-
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7149
|
-
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7150
|
-
});
|
|
7151
|
-
}
|
|
7152
|
-
};
|
|
7153
|
-
for (let i = 0; i < objects.length; i++) {
|
|
7154
|
-
const obj = objects[i];
|
|
7155
|
-
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7156
|
-
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7157
|
-
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7158
|
-
for (const f of asArray35(obj.fields)) {
|
|
7159
|
-
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7160
|
-
}
|
|
7161
|
-
for (const [ai, action] of asArray35(obj.actions).entries()) {
|
|
7162
|
-
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7163
|
-
}
|
|
7164
|
-
}
|
|
7165
|
-
for (let i = 0; i < permissionSets.length; i++) {
|
|
7166
|
-
const ps = permissionSets[i];
|
|
7167
|
-
if (!ps || typeof ps !== "object") continue;
|
|
7168
|
-
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7169
|
-
}
|
|
7170
|
-
for (const [i, pos] of asArray35(stack.positions).entries()) {
|
|
7171
|
-
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7172
|
-
}
|
|
7173
|
-
for (const [i, app] of asArray35(stack.apps).entries()) {
|
|
7174
|
-
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7175
|
-
}
|
|
7176
|
-
for (const [i, book] of asArray35(stack.books).entries()) {
|
|
7177
|
-
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7178
|
-
}
|
|
7179
7584
|
const stackSetNames = new Set(
|
|
7180
7585
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
7181
7586
|
);
|
|
7182
|
-
for (const [i, book] of
|
|
7587
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7183
7588
|
const audience = book.audience;
|
|
7184
7589
|
if (!audience || typeof audience !== "object") continue;
|
|
7185
7590
|
const setName = audience.permissionSet;
|
|
@@ -7257,7 +7662,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7257
7662
|
}
|
|
7258
7663
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
7259
7664
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
7260
|
-
for (const [i, seed] of
|
|
7665
|
+
for (const [i, seed] of asArray36(stack.data).entries()) {
|
|
7261
7666
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
7262
7667
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
7263
7668
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -7296,13 +7701,67 @@ function validateSecurityPosture(stack, opts) {
|
|
|
7296
7701
|
}
|
|
7297
7702
|
return findings;
|
|
7298
7703
|
}
|
|
7704
|
+
function validateSecurityRoleWord(stack) {
|
|
7705
|
+
const findings = [];
|
|
7706
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
7707
|
+
const objects = asArray36(stack.objects);
|
|
7708
|
+
const permissionSets = asArray36(stack.permissions);
|
|
7709
|
+
const flagRole = (kind, name, label2, where, path) => {
|
|
7710
|
+
if (identifierHasRoleToken(name)) {
|
|
7711
|
+
findings.push({
|
|
7712
|
+
severity: "error",
|
|
7713
|
+
rule: SECURITY_ROLE_WORD,
|
|
7714
|
+
where,
|
|
7715
|
+
path,
|
|
7716
|
+
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).`,
|
|
7717
|
+
hint: `Rename using 'position' for distribution groups or a domain word (e.g. 'function', 'duty').`
|
|
7718
|
+
});
|
|
7719
|
+
} else if (labelHasRoleWord(label2)) {
|
|
7720
|
+
findings.push({
|
|
7721
|
+
severity: "error",
|
|
7722
|
+
rule: SECURITY_ROLE_WORD,
|
|
7723
|
+
where,
|
|
7724
|
+
path: `${path.replace(/\.name$/, "")}.label`,
|
|
7725
|
+
message: `${kind} label "${String(label2)}" uses the reserved word "role" (ADR-0090 D3).`,
|
|
7726
|
+
hint: `Relabel with 'Position' (distribution) or a domain word \u2014 admins must meet ONE vocabulary.`
|
|
7727
|
+
});
|
|
7728
|
+
}
|
|
7729
|
+
};
|
|
7730
|
+
for (let i = 0; i < objects.length; i++) {
|
|
7731
|
+
const obj = objects[i];
|
|
7732
|
+
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
7733
|
+
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
7734
|
+
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
7735
|
+
for (const f of asArray36(obj.fields)) {
|
|
7736
|
+
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
7737
|
+
}
|
|
7738
|
+
for (const [ai, action] of asArray36(obj.actions).entries()) {
|
|
7739
|
+
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
for (let i = 0; i < permissionSets.length; i++) {
|
|
7743
|
+
const ps = permissionSets[i];
|
|
7744
|
+
if (!ps || typeof ps !== "object") continue;
|
|
7745
|
+
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
7746
|
+
}
|
|
7747
|
+
for (const [i, pos] of asArray36(stack.positions).entries()) {
|
|
7748
|
+
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
7749
|
+
}
|
|
7750
|
+
for (const [i, app] of asArray36(stack.apps).entries()) {
|
|
7751
|
+
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
7752
|
+
}
|
|
7753
|
+
for (const [i, book] of asArray36(stack.books).entries()) {
|
|
7754
|
+
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
7755
|
+
}
|
|
7756
|
+
return findings;
|
|
7757
|
+
}
|
|
7299
7758
|
|
|
7300
7759
|
// src/validate-org-axis-red-lines.ts
|
|
7301
7760
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
7302
7761
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
7303
7762
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
7304
7763
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
7305
|
-
function
|
|
7764
|
+
function asArray37(v) {
|
|
7306
7765
|
if (Array.isArray(v)) return v;
|
|
7307
7766
|
if (v && typeof v === "object") {
|
|
7308
7767
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7332,9 +7791,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
7332
7791
|
function validateOrgAxisRedLines(stack) {
|
|
7333
7792
|
const findings = [];
|
|
7334
7793
|
const cfg = stack ?? {};
|
|
7335
|
-
const permissionSets =
|
|
7794
|
+
const permissionSets = asArray37(cfg.permissions);
|
|
7336
7795
|
permissionSets.forEach((ps, psIndex) => {
|
|
7337
|
-
|
|
7796
|
+
asArray37(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
7338
7797
|
for (const clause of ["using", "check"]) {
|
|
7339
7798
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
7340
7799
|
findings.push({
|
|
@@ -7348,7 +7807,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7348
7807
|
}
|
|
7349
7808
|
});
|
|
7350
7809
|
});
|
|
7351
|
-
|
|
7810
|
+
asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
7352
7811
|
const slots = [
|
|
7353
7812
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
7354
7813
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -7366,9 +7825,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7366
7825
|
}
|
|
7367
7826
|
});
|
|
7368
7827
|
const tenancyDisabledObjects = new Set(
|
|
7369
|
-
|
|
7828
|
+
asArray37(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
7370
7829
|
);
|
|
7371
|
-
|
|
7830
|
+
asArray37(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
7372
7831
|
const target = str(rule.object);
|
|
7373
7832
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
7374
7833
|
const sharedWith = rule.sharedWith;
|
|
@@ -7391,7 +7850,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
7391
7850
|
import { compileCelToFilter } from "@objectstack/formula";
|
|
7392
7851
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
7393
7852
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
7394
|
-
function
|
|
7853
|
+
function asArray38(v) {
|
|
7395
7854
|
if (Array.isArray(v)) return v;
|
|
7396
7855
|
if (v && typeof v === "object") {
|
|
7397
7856
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7418,7 +7877,7 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
7418
7877
|
function validateSharingRuleEnforceability(stack) {
|
|
7419
7878
|
const findings = [];
|
|
7420
7879
|
const cfg = stack ?? {};
|
|
7421
|
-
|
|
7880
|
+
asArray38(cfg.sharingRules).forEach((rule, index) => {
|
|
7422
7881
|
const input = toCompilerInput(rule.condition);
|
|
7423
7882
|
if (input === null) return;
|
|
7424
7883
|
const result = compileCelToFilter(input, { variables: {} });
|
|
@@ -7463,7 +7922,7 @@ import {
|
|
|
7463
7922
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
7464
7923
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
7465
7924
|
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
7466
|
-
function
|
|
7925
|
+
function asArray39(v) {
|
|
7467
7926
|
if (Array.isArray(v)) return v;
|
|
7468
7927
|
if (v && typeof v === "object") {
|
|
7469
7928
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -7488,8 +7947,8 @@ function consequence(clause) {
|
|
|
7488
7947
|
function validateRlsPredicateEnforceability(stack) {
|
|
7489
7948
|
const findings = [];
|
|
7490
7949
|
const cfg = stack ?? {};
|
|
7491
|
-
|
|
7492
|
-
|
|
7950
|
+
asArray39(cfg.permissions).forEach((ps, psIndex) => {
|
|
7951
|
+
asArray39(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
7493
7952
|
for (const clause of ["using", "check"]) {
|
|
7494
7953
|
const source = str3(policy[clause]);
|
|
7495
7954
|
if (!source.trim()) continue;
|
|
@@ -7548,11 +8007,11 @@ import { createRequire as createRequire4 } from "module";
|
|
|
7548
8007
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
7549
8008
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
7550
8009
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
7551
|
-
var
|
|
7552
|
-
function
|
|
7553
|
-
if (Array.isArray(v)) return v.filter(
|
|
7554
|
-
if (
|
|
7555
|
-
return Object.entries(v).filter(([, def]) =>
|
|
8010
|
+
var isRec23 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8011
|
+
function asArray40(v) {
|
|
8012
|
+
if (Array.isArray(v)) return v.filter(isRec23);
|
|
8013
|
+
if (isRec23(v)) {
|
|
8014
|
+
return Object.entries(v).filter(([, def]) => isRec23(def)).map(([name, def]) => ({ name, ...def }));
|
|
7556
8015
|
}
|
|
7557
8016
|
return [];
|
|
7558
8017
|
}
|
|
@@ -7569,7 +8028,7 @@ function loadAjv() {
|
|
|
7569
8028
|
`@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.`
|
|
7570
8029
|
);
|
|
7571
8030
|
}
|
|
7572
|
-
const ctor =
|
|
8031
|
+
const ctor = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
7573
8032
|
cachedAjv = ctor;
|
|
7574
8033
|
return ctor;
|
|
7575
8034
|
}
|
|
@@ -7584,7 +8043,7 @@ function loadAddFormats() {
|
|
|
7584
8043
|
`@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.`
|
|
7585
8044
|
);
|
|
7586
8045
|
}
|
|
7587
|
-
const plugin =
|
|
8046
|
+
const plugin = isRec23(mod) && "default" in mod ? mod.default : mod;
|
|
7588
8047
|
cachedAddFormats = plugin;
|
|
7589
8048
|
return plugin;
|
|
7590
8049
|
}
|
|
@@ -7614,17 +8073,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
7614
8073
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
7615
8074
|
for (const branch of ["then", "otherwise"]) {
|
|
7616
8075
|
const nested = rule[branch];
|
|
7617
|
-
if (
|
|
8076
|
+
if (isRec23(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
7618
8077
|
}
|
|
7619
8078
|
return out;
|
|
7620
8079
|
}
|
|
7621
8080
|
function walkObjectValidationRules(stack) {
|
|
7622
8081
|
const walked = [];
|
|
7623
|
-
if (!
|
|
7624
|
-
for (const obj of
|
|
8082
|
+
if (!isRec23(stack)) return walked;
|
|
8083
|
+
for (const obj of asArray40(stack.objects)) {
|
|
7625
8084
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
7626
8085
|
const validations = obj.validations;
|
|
7627
|
-
for (const authored of
|
|
8086
|
+
for (const authored of asArray40(validations)) {
|
|
7628
8087
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
7629
8088
|
walked.push({
|
|
7630
8089
|
rule,
|
|
@@ -7655,7 +8114,7 @@ function validateRuleCompilability(stack) {
|
|
|
7655
8114
|
});
|
|
7656
8115
|
}
|
|
7657
8116
|
}
|
|
7658
|
-
if (rule.type === "json_schema" &&
|
|
8117
|
+
if (rule.type === "json_schema" && isRec23(rule.schema)) {
|
|
7659
8118
|
try {
|
|
7660
8119
|
createRuntimeAjv().compile(rule.schema);
|
|
7661
8120
|
} catch (err) {
|
|
@@ -7675,7 +8134,7 @@ function validateRuleCompilability(stack) {
|
|
|
7675
8134
|
|
|
7676
8135
|
// src/validate-rule-schema-formats.ts
|
|
7677
8136
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
7678
|
-
var
|
|
8137
|
+
var isRec24 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7679
8138
|
var SUBSCHEMA_KEYS = [
|
|
7680
8139
|
"additionalItems",
|
|
7681
8140
|
"additionalProperties",
|
|
@@ -7699,7 +8158,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
7699
8158
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
7700
8159
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7701
8160
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
7702
|
-
if (!
|
|
8161
|
+
if (!isRec24(schema)) return;
|
|
7703
8162
|
if (typeof schema.format === "string") {
|
|
7704
8163
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
7705
8164
|
}
|
|
@@ -7718,7 +8177,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7718
8177
|
}
|
|
7719
8178
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
7720
8179
|
const value = schema[key];
|
|
7721
|
-
if (!
|
|
8180
|
+
if (!isRec24(value)) continue;
|
|
7722
8181
|
for (const [name, entry] of Object.entries(value)) {
|
|
7723
8182
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7724
8183
|
}
|
|
@@ -7726,13 +8185,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
7726
8185
|
const items = schema.items;
|
|
7727
8186
|
if (Array.isArray(items)) {
|
|
7728
8187
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
7729
|
-
} else if (
|
|
8188
|
+
} else if (isRec24(items)) {
|
|
7730
8189
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
7731
8190
|
}
|
|
7732
8191
|
const dependencies = schema.dependencies;
|
|
7733
|
-
if (
|
|
8192
|
+
if (isRec24(dependencies)) {
|
|
7734
8193
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
7735
|
-
if (!
|
|
8194
|
+
if (!isRec24(entry)) continue;
|
|
7736
8195
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
7737
8196
|
}
|
|
7738
8197
|
}
|
|
@@ -7771,7 +8230,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7771
8230
|
const findings = [];
|
|
7772
8231
|
const pending = [];
|
|
7773
8232
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
7774
|
-
if (rule.type !== "json_schema" || !
|
|
8233
|
+
if (rule.type !== "json_schema" || !isRec24(rule.schema)) continue;
|
|
7775
8234
|
const uses = [];
|
|
7776
8235
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
7777
8236
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -7797,14 +8256,14 @@ function validateRuleSchemaFormats(stack) {
|
|
|
7797
8256
|
|
|
7798
8257
|
// src/validate-action-locations.ts
|
|
7799
8258
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
7800
|
-
function
|
|
8259
|
+
function asArray41(v) {
|
|
7801
8260
|
if (Array.isArray(v)) return v;
|
|
7802
8261
|
if (v && typeof v === "object") {
|
|
7803
8262
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7804
8263
|
}
|
|
7805
8264
|
return [];
|
|
7806
8265
|
}
|
|
7807
|
-
function
|
|
8266
|
+
function strName20(v) {
|
|
7808
8267
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
7809
8268
|
}
|
|
7810
8269
|
function strList3(v) {
|
|
@@ -7818,8 +8277,8 @@ function collectNamePlacedActions(stack) {
|
|
|
7818
8277
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
7819
8278
|
for (const n of strList3(list3[key])) placed.add(n);
|
|
7820
8279
|
}
|
|
7821
|
-
for (const def of
|
|
7822
|
-
const n =
|
|
8280
|
+
for (const def of asArray41(list3.bulkActionDefs)) {
|
|
8281
|
+
const n = strName20(def?.name);
|
|
7823
8282
|
if (n) placed.add(n);
|
|
7824
8283
|
}
|
|
7825
8284
|
};
|
|
@@ -7827,12 +8286,12 @@ function collectNamePlacedActions(stack) {
|
|
|
7827
8286
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
7828
8287
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
7829
8288
|
};
|
|
7830
|
-
for (const view of
|
|
8289
|
+
for (const view of asArray41(stack.views)) {
|
|
7831
8290
|
if (!view || typeof view !== "object") continue;
|
|
7832
8291
|
harvest(view.list);
|
|
7833
8292
|
harvestListViews(view.listViews);
|
|
7834
8293
|
}
|
|
7835
|
-
for (const obj of
|
|
8294
|
+
for (const obj of asArray41(stack.objects)) {
|
|
7836
8295
|
if (!obj || typeof obj !== "object") continue;
|
|
7837
8296
|
harvestListViews(obj.listViews);
|
|
7838
8297
|
}
|
|
@@ -7845,7 +8304,7 @@ function validateActionLocations(stack) {
|
|
|
7845
8304
|
const check = (action, path) => {
|
|
7846
8305
|
if (!action || typeof action !== "object") return;
|
|
7847
8306
|
if ("locations" in action) return;
|
|
7848
|
-
const name =
|
|
8307
|
+
const name = strName20(action.name);
|
|
7849
8308
|
if (!name) return;
|
|
7850
8309
|
if (namePlaced.has(name)) return;
|
|
7851
8310
|
findings.push({
|
|
@@ -7857,13 +8316,13 @@ function validateActionLocations(stack) {
|
|
|
7857
8316
|
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."
|
|
7858
8317
|
});
|
|
7859
8318
|
};
|
|
7860
|
-
const actions =
|
|
8319
|
+
const actions = asArray41(stack.actions);
|
|
7861
8320
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
7862
|
-
const objects =
|
|
8321
|
+
const objects = asArray41(stack.objects);
|
|
7863
8322
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
7864
8323
|
const obj = objects[oi];
|
|
7865
8324
|
if (!obj || typeof obj !== "object") continue;
|
|
7866
|
-
const own =
|
|
8325
|
+
const own = asArray41(obj.actions);
|
|
7867
8326
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
7868
8327
|
}
|
|
7869
8328
|
return findings;
|
|
@@ -7876,7 +8335,7 @@ import {
|
|
|
7876
8335
|
collectFlowGraphs as collectFlowGraphs2
|
|
7877
8336
|
} from "@objectstack/spec/automation";
|
|
7878
8337
|
import { reduceFilterVerdict as reduceFilterVerdict2 } from "@objectstack/spec/data";
|
|
7879
|
-
function
|
|
8338
|
+
function asArray42(v) {
|
|
7880
8339
|
if (Array.isArray(v)) return v;
|
|
7881
8340
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7882
8341
|
return [];
|
|
@@ -8260,7 +8719,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
8260
8719
|
}
|
|
8261
8720
|
function lintFlowPatterns(stack) {
|
|
8262
8721
|
const findings = [];
|
|
8263
|
-
for (const flow of
|
|
8722
|
+
for (const flow of asArray42(stack.flows)) {
|
|
8264
8723
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
8265
8724
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
8266
8725
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -8357,7 +8816,7 @@ import { dirname, join } from "path";
|
|
|
8357
8816
|
import { existsSync, readFileSync } from "fs";
|
|
8358
8817
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
8359
8818
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
8360
|
-
function
|
|
8819
|
+
function asArray43(v) {
|
|
8361
8820
|
if (Array.isArray(v)) return v;
|
|
8362
8821
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8363
8822
|
return [];
|
|
@@ -8494,11 +8953,11 @@ function lintLivenessProperties(stack) {
|
|
|
8494
8953
|
const findings = [];
|
|
8495
8954
|
const objectWarn = loadWarnMap(dir, "object");
|
|
8496
8955
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
8497
|
-
for (const obj of
|
|
8956
|
+
for (const obj of asArray43(stack.objects)) {
|
|
8498
8957
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8499
8958
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
8500
8959
|
if (fieldWarn.size > 0) {
|
|
8501
|
-
for (const field of
|
|
8960
|
+
for (const field of asArray43(obj.fields)) {
|
|
8502
8961
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
8503
8962
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
8504
8963
|
}
|
|
@@ -8507,7 +8966,7 @@ function lintLivenessProperties(stack) {
|
|
|
8507
8966
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
8508
8967
|
const warnMap = loadWarnMap(dir, type);
|
|
8509
8968
|
if (warnMap.size === 0) continue;
|
|
8510
|
-
for (const item of
|
|
8969
|
+
for (const item of asArray43(stack[key])) {
|
|
8511
8970
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
8512
8971
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
8513
8972
|
}
|
|
@@ -8521,7 +8980,7 @@ var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
|
8521
8980
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
8522
8981
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
8523
8982
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
8524
|
-
function
|
|
8983
|
+
function asArray44(v) {
|
|
8525
8984
|
if (Array.isArray(v)) return v;
|
|
8526
8985
|
if (v && typeof v === "object") {
|
|
8527
8986
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -8530,9 +8989,9 @@ function asArray43(v) {
|
|
|
8530
8989
|
}
|
|
8531
8990
|
function lintAutonumberFormats(stack) {
|
|
8532
8991
|
const findings = [];
|
|
8533
|
-
for (const obj of
|
|
8992
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8534
8993
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8535
|
-
const fields =
|
|
8994
|
+
const fields = asArray44(obj.fields);
|
|
8536
8995
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
8537
8996
|
for (const f of fields) {
|
|
8538
8997
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -8598,7 +9057,7 @@ function lintAutonumberFormats(stack) {
|
|
|
8598
9057
|
|
|
8599
9058
|
// src/lint-view-refs.ts
|
|
8600
9059
|
import { expandViewContainerWithDiagnostics, isAggregatedViewContainer } from "@objectstack/spec";
|
|
8601
|
-
function
|
|
9060
|
+
function asArray45(v) {
|
|
8602
9061
|
if (Array.isArray(v)) return v;
|
|
8603
9062
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8604
9063
|
return [];
|
|
@@ -8626,7 +9085,7 @@ function lintViewRefs(stack) {
|
|
|
8626
9085
|
s.add(kind);
|
|
8627
9086
|
};
|
|
8628
9087
|
const containers = [];
|
|
8629
|
-
for (const v of
|
|
9088
|
+
for (const v of asArray45(stack.views)) {
|
|
8630
9089
|
if (v.viewKind) {
|
|
8631
9090
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
8632
9091
|
continue;
|
|
@@ -8635,7 +9094,7 @@ function lintViewRefs(stack) {
|
|
|
8635
9094
|
const object = viewContainerObjectName(v);
|
|
8636
9095
|
if (object) containers.push({ object, container: v });
|
|
8637
9096
|
}
|
|
8638
|
-
for (const obj of
|
|
9097
|
+
for (const obj of asArray45(stack.objects)) {
|
|
8639
9098
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8640
9099
|
if (!object) continue;
|
|
8641
9100
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -8689,11 +9148,11 @@ function lintViewRefs(stack) {
|
|
|
8689
9148
|
});
|
|
8690
9149
|
}
|
|
8691
9150
|
};
|
|
8692
|
-
for (const obj of
|
|
9151
|
+
for (const obj of asArray45(stack.objects)) {
|
|
8693
9152
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8694
|
-
for (const action of
|
|
9153
|
+
for (const action of asArray45(obj.actions)) checkAction(action, object);
|
|
8695
9154
|
}
|
|
8696
|
-
for (const action of
|
|
9155
|
+
for (const action of asArray45(stack.actions)) checkAction(action);
|
|
8697
9156
|
return findings;
|
|
8698
9157
|
}
|
|
8699
9158
|
|
|
@@ -8828,7 +9287,6 @@ var CLI_ONLY = ["cli"];
|
|
|
8828
9287
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8829
9288
|
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.";
|
|
8830
9289
|
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.";
|
|
8831
|
-
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.";
|
|
8832
9290
|
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.";
|
|
8833
9291
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8834
9292
|
var AUTHORING_RULES = [
|
|
@@ -8894,6 +9352,31 @@ var AUTHORING_RULES = [
|
|
|
8894
9352
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8895
9353
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8896
9354
|
},
|
|
9355
|
+
// [#7521, via cloud#1225] A managed object advertising a generic write verb
|
|
9356
|
+
// in `enable.apiMethods` that its own resolved affordances refuse. Every key
|
|
9357
|
+
// is one we know and each is individually valid, so #4001's unknown-key
|
|
9358
|
+
// rejection and the Zod parse both pass it; the contradiction is only visible
|
|
9359
|
+
// when the two keys are read TOGETHER, which nothing did at authoring time.
|
|
9360
|
+
//
|
|
9361
|
+
// `gating` because the declaration is already false when it ships: objectql's
|
|
9362
|
+
// registry strips the verb at registration, so the metadata advertises an API
|
|
9363
|
+
// the product does not serve. That strip has been correct and silent — a
|
|
9364
|
+
// `console.warn` on every control-plane boot that went unread for the life of
|
|
9365
|
+
// a real divergence (`sys_environment`/`sys_package`). This entry is the
|
|
9366
|
+
// ruling's "close it where the author is"; boot stays warn-and-strip.
|
|
9367
|
+
//
|
|
9368
|
+
// Pre-parse: the predicate reads only authored keys, and the finding must
|
|
9369
|
+
// survive an unrelated schema error elsewhere in the stack.
|
|
9370
|
+
{
|
|
9371
|
+
name: "validateManagedApiMethods",
|
|
9372
|
+
tier: "gating",
|
|
9373
|
+
input: "normalized",
|
|
9374
|
+
commands: ALL,
|
|
9375
|
+
source: "packages/lint/src/validate-managed-api-methods.ts",
|
|
9376
|
+
surfaces: CLI_ONLY,
|
|
9377
|
+
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
9378
|
+
run: (stack) => validateManagedApiMethods(stack)
|
|
9379
|
+
},
|
|
8897
9380
|
// A view container in `views: []` that registers zero views: nothing appears
|
|
8898
9381
|
// in the Console, and the schema step cannot tell it from an intentionally
|
|
8899
9382
|
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
@@ -9273,14 +9756,47 @@ var AUTHORING_RULES = [
|
|
|
9273
9756
|
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
9274
9757
|
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
9275
9758
|
// that gains an `error` finding moves its registry tier in the same edit.
|
|
9759
|
+
//
|
|
9760
|
+
// ─── The `views[]` visibility-predicate FAMILY at the runtime door (#7220) ───
|
|
9761
|
+
//
|
|
9762
|
+
// This entry and `validatePredicatePathRefs` below moved to `runtime-publish`
|
|
9763
|
+
// in ONE edit, on the maintainer's 2026-08-10 ruling, sequenced after #4717's
|
|
9764
|
+
// `advisories` channel landed (PR #7435). Before that move a `view` written
|
|
9765
|
+
// through Studio / REST `/meta` / MCP — the only door most tenants have, and
|
|
9766
|
+
// the door AI authors use — was judged by NONE of the family's rule ids (six
|
|
9767
|
+
// at the time of the move; seven since #7659 added
|
|
9768
|
+
// `predicate-rhs-path-shaped` inside the second entry).
|
|
9769
|
+
//
|
|
9770
|
+
// They move together on purpose, and the two entries carry one comment because
|
|
9771
|
+
// they are one wall: #7214's implementer wired its own rule here alone and then
|
|
9772
|
+
// REVERTED it, because a `view` refused for an unresolvable predicate PATH
|
|
9773
|
+
// while a predicate that does not parse at all walks through the same door is
|
|
9774
|
+
// less predictable than refusing neither. A half-wired wall is worse than an
|
|
9775
|
+
// unwired one, so `authoring-rule-wiring.test.ts` now pins the family property
|
|
9776
|
+
// directly: every id on this surface is gated at the runtime door, or none is.
|
|
9777
|
+
//
|
|
9778
|
+
// The previous `surfaceReason` on THIS entry was `RUNTIME_NEEDS_FULL_SNAPSHOT`,
|
|
9779
|
+
// and re-measuring it at move time found it false: both rule functions read
|
|
9780
|
+
// `stack.views` and `stack.pages` and NO other collection — never `objects` —
|
|
9781
|
+
// so the per-write snapshot the gate builds is not partial for them, it is
|
|
9782
|
+
// complete. (`pages` is simply absent on a `view` write, so the page half
|
|
9783
|
+
// contributes zero findings to both differential passes rather than inventing
|
|
9784
|
+
// any.) The reason was not describing this rule; it was the default a rule got
|
|
9785
|
+
// when nobody measured, which is the #4409/#4463 defect one layer in.
|
|
9786
|
+
//
|
|
9787
|
+
// Runtime input tier: the gate hands the rules the body as persisted, without
|
|
9788
|
+
// `normalizeStackInput`, so the ADR-0087 D2 alias fold does NOT run at this
|
|
9789
|
+
// door. That costs the family nothing — `validateVisibilityPredicates` reads
|
|
9790
|
+
// `visibleWhen ?? visibleOn ?? visibility` itself, canonical-first, precisely
|
|
9791
|
+
// so a caller handing it a raw authored object still gets a verdict.
|
|
9276
9792
|
{
|
|
9277
9793
|
name: "validateVisibilityPredicates",
|
|
9278
9794
|
tier: "gating",
|
|
9279
9795
|
input: "normalized",
|
|
9280
9796
|
commands: ALL,
|
|
9281
9797
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
9282
|
-
surfaces:
|
|
9283
|
-
|
|
9798
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9799
|
+
runtimeTypes: ["view"],
|
|
9284
9800
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
9285
9801
|
},
|
|
9286
9802
|
// #7010 — the same predicate surface, one question further in. The three
|
|
@@ -9297,14 +9813,28 @@ var AUTHORING_RULES = [
|
|
|
9297
9813
|
// object's addressable path set is NOT closed (lookup traversal, system
|
|
9298
9814
|
// columns, formula outputs), and an `error` gate over an open set generates
|
|
9299
9815
|
// false build errors. See the rule's module note.
|
|
9816
|
+
//
|
|
9817
|
+
// #7659 adds a THIRD id here, `predicate-rhs-path-shaped`, which is not a
|
|
9818
|
+
// resolution question at all: the metadata-admin renderer resolves paths only
|
|
9819
|
+
// on the LEFT of `==` / `!=` and hands the right side to its literal parser,
|
|
9820
|
+
// so `data.a == data.b` resolves both sides cleanly, passes the two rules
|
|
9821
|
+
// above, and still compares against the string "data.b" — a constant verdict.
|
|
9822
|
+
// It carries `error` on a dotted chain (no reading under which it worked) and
|
|
9823
|
+
// `warning` on a bare word (`status == active` compares as the text today, so
|
|
9824
|
+
// refusing it would fail a build over metadata that renders correctly). The
|
|
9825
|
+
// per-finding severity is what gates, exactly as `lintFlowPatterns` has worked
|
|
9826
|
+
// since #3760; the entry's `gating` tier is unchanged because it already was.
|
|
9300
9827
|
{
|
|
9301
9828
|
name: "validatePredicatePathRefs",
|
|
9302
9829
|
tier: "gating",
|
|
9303
9830
|
input: "normalized",
|
|
9304
9831
|
commands: ALL,
|
|
9305
9832
|
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
9306
|
-
|
|
9307
|
-
|
|
9833
|
+
// The second half of the #7220 family move — see the block above the
|
|
9834
|
+
// `validateVisibilityPredicates` entry. This is the rule whose solo wiring
|
|
9835
|
+
// was reverted; it is wired now because its siblings are.
|
|
9836
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9837
|
+
runtimeTypes: ["view"],
|
|
9308
9838
|
run: (stack) => validatePredicatePathRefs(stack)
|
|
9309
9839
|
},
|
|
9310
9840
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
@@ -9464,16 +9994,110 @@ var AUTHORING_RULES = [
|
|
|
9464
9994
|
// a runtime enforcement point (fail-closed OWD default, canonical enum, anchor
|
|
9465
9995
|
// binding gate, vocabulary freeze), moving the failure from a runtime deny to
|
|
9466
9996
|
// an author-time fix-it. Per ADR-0049 this is not advisory security.
|
|
9997
|
+
//
|
|
9998
|
+
// [#7576] The `surfaceReason` below is MEASURED. Its predecessor was not, and
|
|
9999
|
+
// was false in both halves — it read: "Already gated at this surface by a
|
|
10000
|
+
// DIFFERENT mechanism: plugin-security registers an ADR-0094 authoring gate on
|
|
10001
|
+
// `object` (`registerAuthoringGate`) that enforces the same OWD posture rules
|
|
10002
|
+
// on every runtime write. Running the linter here as well would double-report
|
|
10003
|
+
// one refusal in two vocabularies."
|
|
10004
|
+
//
|
|
10005
|
+
// - COVERAGE. `object-posture-gate.ts` reads exactly `sharingModel` and
|
|
10006
|
+
// `externalSharingModel` through a local `OWD_WIDTH`, and never touches
|
|
10007
|
+
// `fields`, `permissions`, `books` or `data`. Of the THIRTEEN rule ids this
|
|
10008
|
+
// block carries it covers ONE — `security-external-wider-than-internal`
|
|
10009
|
+
// (its R2). The gate's other half, R1 (env-tighten-only, ADR-0086 D1),
|
|
10010
|
+
// corresponds to no lint rule at all, so it is not coverage in the other
|
|
10011
|
+
// direction either. Twelve rules were enforced at no runtime door while
|
|
10012
|
+
// this field said they were.
|
|
10013
|
+
// - DOUBLE-REPORTING. It cannot happen, and not by luck: `saveMetaItem` runs
|
|
10014
|
+
// `assertRuntimeAuthoringRules` (this table, 422 `invalid_metadata`) BEFORE
|
|
10015
|
+
// `runAuthoringGate` (the ADR-0094 gate, 403 `owd_external_wider`), and
|
|
10016
|
+
// both refuse by THROWING. The first to fire ends the write, so an author
|
|
10017
|
+
// sees one refusal, never two. The stated cost of moving was imaginary; the
|
|
10018
|
+
// reason it has not moved is the measured one below.
|
|
10019
|
+
//
|
|
10020
|
+
// The move IS taken now — the #7891 programme's three slices, in order:
|
|
10021
|
+
//
|
|
10022
|
+
// - #8307: the ADR-0091 seed pair crossed (`runtimeTypes: ['seed']`), with
|
|
10023
|
+
// the isolation proof that the differential cancels every finding this
|
|
10024
|
+
// function derives from the sibling collections.
|
|
10025
|
+
// - #8309: the snapshot repair. The gate used to carry `objects` and
|
|
10026
|
+
// nothing else, so the three cross-collection rules judged a universe
|
|
10027
|
+
// missing the collection they compare against (measured: 38 phantom
|
|
10028
|
+
// `security-master-detail-ungranted` per-write vs 4 whole-stack,
|
|
10029
|
+
// PR #7886). `RuntimeStackContext` now carries `permissions`/`books` in
|
|
10030
|
+
// BOTH differential passes and `TYPE_TO_STACK_KEY` maps both types.
|
|
10031
|
+
// - #8310 slice 1: `runtimeTypes` gains `permission` + `book` (PR #8546).
|
|
10032
|
+
// `object` measured DIRTY on that tree and was escalated, not forced.
|
|
10033
|
+
// - #8310 slice 2 (this state): `object` crosses under the maintainer
|
|
10034
|
+
// ruling recorded on #8310 (2026-08-13, 「接受你的全部建议」): an
|
|
10035
|
+
// authored OWD is REQUIRED at the runtime object door — an object
|
|
10036
|
+
// publish with no authored `sharingModel` is refused with the 422 lint
|
|
10037
|
+
// envelope (`security-owd-unset`); absence is not a decision. The ~16
|
|
10038
|
+
// objectql/rest suite files that relied on OWD-less publishes were
|
|
10039
|
+
// repaired honestly (fixtures author their posture), and
|
|
10040
|
+
// `meta-object-owd-gate.test.ts` re-pins the door ORDER: this table
|
|
10041
|
+
// answers first (`saveMetaItem` runs it before `runAuthoringGate`), the
|
|
10042
|
+
// ADR-0094-seam 403 doors answer for what passes lint. The same ruling
|
|
10043
|
+
// retired the plugin gate's R2 `owd_external_wider` arm as a duplicate
|
|
10044
|
+
// of this door (R1 env-tighten-only STAYS — no lint rule covers it);
|
|
10045
|
+
// see `object-posture-gate.ts` and the ADR-0094 amendment.
|
|
10046
|
+
//
|
|
10047
|
+
// `security-role-word` is NOT in this entry any more — that is what the
|
|
10048
|
+
// `validateSecurityRoleWord` entry below records. It judges six collections
|
|
10049
|
+
// (objects, fields, actions, permission sets, positions, apps — plus books),
|
|
10050
|
+
// and `positions`/`apps` are neither carried by the per-write snapshot nor
|
|
10051
|
+
// mapped in `TYPE_TO_STACK_KEY`, so declaring `permission`/`book` on a
|
|
10052
|
+
// function that still contained it would have enforced ONE rule id for a
|
|
10053
|
+
// strict subset of its collections: a door where a permission set named
|
|
10054
|
+
// `role_manager` is refused and a position named `sales_role` walks through
|
|
10055
|
+
// — the #7220 failure this table refuses to build, in either direction. The
|
|
10056
|
+
// rule therefore stays behind WHOLE (#8310's explicit call), as its own
|
|
10057
|
+
// entry.
|
|
10058
|
+
//
|
|
10059
|
+
// This entry remains the rest of the D7 block (12 rule ids) as ONE
|
|
10060
|
+
// registration, not a per-rule split: the baseline/candidate differential is
|
|
10061
|
+
// what keeps a write of one declared type from leaking the other rules'
|
|
10062
|
+
// whole-stack findings — every finding derived from a sibling collection is
|
|
10063
|
+
// produced byte-identically in both passes and cancels in the diff. Only
|
|
10064
|
+
// findings the written item itself adds are attributed to the write.
|
|
9467
10065
|
{
|
|
9468
10066
|
name: "validateSecurityPosture",
|
|
9469
10067
|
tier: "gating",
|
|
9470
10068
|
input: "parsed",
|
|
9471
10069
|
commands: ALL,
|
|
9472
10070
|
source: "packages/lint/src/validate-security-posture.ts",
|
|
9473
|
-
surfaces:
|
|
9474
|
-
|
|
10071
|
+
surfaces: CLI_AND_RUNTIME,
|
|
10072
|
+
runtimeTypes: ["seed", "permission", "book", "object"],
|
|
9475
10073
|
run: (stack) => validateSecurityPosture(stack)
|
|
9476
10074
|
},
|
|
10075
|
+
// [ADR-0090 D3 / #8310] The vocabulary freeze, split out of
|
|
10076
|
+
// `validateSecurityPosture` the day the rest of that block crossed the
|
|
10077
|
+
// runtime wall — so that it could stay behind WHOLE rather than cross for
|
|
10078
|
+
// three of the six collections it judges (#7220: one rule id must sit on ONE
|
|
10079
|
+
// side of the wall). The split is a surface boundary, not taste: the rule's
|
|
10080
|
+
// verdict and findings are byte-identical to before on every CLI command
|
|
10081
|
+
// (both entries run on all three), and the runtime door does not run it for
|
|
10082
|
+
// ANY type.
|
|
10083
|
+
//
|
|
10084
|
+
// The road to crossing is concrete and short, recorded here so the next
|
|
10085
|
+
// seat prices it correctly: carry `positions`/`apps` in
|
|
10086
|
+
// `RuntimeStackContext` + `CONTEXT_STACK_KEYS`, map both types in
|
|
10087
|
+
// `TYPE_TO_STACK_KEY` (both are `allowRuntimeCreate: true`, so the writes
|
|
10088
|
+
// are real), then declare `runtimeTypes: ['object', 'permission', 'book',
|
|
10089
|
+
// 'position', 'app']` on THIS entry — all six collections in one edit, the
|
|
10090
|
+
// #7220 discipline satisfied.
|
|
10091
|
+
{
|
|
10092
|
+
name: "validateSecurityRoleWord",
|
|
10093
|
+
tier: "gating",
|
|
10094
|
+
input: "parsed",
|
|
10095
|
+
commands: ALL,
|
|
10096
|
+
source: "packages/lint/src/validate-security-posture.ts",
|
|
10097
|
+
surfaces: CLI_ONLY,
|
|
10098
|
+
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.",
|
|
10099
|
+
run: (stack) => validateSecurityRoleWord(stack)
|
|
10100
|
+
},
|
|
9477
10101
|
// ADR-0105 D6 — the org tree is a REPORTING dimension. An RLS policy or
|
|
9478
10102
|
// sharing rule that walks it builds a second permission hierarchy (the
|
|
9479
10103
|
// dual-hierarchy mistake ADR-0057 D5 retired) and cannot widen Layer 0 anyway,
|
|
@@ -9524,7 +10148,7 @@ var AUTHORING_RULES = [
|
|
|
9524
10148
|
commands: ALL,
|
|
9525
10149
|
source: "packages/lint/src/validate-rls-predicate-enforceability.ts",
|
|
9526
10150
|
surfaces: CLI_ONLY,
|
|
9527
|
-
surfaceReason: "
|
|
10151
|
+
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.",
|
|
9528
10152
|
run: (stack) => validateRlsPredicateEnforceability(stack)
|
|
9529
10153
|
},
|
|
9530
10154
|
// #4762 — the same "declared but enforces nothing" question, for the two
|
|
@@ -9582,8 +10206,34 @@ var TYPE_TO_STACK_KEY = {
|
|
|
9582
10206
|
dashboard: "dashboards",
|
|
9583
10207
|
agent: "agents",
|
|
9584
10208
|
hook: "hooks",
|
|
9585
|
-
seed
|
|
10209
|
+
// [#7576] `data`, NOT `seeds`. The metadata TYPE is `seed`; the stack KEY that
|
|
10210
|
+
// holds seeds is `data` (`ObjectStackDefinitionSchema.data: z.array(SeedSchema)`)
|
|
10211
|
+
// — a stack has no `seeds` key at all, and `PLURAL_TO_SINGULAR` declares no
|
|
10212
|
+
// mapping onto one either.
|
|
10213
|
+
//
|
|
10214
|
+
// The wrong spelling was INERT rather than harmless, and it is the #4449 shape
|
|
10215
|
+
// one surface over: the wiring guard asks only that a declared type HAS a
|
|
10216
|
+
// mapping, never that the mapping names a key some rule reads. So it would
|
|
10217
|
+
// have stayed green while the gate built `{ objects, seeds: [item] }` for
|
|
10218
|
+
// every seed write and every rule reading `stack.data` saw nothing — wired,
|
|
10219
|
+
// and running on nothing, with `rulesRun` reporting the rules as having run.
|
|
10220
|
+
// Nothing declared `seed` in `runtimeTypes` at the time, so correcting it
|
|
10221
|
+
// changed no behaviour then; it was corrected here, with the measurement
|
|
10222
|
+
// that found it (#7576), rather than left for the rollout card to trip
|
|
10223
|
+
// over. The ADR-0091 seed pair now DOES declare `seed` (#8307), so this
|
|
10224
|
+
// mapping is load-bearing today, not merely inert-and-correct.
|
|
10225
|
+
seed: "data",
|
|
10226
|
+
// [#8309] `permission`/`book` map ahead of their registration (#8310), the
|
|
10227
|
+
// same order `seed` arrived in: the mapping plus the enriched snapshot below
|
|
10228
|
+
// are this card's halves, and the `runtimeTypes` flip is deliberately NOT —
|
|
10229
|
+
// a mapping without a declaring rule is inert by construction (the gate
|
|
10230
|
+
// filters by `runtimeTypes` before it ever consults this table), while a
|
|
10231
|
+
// declaration without the mapping is the wired-onto-nothing state the wiring
|
|
10232
|
+
// guard refuses. Landing the mapping first keeps #8310 a registry data edit.
|
|
10233
|
+
permission: "permissions",
|
|
10234
|
+
book: "books"
|
|
9586
10235
|
};
|
|
10236
|
+
var CONTEXT_STACK_KEYS = ["objects", "permissions", "books"];
|
|
9587
10237
|
function runtimeAuthoringRulesFor(type) {
|
|
9588
10238
|
return AUTHORING_RULES.filter(
|
|
9589
10239
|
(r) => r.surfaces.includes("runtime-publish") && (r.runtimeTypes ?? []).includes(type)
|
|
@@ -9601,6 +10251,23 @@ function stackKeyForType(type) {
|
|
|
9601
10251
|
return TYPE_TO_STACK_KEY[type] ?? null;
|
|
9602
10252
|
}
|
|
9603
10253
|
var fingerprint = (f) => `${f.rule}\0${f.where}\0${f.path}\0${f.message}`;
|
|
10254
|
+
function buildRuntimeWriteSnapshots(args) {
|
|
10255
|
+
const stackKey = stackKeyForType(args.type);
|
|
10256
|
+
if (!stackKey) return null;
|
|
10257
|
+
if (!args.item || typeof args.item !== "object") return null;
|
|
10258
|
+
const item = args.item;
|
|
10259
|
+
const itemName = typeof item.name === "string" ? item.name : void 0;
|
|
10260
|
+
const baseline = {};
|
|
10261
|
+
for (const key of CONTEXT_STACK_KEYS) {
|
|
10262
|
+
const collection = args.context?.[key] ?? [];
|
|
10263
|
+
baseline[key] = key === stackKey ? collection.filter((o) => !itemName || o?.name !== itemName) : collection;
|
|
10264
|
+
}
|
|
10265
|
+
const candidate = {
|
|
10266
|
+
...baseline,
|
|
10267
|
+
[stackKey]: [...baseline[stackKey] ?? [], item]
|
|
10268
|
+
};
|
|
10269
|
+
return { baseline, candidate };
|
|
10270
|
+
}
|
|
9604
10271
|
function runRules(rules, stack, ctx) {
|
|
9605
10272
|
const findings = [];
|
|
9606
10273
|
for (const rule of rules) {
|
|
@@ -9623,19 +10290,15 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9623
10290
|
const rules = runtimeAuthoringRulesFor(args.type);
|
|
9624
10291
|
const empty = { errors: [], advisories: [], rulesRun: [] };
|
|
9625
10292
|
if (rules.length === 0) return empty;
|
|
9626
|
-
const
|
|
9627
|
-
|
|
9628
|
-
|
|
9629
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
10293
|
+
const snapshots = buildRuntimeWriteSnapshots({
|
|
10294
|
+
type: args.type,
|
|
10295
|
+
item: args.item,
|
|
10296
|
+
...args.context !== void 0 ? { context: args.context } : {}
|
|
10297
|
+
});
|
|
10298
|
+
if (!snapshots) return empty;
|
|
9632
10299
|
const ctx = { sduiManifest: args.sduiManifest };
|
|
9633
|
-
const
|
|
9634
|
-
const
|
|
9635
|
-
const baseline = { objects: baselineObjects };
|
|
9636
|
-
const candidate = writesIntoContext ? { objects: [...baselineObjects, item] } : { objects: baselineObjects, [stackKey]: [item] };
|
|
9637
|
-
const before = new Set(runRules(rules, baseline, ctx).map(fingerprint));
|
|
9638
|
-
const added = runRules(rules, candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
10300
|
+
const before = new Set(runRules(rules, snapshots.baseline, ctx).map(fingerprint));
|
|
10301
|
+
const added = runRules(rules, snapshots.candidate, ctx).filter((f) => !before.has(fingerprint(f)));
|
|
9639
10302
|
return {
|
|
9640
10303
|
errors: added.filter((f) => f.severity === "error"),
|
|
9641
10304
|
advisories: added.filter((f) => f.severity !== "error"),
|
|
@@ -9643,6 +10306,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9643
10306
|
};
|
|
9644
10307
|
}
|
|
9645
10308
|
export {
|
|
10309
|
+
buildRuntimeWriteSnapshots,
|
|
9646
10310
|
runRuntimeAuthoringRules,
|
|
9647
10311
|
runtimeAuthoringRulesFor,
|
|
9648
10312
|
runtimeGatedTypes,
|