@objectstack/lint 17.0.0-rc.0 → 17.0.0-rc.1
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 +3627 -0
- package/dist/index.cjs +1836 -863
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +297 -61
- package/dist/index.d.ts +297 -61
- package/dist/index.js +1791 -840
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
package/dist/index.js
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
// src/validate-widget-bindings.ts
|
|
2
2
|
import { isIncoherentAggregate } from "@objectstack/spec/data";
|
|
3
|
+
import { ChartTypeSchema } from "@objectstack/spec/ui";
|
|
4
|
+
|
|
5
|
+
// src/system-fields.ts
|
|
6
|
+
import { FIELD_GROUP_SYSTEM_FIELDS } from "@objectstack/spec/data";
|
|
7
|
+
import { SystemFieldName } from "@objectstack/spec/system";
|
|
8
|
+
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
9
|
+
...FIELD_GROUP_SYSTEM_FIELDS,
|
|
10
|
+
...Object.values(SystemFieldName)
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
// src/validate-widget-bindings.ts
|
|
3
14
|
var WIDGET_DATASET_UNKNOWN = "widget-dataset-unknown";
|
|
4
15
|
var WIDGET_DIMENSION_UNKNOWN = "widget-dimension-unknown";
|
|
5
16
|
var WIDGET_MEASURE_UNKNOWN = "widget-measure-unknown";
|
|
@@ -30,20 +41,18 @@ function asArray(v) {
|
|
|
30
41
|
function asStrings(v) {
|
|
31
42
|
return Array.isArray(v) ? v.filter((s) => typeof s === "string") : [];
|
|
32
43
|
}
|
|
33
|
-
var
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"funnel",
|
|
42
|
-
"scatter",
|
|
43
|
-
"treemap",
|
|
44
|
-
"sankey",
|
|
45
|
-
"radar"
|
|
44
|
+
var MEASURE_EXEMPT_CHART_TYPES = /* @__PURE__ */ new Set([
|
|
45
|
+
"gauge",
|
|
46
|
+
"solid-gauge",
|
|
47
|
+
"metric",
|
|
48
|
+
"kpi",
|
|
49
|
+
"bullet",
|
|
50
|
+
"table",
|
|
51
|
+
"pivot"
|
|
46
52
|
]);
|
|
53
|
+
var CHART_TYPES = new Set(
|
|
54
|
+
ChartTypeSchema.options.filter((t) => !MEASURE_EXEMPT_CHART_TYPES.has(t))
|
|
55
|
+
);
|
|
47
56
|
function levenshtein(a, b) {
|
|
48
57
|
const m = a.length, n = b.length;
|
|
49
58
|
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
@@ -89,18 +98,6 @@ function list(names) {
|
|
|
89
98
|
}
|
|
90
99
|
var DATE_RANGE_FILTER_NAME = "dateRange";
|
|
91
100
|
var DATE_RANGE_DEFAULT_FIELD = "created_at";
|
|
92
|
-
var SYSTEM_FIELDS = /* @__PURE__ */ new Set([
|
|
93
|
-
"id",
|
|
94
|
-
"created_at",
|
|
95
|
-
"created_by",
|
|
96
|
-
"updated_at",
|
|
97
|
-
"updated_by",
|
|
98
|
-
"owner_id",
|
|
99
|
-
"organization_id",
|
|
100
|
-
"tenant_id",
|
|
101
|
-
"user_id",
|
|
102
|
-
"deleted_at"
|
|
103
|
-
]);
|
|
104
101
|
function dashboardFilterDefs(dash) {
|
|
105
102
|
const byName = /* @__PURE__ */ new Map();
|
|
106
103
|
const dateRange = dash.dateRange;
|
|
@@ -334,6 +331,7 @@ function validateWidgetBindings(stack) {
|
|
|
334
331
|
|
|
335
332
|
// src/validate-expressions.ts
|
|
336
333
|
import { validateExpression } from "@objectstack/formula";
|
|
334
|
+
import { collectFlowGraphs, resolveFlowNodeExpressions } from "@objectstack/spec/automation";
|
|
337
335
|
function asArray2(v) {
|
|
338
336
|
if (Array.isArray(v)) return v;
|
|
339
337
|
if (v && typeof v === "object") {
|
|
@@ -394,37 +392,53 @@ function validateStackExpressions(stack) {
|
|
|
394
392
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
395
393
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
396
394
|
};
|
|
395
|
+
const checkDeclaredPredicate = (where, raw) => {
|
|
396
|
+
if (raw == null) return;
|
|
397
|
+
const res = validateExpression("predicate", raw);
|
|
398
|
+
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
399
|
+
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
400
|
+
};
|
|
397
401
|
for (const flow of asArray2(stack.flows)) {
|
|
398
402
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
399
403
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
400
|
-
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
401
404
|
const startNode = nodes.find((n) => n.type === "start");
|
|
402
405
|
const startCfg = startNode?.config ?? {};
|
|
403
406
|
const objectName = typeof startCfg.objectName === "string" ? startCfg.objectName : void 0;
|
|
404
|
-
for (const
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
407
|
+
for (const graph of collectFlowGraphs(flow)) {
|
|
408
|
+
const at = graph.scope ? `flow '${flowName}' \xB7 ${graph.scope}` : `flow '${flowName}'`;
|
|
409
|
+
for (const node of graph.nodes) {
|
|
410
|
+
const cfg = node.config ?? {};
|
|
411
|
+
check(`${at} \xB7 node '${node.id}' (${node.type}) condition`, cfg.condition, objectName);
|
|
412
|
+
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
413
|
+
for (const found of resolveFlowNodeExpressions(nodeType, cfg)) {
|
|
414
|
+
if (found.entry.role !== "predicate") continue;
|
|
415
|
+
checkDeclaredPredicate(
|
|
416
|
+
`${at} \xB7 node '${node.id}' (${nodeType}) ${found.entry.label} at config.${found.path}`,
|
|
417
|
+
found.value
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
if (node.type === "script") {
|
|
421
|
+
const fn = (typeof cfg.function === "string" ? cfg.function.trim() : "") || (typeof cfg.functionName === "string" ? cfg.functionName.trim() : "");
|
|
422
|
+
const action = typeof cfg.actionType === "string" ? cfg.actionType.trim() : "";
|
|
423
|
+
const inline = typeof cfg.script === "string" ? cfg.script.trim() : "";
|
|
424
|
+
if (!fn && !action && !inline) {
|
|
425
|
+
issues.push({
|
|
426
|
+
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
427
|
+
message: `script node declares neither \`actionType\` nor \`function\` \u2014 it would do nothing at runtime. Name a built-in action (e.g. \`actionType: 'email'\`) or a registered function (\`function: 'my_fn'\`, registered via \`defineStack({ functions })\`).`,
|
|
428
|
+
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
429
|
+
});
|
|
430
|
+
} else if (action === "invoke_function" && !fn) {
|
|
431
|
+
issues.push({
|
|
432
|
+
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
433
|
+
message: `script node uses \`actionType: 'invoke_function'\` but no \`function\` (or \`functionName\`) \u2014 it names no callable. Set \`function: 'my_fn'\` and register it via \`defineStack({ functions })\`.`,
|
|
434
|
+
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
435
|
+
});
|
|
436
|
+
}
|
|
423
437
|
}
|
|
424
438
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
439
|
+
for (const edge of graph.edges) {
|
|
440
|
+
check(`${at} \xB7 edge '${edge.id}' (${edge.source}\u2192${edge.target}) condition`, edge.condition, objectName);
|
|
441
|
+
}
|
|
428
442
|
}
|
|
429
443
|
}
|
|
430
444
|
for (const obj of objects) {
|
|
@@ -677,6 +691,85 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
677
691
|
return findings;
|
|
678
692
|
}
|
|
679
693
|
|
|
694
|
+
// src/flow-walk.ts
|
|
695
|
+
import { FLOW_REGION_SLOTS_BY_TYPE, FLOW_REGION_CONFIG_KEYS } from "@objectstack/spec/automation";
|
|
696
|
+
function isRec(v) {
|
|
697
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
698
|
+
}
|
|
699
|
+
function strName(v) {
|
|
700
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
701
|
+
}
|
|
702
|
+
var REGION_SLOTS = new Map(
|
|
703
|
+
[...FLOW_REGION_SLOTS_BY_TYPE].map(([type, slots]) => [type, slots.map((s) => s.key)])
|
|
704
|
+
);
|
|
705
|
+
var REGION_CONFIG_KEYS = FLOW_REGION_CONFIG_KEYS;
|
|
706
|
+
var MAX_REGION_DEPTH = 16;
|
|
707
|
+
function flowNodeLabel(node, index) {
|
|
708
|
+
return strName(node.label) ?? strName(node.id) ?? `#${index}`;
|
|
709
|
+
}
|
|
710
|
+
function stripRegions(config) {
|
|
711
|
+
if (!isRec(config)) return void 0;
|
|
712
|
+
let out;
|
|
713
|
+
for (const key of Object.keys(config)) {
|
|
714
|
+
if (!REGION_CONFIG_KEYS.has(key)) continue;
|
|
715
|
+
out ?? (out = { ...config });
|
|
716
|
+
delete out[key];
|
|
717
|
+
}
|
|
718
|
+
return out ?? config;
|
|
719
|
+
}
|
|
720
|
+
function walkFlowNodes(flow, flowPath) {
|
|
721
|
+
const out = [];
|
|
722
|
+
if (!isRec(flow)) return out;
|
|
723
|
+
const visitList = (nodes, basePath, trail, depth) => {
|
|
724
|
+
if (!Array.isArray(nodes) || depth > MAX_REGION_DEPTH) return;
|
|
725
|
+
nodes.forEach((raw, index) => {
|
|
726
|
+
if (!isRec(raw)) return;
|
|
727
|
+
const path = `${basePath}[${index}]`;
|
|
728
|
+
out.push({
|
|
729
|
+
node: raw,
|
|
730
|
+
path,
|
|
731
|
+
localConfig: stripRegions(raw.config),
|
|
732
|
+
regionTrail: trail,
|
|
733
|
+
depth
|
|
734
|
+
});
|
|
735
|
+
const type = strName(raw.type);
|
|
736
|
+
const slots = type ? REGION_SLOTS.get(type) : void 0;
|
|
737
|
+
if (!slots || !isRec(raw.config)) return;
|
|
738
|
+
const config = raw.config;
|
|
739
|
+
const here = `${type} "${flowNodeLabel(raw, index)}"`;
|
|
740
|
+
for (const slot of slots) {
|
|
741
|
+
const value = config[slot];
|
|
742
|
+
if (slot === "branches") {
|
|
743
|
+
if (!Array.isArray(value)) continue;
|
|
744
|
+
value.forEach((branch, b) => {
|
|
745
|
+
if (!isRec(branch)) return;
|
|
746
|
+
const branchName = strName(branch.name) ?? `#${b}`;
|
|
747
|
+
visitList(
|
|
748
|
+
branch.nodes,
|
|
749
|
+
`${path}.config.branches[${b}].nodes`,
|
|
750
|
+
joinTrail(trail, `${here} \u203A branch ${branchName}`),
|
|
751
|
+
depth + 1
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
if (!isRec(value)) continue;
|
|
757
|
+
visitList(
|
|
758
|
+
value.nodes,
|
|
759
|
+
`${path}.config.${slot}.nodes`,
|
|
760
|
+
joinTrail(trail, `${here} \u203A ${slot}`),
|
|
761
|
+
depth + 1
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
};
|
|
766
|
+
visitList(flow.nodes, `${flowPath}.nodes`, "", 0);
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
769
|
+
function joinTrail(trail, segment) {
|
|
770
|
+
return trail ? `${trail} \u203A ${segment}` : segment;
|
|
771
|
+
}
|
|
772
|
+
|
|
680
773
|
// src/validate-flow-template-paths.ts
|
|
681
774
|
var FLOW_TEMPLATE_UNKNOWN_FIELD = "flow-template-unknown-field";
|
|
682
775
|
var FLOW_TEMPLATE_LOOKUP_TRAVERSAL = "flow-template-lookup-traversal";
|
|
@@ -690,19 +783,10 @@ function asArray5(v) {
|
|
|
690
783
|
}
|
|
691
784
|
return [];
|
|
692
785
|
}
|
|
693
|
-
var
|
|
694
|
-
|
|
786
|
+
var IMPLICIT_HEADS = /* @__PURE__ */ new Set([
|
|
787
|
+
...SYSTEM_FIELDS,
|
|
695
788
|
"name",
|
|
696
789
|
"owner",
|
|
697
|
-
"owner_id",
|
|
698
|
-
"created_at",
|
|
699
|
-
"created_by",
|
|
700
|
-
"updated_at",
|
|
701
|
-
"updated_by",
|
|
702
|
-
"organization_id",
|
|
703
|
-
"tenant_id",
|
|
704
|
-
"is_deleted",
|
|
705
|
-
"deleted_at",
|
|
706
790
|
"record_type"
|
|
707
791
|
]);
|
|
708
792
|
var RELATION_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -832,12 +916,13 @@ function validateFlowTemplatePaths(stack) {
|
|
|
832
916
|
if (!obj) return;
|
|
833
917
|
const fieldTypes = fieldTypesOf(obj);
|
|
834
918
|
const expandSet = declaredExpandOf(flow);
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
const
|
|
919
|
+
walkFlowNodes(flow, `flows[${flowIndex}]`).forEach(({ node, path: nodePath, regionTrail, localConfig }, walkIndex) => {
|
|
920
|
+
const nodeLabel = typeof node.type === "string" ? node.type : typeof node.id === "string" ? node.id : `#${walkIndex}`;
|
|
921
|
+
const where = regionTrail ? `flow "${flowName}" ${regionTrail} node "${nodeLabel}"` : `flow "${flowName}" node "${nodeLabel}"`;
|
|
838
922
|
const nodeType = typeof node.type === "string" ? node.type : "";
|
|
839
923
|
const guarded = FILTER_GUARDED_NODE_TYPES.has(nodeType);
|
|
840
|
-
const
|
|
924
|
+
const scanNode = localConfig !== void 0 && localConfig !== node.config ? { ...node, config: localConfig } : node;
|
|
925
|
+
const leaves = collectNodeLeaves(scanNode, guarded);
|
|
841
926
|
if (leaves.length === 0) return;
|
|
842
927
|
const seenUnknown = /* @__PURE__ */ new Set();
|
|
843
928
|
const seenTraversal = /* @__PURE__ */ new Set();
|
|
@@ -847,15 +932,15 @@ function validateFlowTemplatePaths(stack) {
|
|
|
847
932
|
const head = rest[0];
|
|
848
933
|
const hasSubPath = rest.length > 1;
|
|
849
934
|
const nextIsIdentifier = hasSubPath && !/^\d+$/.test(rest[1]);
|
|
850
|
-
const isKnown = fieldTypes.has(head) ||
|
|
935
|
+
const isKnown = fieldTypes.has(head) || IMPLICIT_HEADS.has(head);
|
|
851
936
|
if (!isKnown) {
|
|
852
937
|
if (seenUnknown.has(head)) continue;
|
|
853
938
|
seenUnknown.add(head);
|
|
854
939
|
findings.push({
|
|
855
940
|
severity: inFilter ? "error" : "warning",
|
|
856
941
|
rule: FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
857
|
-
where
|
|
858
|
-
path:
|
|
942
|
+
where,
|
|
943
|
+
path: nodePath,
|
|
859
944
|
message: inFilter ? `${nodeType} filter references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 the token resolves to nothing, which DROPS the condition from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${rest.join(".")}}', but '${head}' is not a field on object '${objectName}' \u2014 it resolves to an empty string at runtime (silently).`,
|
|
860
945
|
hint: inFilter ? `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'); system columns like id/created_at/owner are also addressable. This gates the build rather than warning: an absent condition WIDENS the query, so the runtime has already decided to refuse this node.` : `Check the field name against the object's field definitions (e.g. '{record.full_name}', not '{record.full_naem}'). System columns like id/created_at/owner are also addressable.`
|
|
861
946
|
});
|
|
@@ -870,8 +955,8 @@ function validateFlowTemplatePaths(stack) {
|
|
|
870
955
|
findings.push({
|
|
871
956
|
severity: inFilter ? "error" : "warning",
|
|
872
957
|
rule: FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
873
|
-
where
|
|
874
|
-
path:
|
|
958
|
+
where,
|
|
959
|
+
path: nodePath,
|
|
875
960
|
message: inFilter ? `${nodeType} filter references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so the token resolves to nothing and the condition is DROPPED from the query instead of narrowing it. The node refuses to run at execution time (#3810).` : `template references '{record.${key}}', a cross-object hop through the ${headType} field '${head}' \u2014 the flow record carries '${head}' as a scalar id, not an expanded object, so this resolves to an empty string at runtime (silently).`,
|
|
876
961
|
hint: inFilter ? `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise filter on the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'. This gates the build rather than warning: an absent condition WIDENS the query.` : `Opt in to resolve it: add '${head}' to the start node's config.expand (#3475) and the engine re-reads it as the run's identity. Otherwise reference the foreign-key id directly ('{record.${head}}'), or project the value via a formula field on '${objectName}'.`
|
|
877
962
|
});
|
|
@@ -935,8 +1020,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
935
1020
|
if (flow.runAs === "system") return;
|
|
936
1021
|
const runAs = flow.runAs === "user" || flow.runAs === "system" ? flow.runAs : "user";
|
|
937
1022
|
const flowName = typeof flow.name === "string" ? flow.name : `#${flowIndex}`;
|
|
938
|
-
const
|
|
939
|
-
|
|
1023
|
+
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
1024
|
+
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
940
1025
|
if (node?.type !== "update_record") return;
|
|
941
1026
|
const config = node.config ?? {};
|
|
942
1027
|
const objectName = readLiteralObjectName(config);
|
|
@@ -945,7 +1030,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
945
1030
|
if (!fieldMap) return;
|
|
946
1031
|
const fields = config.fields;
|
|
947
1032
|
if (!fields || typeof fields !== "object" || Array.isArray(fields)) return;
|
|
948
|
-
const nodeName =
|
|
1033
|
+
const nodeName = flowNodeLabel(node, walkIndex);
|
|
1034
|
+
const where = regionTrail ? `flow "${flowName}" \u203A ${regionTrail} \u203A node "${nodeName}"` : `flow "${flowName}" \u203A node "${nodeName}"`;
|
|
949
1035
|
for (const fieldName of Object.keys(fields)) {
|
|
950
1036
|
const meta = fieldMap.get(fieldName);
|
|
951
1037
|
if (!meta) continue;
|
|
@@ -953,8 +1039,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
953
1039
|
findings.push({
|
|
954
1040
|
severity: "error",
|
|
955
1041
|
rule: FLOW_UPDATE_READONLY_FIELD,
|
|
956
|
-
where
|
|
957
|
-
path:
|
|
1042
|
+
where,
|
|
1043
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
958
1044
|
message: `writes field '${fieldName}', which object '${objectName}' declares readonly:true. Under runAs:'${runAs}' the engine silently strips readonly fields from the UPDATE payload (#2948), so this write never lands \u2014 while the step still reports success.`,
|
|
959
1045
|
hint: `If automation is meant to maintain this field, declare the flow runAs:'system' (the intended channel \u2014 readonly governs the end-user/API surface, not trusted system writers). Otherwise remove '${fieldName}' from this update_record node.`
|
|
960
1046
|
});
|
|
@@ -962,8 +1048,8 @@ function validateReadonlyFlowWrites(stack) {
|
|
|
962
1048
|
findings.push({
|
|
963
1049
|
severity: "warning",
|
|
964
1050
|
rule: FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
965
|
-
where
|
|
966
|
-
path:
|
|
1051
|
+
where,
|
|
1052
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
967
1053
|
message: `writes field '${fieldName}', which object '${objectName}' declares readonlyWhen. On records where that predicate is TRUE, a runAs:'${runAs}' UPDATE strips the field (#3042), so this write may silently not land depending on the record's state.`,
|
|
968
1054
|
hint: `If automation must maintain this field regardless of record state, run the flow runAs:'system'. Otherwise confirm this node only targets records whose readonlyWhen predicate is FALSE.`
|
|
969
1055
|
});
|
|
@@ -1415,206 +1501,584 @@ function validateReactPages(stack) {
|
|
|
1415
1501
|
// src/validate-react-page-props.ts
|
|
1416
1502
|
import { createRequire as createRequire2 } from "module";
|
|
1417
1503
|
import { REACT_BLOCKS, chartAggregateResultKeys } from "@objectstack/spec/ui";
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
`@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
|
|
1427
|
-
);
|
|
1504
|
+
import { VALID_AST_OPERATORS } from "@objectstack/spec/data";
|
|
1505
|
+
|
|
1506
|
+
// src/validate-searchable-fields.ts
|
|
1507
|
+
var SEARCHABLE_FIELD_UNKNOWN = "searchable-field-unknown";
|
|
1508
|
+
function asArray10(v) {
|
|
1509
|
+
if (Array.isArray(v)) return v;
|
|
1510
|
+
if (v && typeof v === "object") {
|
|
1511
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1428
1512
|
}
|
|
1429
|
-
return
|
|
1513
|
+
return [];
|
|
1430
1514
|
}
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
REACT_BLOCKS.map((b) => [
|
|
1434
|
-
b.tag,
|
|
1435
|
-
{
|
|
1436
|
-
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
1437
|
-
knownProps: new Set(b.interactions.map((i) => i.name))
|
|
1438
|
-
}
|
|
1439
|
-
])
|
|
1440
|
-
);
|
|
1441
|
-
function editDistance(a, b, cap = 2) {
|
|
1442
|
-
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
1443
|
-
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
1444
|
-
for (let j = 1; j <= b.length; j++) {
|
|
1445
|
-
let prev = dp[0];
|
|
1446
|
-
dp[0] = j;
|
|
1447
|
-
for (let i = 1; i <= a.length; i++) {
|
|
1448
|
-
const tmp = dp[i];
|
|
1449
|
-
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1450
|
-
prev = tmp;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
return dp[a.length];
|
|
1515
|
+
function isRec2(v) {
|
|
1516
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1454
1517
|
}
|
|
1455
|
-
function
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1518
|
+
function strName2(v) {
|
|
1519
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1520
|
+
}
|
|
1521
|
+
function declaredFieldNames(obj) {
|
|
1522
|
+
const fields = obj.fields;
|
|
1523
|
+
if (!fields || typeof fields !== "object") return null;
|
|
1524
|
+
const names = /* @__PURE__ */ new Set();
|
|
1525
|
+
for (const f of asArray10(fields)) {
|
|
1526
|
+
const n = strName2(f.name);
|
|
1527
|
+
if (n) names.add(n);
|
|
1465
1528
|
}
|
|
1466
|
-
return
|
|
1529
|
+
return names.size > 0 ? names : null;
|
|
1467
1530
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
|
|
1477
|
-
if (tsc.isArrayLiteralExpression(node)) {
|
|
1478
|
-
const out = [];
|
|
1479
|
-
for (const el of node.elements) {
|
|
1480
|
-
const v = staticValue(tsc, sf, el);
|
|
1481
|
-
if (v === NOT_STATIC) return NOT_STATIC;
|
|
1482
|
-
out.push(v);
|
|
1531
|
+
function suggest2(target, known) {
|
|
1532
|
+
let best;
|
|
1533
|
+
let bestScore = Infinity;
|
|
1534
|
+
for (const candidate of known) {
|
|
1535
|
+
const d = distance(target, candidate);
|
|
1536
|
+
if (d < bestScore) {
|
|
1537
|
+
bestScore = d;
|
|
1538
|
+
best = candidate;
|
|
1483
1539
|
}
|
|
1484
|
-
return out;
|
|
1485
1540
|
}
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1541
|
+
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
1542
|
+
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
1543
|
+
}
|
|
1544
|
+
function distance(a, b) {
|
|
1545
|
+
const m = a.length;
|
|
1546
|
+
const n = b.length;
|
|
1547
|
+
if (m === 0) return n;
|
|
1548
|
+
if (n === 0) return m;
|
|
1549
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
1550
|
+
for (let i = 1; i <= m; i++) {
|
|
1551
|
+
const curr = [i, ...new Array(n).fill(0)];
|
|
1552
|
+
for (let j = 1; j <= n; j++) {
|
|
1553
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1554
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
1495
1555
|
}
|
|
1496
|
-
|
|
1556
|
+
prev = curr;
|
|
1497
1557
|
}
|
|
1498
|
-
return
|
|
1499
|
-
}
|
|
1500
|
-
function attrValue(tsc, sf, attr) {
|
|
1501
|
-
const init = attr.initializer;
|
|
1502
|
-
if (!init) return true;
|
|
1503
|
-
if (tsc.isStringLiteral(init)) return init.text;
|
|
1504
|
-
if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
|
|
1505
|
-
return NOT_STATIC;
|
|
1558
|
+
return prev[n];
|
|
1506
1559
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
"created_at",
|
|
1514
|
-
"created_by",
|
|
1515
|
-
"updated_at",
|
|
1516
|
-
"updated_by",
|
|
1517
|
-
"owner_id",
|
|
1518
|
-
"organization_id",
|
|
1519
|
-
"tenant_id",
|
|
1520
|
-
"user_id",
|
|
1521
|
-
"deleted_at"
|
|
1522
|
-
]);
|
|
1523
|
-
function namedArray(v) {
|
|
1524
|
-
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
1525
|
-
if (v && typeof v === "object") {
|
|
1526
|
-
return Object.entries(v).map(([name, def]) => ({
|
|
1527
|
-
name,
|
|
1528
|
-
...def && typeof def === "object" ? def : {}
|
|
1529
|
-
}));
|
|
1560
|
+
function indexObjectSearchTargets(stack) {
|
|
1561
|
+
const fieldsByObject = /* @__PURE__ */ new Map();
|
|
1562
|
+
if (!isRec2(stack)) return fieldsByObject;
|
|
1563
|
+
for (const obj of asArray10(stack.objects)) {
|
|
1564
|
+
const name = strName2(obj.name);
|
|
1565
|
+
if (name) fieldsByObject.set(name, declaredFieldNames(obj));
|
|
1530
1566
|
}
|
|
1531
|
-
return
|
|
1567
|
+
return fieldsByObject;
|
|
1532
1568
|
}
|
|
1533
|
-
function
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1569
|
+
function checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject) {
|
|
1570
|
+
const findings = [];
|
|
1571
|
+
if (!Array.isArray(declared) || declared.length === 0) return findings;
|
|
1572
|
+
if (!objectName) return findings;
|
|
1573
|
+
if (!fieldsByObject.has(objectName)) return findings;
|
|
1574
|
+
const known = fieldsByObject.get(objectName);
|
|
1575
|
+
if (!known) return findings;
|
|
1576
|
+
for (let i = 0; i < declared.length; i++) {
|
|
1577
|
+
const entry = declared[i];
|
|
1578
|
+
const name = strName2(entry);
|
|
1537
1579
|
if (!name) continue;
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1580
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) continue;
|
|
1581
|
+
const dotted = name.includes(".");
|
|
1582
|
+
findings.push({
|
|
1583
|
+
severity: "error",
|
|
1584
|
+
rule: SEARCHABLE_FIELD_UNKNOWN,
|
|
1585
|
+
where,
|
|
1586
|
+
path: `${path}[${i}]`,
|
|
1587
|
+
message: `${subject} entry "${name}" is not a field on object "${objectName}". The declaration is stale: searching it can never match, and the engine silently drops it \u2014 leaving a narrower search than declared, or the auto-default set once every entry is dropped.` + (dotted ? "" : suggest2(name, known)),
|
|
1588
|
+
hint: (dotted ? `'search' scans this object's own columns, so a related record's column cannot be a search target \u2014 expand the relation and search the related object, or copy the value onto a formula field here. ` : `Fix the name, or add "${name}" to ${objectName}.fields. `) + `Clients echo this declaration verbatim as the '$searchFields' override, so a stale entry becomes a 400 INVALID_FIELD on list search (#4254), not just a quietly narrowed one.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
1589
|
+
});
|
|
1543
1590
|
}
|
|
1544
|
-
return
|
|
1591
|
+
return findings;
|
|
1545
1592
|
}
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
const
|
|
1550
|
-
const
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
if (!isRec(aggregate)) return;
|
|
1555
|
-
const fn = strOf(aggregate.function);
|
|
1556
|
-
const field = strOf(aggregate.field);
|
|
1557
|
-
const groupBy = aggregate.groupBy;
|
|
1558
|
-
const groupByField = strOf(groupBy) ?? (isRec(groupBy) ? strOf(groupBy.field) : void 0);
|
|
1559
|
-
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
1560
|
-
push(
|
|
1561
|
-
"error",
|
|
1562
|
-
REACT_CHART_AGGREGATE_INVALID,
|
|
1563
|
-
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
1564
|
-
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
1593
|
+
function validateSearchableFields(stack) {
|
|
1594
|
+
const findings = [];
|
|
1595
|
+
if (!isRec2(stack)) return findings;
|
|
1596
|
+
const objects = asArray10(stack.objects);
|
|
1597
|
+
const fieldsByObject = indexObjectSearchTargets(stack);
|
|
1598
|
+
const check = (declared, objectName, where, path, subject) => {
|
|
1599
|
+
findings.push(
|
|
1600
|
+
...checkSearchableFieldList(declared, objectName, fieldsByObject, where, path, subject)
|
|
1565
1601
|
);
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1602
|
+
};
|
|
1603
|
+
for (let oi = 0; oi < objects.length; oi++) {
|
|
1604
|
+
const obj = objects[oi];
|
|
1605
|
+
if (!isRec2(obj)) continue;
|
|
1606
|
+
const objName = strName2(obj.name);
|
|
1607
|
+
const label2 = objName ? `object "${objName}"` : `objects[${oi}]`;
|
|
1608
|
+
check(
|
|
1609
|
+
obj.searchableFields,
|
|
1610
|
+
objName,
|
|
1611
|
+
label2,
|
|
1612
|
+
`objects[${oi}].searchableFields`,
|
|
1613
|
+
"searchableFields"
|
|
1572
1614
|
);
|
|
1615
|
+
if (isRec2(obj.listViews)) {
|
|
1616
|
+
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
1617
|
+
if (!isRec2(lv)) continue;
|
|
1618
|
+
check(
|
|
1619
|
+
lv.searchableFields,
|
|
1620
|
+
// A built-in list view belongs to its object; an inline `data.object`
|
|
1621
|
+
// may still retarget it (ADR-0047 allows the explicit binding).
|
|
1622
|
+
listViewObject(lv) ?? objName,
|
|
1623
|
+
`${label2} \u203A listViews.${key}`,
|
|
1624
|
+
`objects[${oi}].listViews.${key}.searchableFields`,
|
|
1625
|
+
"list-view searchableFields"
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1573
1629
|
}
|
|
1574
|
-
const
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
`
|
|
1585
|
-
`
|
|
1630
|
+
const views = asArray10(stack.views);
|
|
1631
|
+
for (let vi = 0; vi < views.length; vi++) {
|
|
1632
|
+
const view = views[vi];
|
|
1633
|
+
if (!isRec2(view)) continue;
|
|
1634
|
+
const viewLabel = strName2(view.name) ?? strName2(view.objectName) ?? `#${vi}`;
|
|
1635
|
+
const viewObject = strName2(view.objectName) ?? strName2(view.object);
|
|
1636
|
+
if (isRec2(view.list)) {
|
|
1637
|
+
check(
|
|
1638
|
+
view.list.searchableFields,
|
|
1639
|
+
listViewObject(view.list) ?? viewObject,
|
|
1640
|
+
`view "${viewLabel}" \u203A list`,
|
|
1641
|
+
`views[${vi}].list.searchableFields`,
|
|
1642
|
+
"list-view searchableFields"
|
|
1586
1643
|
);
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
|
|
1644
|
+
}
|
|
1645
|
+
if (isRec2(view.listViews)) {
|
|
1646
|
+
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
1647
|
+
if (!isRec2(lv)) continue;
|
|
1648
|
+
check(
|
|
1649
|
+
lv.searchableFields,
|
|
1650
|
+
listViewObject(lv) ?? viewObject,
|
|
1651
|
+
`view "${viewLabel}" \u203A listViews.${key}`,
|
|
1652
|
+
`views[${vi}].listViews.${key}.searchableFields`,
|
|
1653
|
+
"list-view searchableFields"
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1590
1657
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1658
|
+
return findings;
|
|
1659
|
+
}
|
|
1660
|
+
function listViewObject(listView) {
|
|
1661
|
+
const data = listView.data;
|
|
1662
|
+
return isRec2(data) ? strName2(data.object) : void 0;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/page-walk.ts
|
|
1666
|
+
function isRec3(v) {
|
|
1667
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1668
|
+
}
|
|
1669
|
+
function strName3(v) {
|
|
1670
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1671
|
+
}
|
|
1672
|
+
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
1673
|
+
function isSourceAuthoredPage(page) {
|
|
1674
|
+
const kind = strName3(page.kind);
|
|
1675
|
+
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
1676
|
+
}
|
|
1677
|
+
function walkPageComponents(page, pagePath) {
|
|
1678
|
+
const out = [];
|
|
1679
|
+
if (!isRec3(page) || isSourceAuthoredPage(page)) return out;
|
|
1680
|
+
const pageObject = strName3(page.object);
|
|
1681
|
+
const visit = (node, path, inheritedObject) => {
|
|
1682
|
+
if (!isRec3(node)) return;
|
|
1683
|
+
const props = isRec3(node.properties) ? node.properties : void 0;
|
|
1684
|
+
const dataSource = isRec3(node.dataSource) ? node.dataSource : void 0;
|
|
1685
|
+
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
1686
|
+
out.push({ component: node, path, objectName });
|
|
1687
|
+
if (!props) return;
|
|
1688
|
+
if (Array.isArray(props.items)) {
|
|
1689
|
+
for (let i = 0; i < props.items.length; i++) {
|
|
1690
|
+
const item = props.items[i];
|
|
1691
|
+
if (!isRec3(item) || !Array.isArray(item.children)) continue;
|
|
1692
|
+
for (let c = 0; c < item.children.length; c++) {
|
|
1693
|
+
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
if (Array.isArray(props.children)) {
|
|
1698
|
+
for (let i = 0; i < props.children.length; i++) {
|
|
1699
|
+
visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
for (const key of ["body", "footer"]) {
|
|
1703
|
+
const slotList = props[key];
|
|
1704
|
+
if (!Array.isArray(slotList)) continue;
|
|
1705
|
+
for (let i = 0; i < slotList.length; i++) {
|
|
1706
|
+
visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
1711
|
+
for (let r = 0; r < regions.length; r++) {
|
|
1712
|
+
const region = regions[r];
|
|
1713
|
+
if (!isRec3(region) || !Array.isArray(region.components)) continue;
|
|
1714
|
+
for (let c = 0; c < region.components.length; c++) {
|
|
1715
|
+
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
const slots = isRec3(page.slots) ? page.slots : void 0;
|
|
1719
|
+
if (slots) {
|
|
1720
|
+
for (const [slot, value] of Object.entries(slots)) {
|
|
1721
|
+
const list3 = Array.isArray(value) ? value : [value];
|
|
1722
|
+
const indexed = Array.isArray(value);
|
|
1723
|
+
for (let i = 0; i < list3.length; i++) {
|
|
1724
|
+
visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
return out;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// src/validate-page-field-bindings.ts
|
|
1732
|
+
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
1733
|
+
function asArray11(v) {
|
|
1734
|
+
if (Array.isArray(v)) return v;
|
|
1735
|
+
if (v && typeof v === "object") {
|
|
1736
|
+
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
1737
|
+
}
|
|
1738
|
+
return [];
|
|
1739
|
+
}
|
|
1740
|
+
function strName4(v) {
|
|
1741
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
1742
|
+
}
|
|
1743
|
+
function isRec4(v) {
|
|
1744
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
1745
|
+
}
|
|
1746
|
+
function fieldRefsFrom(value, basePath) {
|
|
1747
|
+
const out = [];
|
|
1748
|
+
const one = (v, path) => {
|
|
1749
|
+
const bare = strName4(v);
|
|
1750
|
+
if (bare) {
|
|
1751
|
+
out.push({ name: bare, path });
|
|
1752
|
+
return;
|
|
1753
|
+
}
|
|
1754
|
+
if (!isRec4(v)) return;
|
|
1755
|
+
const named = strName4(v.field) ?? strName4(v.name);
|
|
1756
|
+
if (named) out.push({ name: named, path: `${path}.${strName4(v.field) ? "field" : "name"}` });
|
|
1757
|
+
};
|
|
1758
|
+
if (Array.isArray(value)) {
|
|
1759
|
+
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
1760
|
+
} else {
|
|
1761
|
+
one(value, basePath);
|
|
1762
|
+
}
|
|
1763
|
+
return out;
|
|
1764
|
+
}
|
|
1765
|
+
function sortFieldRefs(value, basePath) {
|
|
1766
|
+
if (typeof value === "string") {
|
|
1767
|
+
const head = value.trim().split(/\s+/)[0];
|
|
1768
|
+
return head ? [{ name: head, path: basePath }] : [];
|
|
1769
|
+
}
|
|
1770
|
+
return fieldRefsFrom(value, basePath);
|
|
1771
|
+
}
|
|
1772
|
+
var COMPONENT_FIELD_SPECS = {
|
|
1773
|
+
"record:highlights": { props: ["fields"] },
|
|
1774
|
+
// `sections`/`hideFields` are not in RecordDetailsProps, but every real page
|
|
1775
|
+
// authors them (they survive because `properties` is unvalidated).
|
|
1776
|
+
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
1777
|
+
"record:path": { props: ["statusField"] },
|
|
1778
|
+
"element:number": { props: ["field"] },
|
|
1779
|
+
"element:filter": { props: ["fields"] },
|
|
1780
|
+
"element:form": { props: ["fields"] },
|
|
1781
|
+
// The schema says `displayField`; real pages author `labelField`. Accept both.
|
|
1782
|
+
"element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
|
|
1783
|
+
};
|
|
1784
|
+
var RELATED_LIST_TYPE = "record:related_list";
|
|
1785
|
+
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
1786
|
+
const spec = COMPONENT_FIELD_SPECS[type];
|
|
1787
|
+
if (!spec) return null;
|
|
1788
|
+
const refs = [];
|
|
1789
|
+
for (const key of spec.props ?? []) {
|
|
1790
|
+
refs.push(...fieldRefsFrom(props[key], `${basePath}${sep}${key}`));
|
|
1791
|
+
}
|
|
1792
|
+
for (const key of spec.nestedSections ?? []) {
|
|
1793
|
+
const sections = Array.isArray(props[key]) ? props[key] : [];
|
|
1794
|
+
for (let si = 0; si < sections.length; si++) {
|
|
1795
|
+
const section = sections[si];
|
|
1796
|
+
if (!isRec4(section)) continue;
|
|
1797
|
+
refs.push(...fieldRefsFrom(section.fields, `${basePath}${sep}${key}[${si}].fields`));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
return refs;
|
|
1801
|
+
}
|
|
1802
|
+
function relatedListFieldRefs(props, basePath, sep = ".") {
|
|
1803
|
+
const add = isRec4(props.add) ? props.add : void 0;
|
|
1804
|
+
const picker = add && isRec4(add.picker) ? add.picker : void 0;
|
|
1805
|
+
const at = (key) => `${basePath}${sep}${key}`;
|
|
1806
|
+
return {
|
|
1807
|
+
relatedObject: strName4(props.objectName),
|
|
1808
|
+
related: [
|
|
1809
|
+
...fieldRefsFrom(props.columns, at("columns")),
|
|
1810
|
+
...sortFieldRefs(props.sort, at("sort")),
|
|
1811
|
+
...fieldRefsFrom(props.filter, at("filter")),
|
|
1812
|
+
...fieldRefsFrom(props.relationshipField, at("relationshipField")),
|
|
1813
|
+
...add ? fieldRefsFrom(add.linkField, at("add.linkField")) : []
|
|
1814
|
+
],
|
|
1815
|
+
parent: fieldRefsFrom(props.relationshipValueField, at("relationshipValueField")),
|
|
1816
|
+
pickerObject: picker ? strName4(picker.object) : void 0,
|
|
1817
|
+
picker: picker ? [
|
|
1818
|
+
...fieldRefsFrom(picker.valueField, at("add.picker.valueField")),
|
|
1819
|
+
...fieldRefsFrom(picker.labelField, at("add.picker.labelField"))
|
|
1820
|
+
] : []
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function indexObjectFields(stack) {
|
|
1824
|
+
const objectFields = /* @__PURE__ */ new Map();
|
|
1825
|
+
if (!isRec4(stack)) return objectFields;
|
|
1826
|
+
for (const obj of asArray11(stack.objects)) {
|
|
1827
|
+
const name = strName4(obj.name);
|
|
1828
|
+
if (!name) continue;
|
|
1829
|
+
const names = /* @__PURE__ */ new Set();
|
|
1830
|
+
for (const f of asArray11(obj.fields)) {
|
|
1831
|
+
const fn = strName4(f.name);
|
|
1832
|
+
if (fn) names.add(fn);
|
|
1833
|
+
}
|
|
1834
|
+
objectFields.set(name, names);
|
|
1835
|
+
}
|
|
1836
|
+
return objectFields;
|
|
1837
|
+
}
|
|
1838
|
+
function checkFieldRefs(refs, objectName, objectFields, where, consequence = "skipped") {
|
|
1839
|
+
const findings = [];
|
|
1840
|
+
if (!objectName) return findings;
|
|
1841
|
+
const known = objectFields.get(objectName);
|
|
1842
|
+
if (!known) return findings;
|
|
1843
|
+
for (const ref of refs) {
|
|
1844
|
+
if (ref.name.includes(".")) continue;
|
|
1845
|
+
if (known.has(ref.name) || SYSTEM_FIELDS.has(ref.name)) continue;
|
|
1846
|
+
findings.push({
|
|
1847
|
+
severity: consequence === "queried" ? "error" : "warning",
|
|
1848
|
+
rule: PAGE_FIELD_UNKNOWN,
|
|
1849
|
+
where,
|
|
1850
|
+
path: ref.path,
|
|
1851
|
+
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 ` + (consequence === "queried" ? 'it is used in a QUERY, so the predicate can never match: the surface renders an empty result that looks exactly like "there is no data".' : "the component silently skips it, so it never renders."),
|
|
1852
|
+
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
return findings;
|
|
1856
|
+
}
|
|
1857
|
+
function validatePageFieldBindings(stack) {
|
|
1858
|
+
const findings = [];
|
|
1859
|
+
if (!stack || typeof stack !== "object") return findings;
|
|
1860
|
+
const objectFields = indexObjectFields(stack);
|
|
1861
|
+
const pages = asArray11(stack.pages);
|
|
1862
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
1863
|
+
const page = pages[pi];
|
|
1864
|
+
if (!page || typeof page !== "object") continue;
|
|
1865
|
+
const pageName = strName4(page.name) ?? `#${pi}`;
|
|
1866
|
+
const checkRefs = (refs, objectName, where) => {
|
|
1867
|
+
findings.push(...checkFieldRefs(refs, objectName, objectFields, where));
|
|
1868
|
+
};
|
|
1869
|
+
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
1870
|
+
const type = strName4(component.type);
|
|
1871
|
+
const props = isRec4(component.properties) ? component.properties : void 0;
|
|
1872
|
+
if (!type || !props) continue;
|
|
1873
|
+
const where = `page "${pageName}" \xB7 ${type}`;
|
|
1874
|
+
const base = `${path}.properties`;
|
|
1875
|
+
if (type === RELATED_LIST_TYPE) {
|
|
1876
|
+
const split = relatedListFieldRefs(props, base);
|
|
1877
|
+
checkRefs(split.related, split.relatedObject, where);
|
|
1878
|
+
checkRefs(split.parent, objectName, where);
|
|
1879
|
+
checkRefs(split.picker, split.pickerObject, where);
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
const refs = componentFieldRefs(type, props, base);
|
|
1883
|
+
if (!refs) continue;
|
|
1884
|
+
checkRefs(refs, objectName, where);
|
|
1885
|
+
}
|
|
1886
|
+
const cfg = isRec4(page.interfaceConfig) ? page.interfaceConfig : void 0;
|
|
1887
|
+
if (cfg) {
|
|
1888
|
+
const cfgObject = strName4(cfg.source) ?? strName4(page.object);
|
|
1889
|
+
const base = `pages[${pi}].interfaceConfig`;
|
|
1890
|
+
const refs = [
|
|
1891
|
+
...fieldRefsFrom(cfg.columns, `${base}.columns`),
|
|
1892
|
+
...sortFieldRefs(cfg.sort, `${base}.sort`),
|
|
1893
|
+
...fieldRefsFrom(cfg.filterBy, `${base}.filterBy`)
|
|
1894
|
+
];
|
|
1895
|
+
const userFilters = isRec4(cfg.userFilters) ? cfg.userFilters : void 0;
|
|
1896
|
+
if (userFilters) {
|
|
1897
|
+
refs.push(...fieldRefsFrom(userFilters.fields, `${base}.userFilters.fields`));
|
|
1898
|
+
}
|
|
1899
|
+
checkRefs(refs, cfgObject, `page "${pageName}" \xB7 interfaceConfig`);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
return findings;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
// src/validate-react-page-props.ts
|
|
1906
|
+
var cachedTs = null;
|
|
1907
|
+
function loadTypeScript() {
|
|
1908
|
+
if (cachedTs) return cachedTs;
|
|
1909
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
1910
|
+
try {
|
|
1911
|
+
cachedTs = createRequire2(anchor)("typescript");
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
throw new Error(
|
|
1914
|
+
`@objectstack/lint: validating a kind:'react' page requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a react-source page is validated.`
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
return cachedTs;
|
|
1918
|
+
}
|
|
1919
|
+
var asArray12 = (v) => Array.isArray(v) ? v : [];
|
|
1920
|
+
var BLOCKS = new Map(
|
|
1921
|
+
REACT_BLOCKS.map((b) => [
|
|
1922
|
+
b.tag,
|
|
1923
|
+
{
|
|
1924
|
+
requiredBindings: b.interactions.filter((i) => i.required).map((i) => i.name),
|
|
1925
|
+
knownProps: new Set(b.interactions.map((i) => i.name))
|
|
1926
|
+
}
|
|
1927
|
+
])
|
|
1928
|
+
);
|
|
1929
|
+
function editDistance(a, b, cap = 2) {
|
|
1930
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
1931
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => i);
|
|
1932
|
+
for (let j = 1; j <= b.length; j++) {
|
|
1933
|
+
let prev = dp[0];
|
|
1934
|
+
dp[0] = j;
|
|
1935
|
+
for (let i = 1; i <= a.length; i++) {
|
|
1936
|
+
const tmp = dp[i];
|
|
1937
|
+
dp[i] = Math.min(dp[i] + 1, dp[i - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1938
|
+
prev = tmp;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return dp[a.length];
|
|
1942
|
+
}
|
|
1943
|
+
function nearestKnown(prop, known) {
|
|
1944
|
+
if (known.has(prop)) return null;
|
|
1945
|
+
let best = null;
|
|
1946
|
+
let bestD = 3;
|
|
1947
|
+
for (const k of known) {
|
|
1948
|
+
const d = editDistance(prop, k);
|
|
1949
|
+
if (d < bestD) {
|
|
1950
|
+
bestD = d;
|
|
1951
|
+
best = k;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return bestD <= 2 ? best : null;
|
|
1955
|
+
}
|
|
1956
|
+
var NOT_STATIC = /* @__PURE__ */ Symbol("not-static");
|
|
1957
|
+
function staticValue(tsc, sf, node) {
|
|
1958
|
+
if (!node) return NOT_STATIC;
|
|
1959
|
+
if (tsc.isParenthesizedExpression(node)) return staticValue(tsc, sf, node.expression);
|
|
1960
|
+
if (tsc.isStringLiteral(node) || tsc.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
1961
|
+
if (tsc.isNumericLiteral(node)) return Number(node.text);
|
|
1962
|
+
if (node.kind === tsc.SyntaxKind.TrueKeyword) return true;
|
|
1963
|
+
if (node.kind === tsc.SyntaxKind.FalseKeyword) return false;
|
|
1964
|
+
if (node.kind === tsc.SyntaxKind.NullKeyword) return null;
|
|
1965
|
+
if (tsc.isArrayLiteralExpression(node)) {
|
|
1966
|
+
const out = [];
|
|
1967
|
+
for (const el of node.elements) {
|
|
1968
|
+
const v = staticValue(tsc, sf, el);
|
|
1969
|
+
if (v === NOT_STATIC) return NOT_STATIC;
|
|
1970
|
+
out.push(v);
|
|
1971
|
+
}
|
|
1972
|
+
return out;
|
|
1973
|
+
}
|
|
1974
|
+
if (tsc.isObjectLiteralExpression(node)) {
|
|
1975
|
+
const out = {};
|
|
1976
|
+
for (const p of node.properties) {
|
|
1977
|
+
if (!tsc.isPropertyAssignment(p)) return NOT_STATIC;
|
|
1978
|
+
const key = tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name) ? p.name.text : null;
|
|
1979
|
+
if (key === null) return NOT_STATIC;
|
|
1980
|
+
const v = staticValue(tsc, sf, p.initializer);
|
|
1981
|
+
if (v === NOT_STATIC) return NOT_STATIC;
|
|
1982
|
+
out[key] = v;
|
|
1983
|
+
}
|
|
1984
|
+
return out;
|
|
1985
|
+
}
|
|
1986
|
+
return NOT_STATIC;
|
|
1987
|
+
}
|
|
1988
|
+
function attrValue(tsc, sf, attr) {
|
|
1989
|
+
const init = attr.initializer;
|
|
1990
|
+
if (!init) return true;
|
|
1991
|
+
if (tsc.isStringLiteral(init)) return init.text;
|
|
1992
|
+
if (tsc.isJsxExpression(init)) return staticValue(tsc, sf, init.expression);
|
|
1993
|
+
return NOT_STATIC;
|
|
1994
|
+
}
|
|
1995
|
+
function filterAttrValue(tsc, sf, attr) {
|
|
1996
|
+
const init = attr.initializer;
|
|
1997
|
+
if (!init || !tsc.isJsxExpression(init)) return NOT_STATIC;
|
|
1998
|
+
const perPosition = (node) => {
|
|
1999
|
+
if (!node) return NOT_STATIC;
|
|
2000
|
+
if (tsc.isParenthesizedExpression(node)) return perPosition(node.expression);
|
|
2001
|
+
if (tsc.isArrayLiteralExpression(node)) return node.elements.map((el) => perPosition(el));
|
|
2002
|
+
return staticValue(tsc, sf, node);
|
|
2003
|
+
};
|
|
2004
|
+
return perPosition(init.expression);
|
|
2005
|
+
}
|
|
2006
|
+
var REACT_CHART_FIELD_UNKNOWN = "react-chart-field-unknown";
|
|
2007
|
+
var REACT_CHART_AGGREGATE_INVALID = "react-chart-aggregate-invalid";
|
|
2008
|
+
var REACT_CHART_AXIS_UNKNOWN = "react-chart-axis-unknown";
|
|
2009
|
+
var CHART_FUNCTIONS = ["count", "sum", "avg", "min", "max"];
|
|
2010
|
+
var isRec5 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
2011
|
+
var strOf = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2012
|
+
function checkObjectChart(attrs, objectFields, findings) {
|
|
2013
|
+
const { values, where, path } = attrs;
|
|
2014
|
+
const push = (severity, rule, message, hint) => findings.push({ severity, rule, where, path, message, hint });
|
|
2015
|
+
if (values.has("data")) return;
|
|
2016
|
+
const aggregate = values.get("aggregate");
|
|
2017
|
+
if (aggregate === void 0 || aggregate === NOT_STATIC) return;
|
|
2018
|
+
if (!isRec5(aggregate)) return;
|
|
2019
|
+
const fn = strOf(aggregate.function);
|
|
2020
|
+
const field = strOf(aggregate.field);
|
|
2021
|
+
const groupBy = aggregate.groupBy;
|
|
2022
|
+
const groupByField = strOf(groupBy) ?? (isRec5(groupBy) ? strOf(groupBy.field) : void 0);
|
|
2023
|
+
if (fn && !CHART_FUNCTIONS.includes(fn)) {
|
|
2024
|
+
push(
|
|
2025
|
+
"error",
|
|
2026
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2027
|
+
`aggregate.function "${fn}" is not an aggregation this chart can run.`,
|
|
2028
|
+
`Use one of: ${CHART_FUNCTIONS.join(", ")}.`
|
|
2029
|
+
);
|
|
2030
|
+
} else if (fn && fn !== "count" && !field) {
|
|
2031
|
+
push(
|
|
2032
|
+
"error",
|
|
2033
|
+
REACT_CHART_AGGREGATE_INVALID,
|
|
2034
|
+
`aggregate.function "${fn}" has no "field" to aggregate.`,
|
|
2035
|
+
'Add aggregate.field, or use function "count" (the only one that may omit it).'
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
const objectName = strOf(values.get("objectName"));
|
|
2039
|
+
const known = objectName ? objectFields.get(objectName) : void 0;
|
|
2040
|
+
if (objectName && known) {
|
|
2041
|
+
const fieldRef = (name, prop) => {
|
|
2042
|
+
if (!name) return;
|
|
2043
|
+
if (name.includes(".")) return;
|
|
2044
|
+
if (known.has(name) || SYSTEM_FIELDS.has(name)) return;
|
|
2045
|
+
push(
|
|
2046
|
+
"error",
|
|
2047
|
+
REACT_CHART_FIELD_UNKNOWN,
|
|
2048
|
+
`aggregate.${prop} "${name}" is not a field on object "${objectName}" \u2014 the aggregate query has nothing to ${prop === "groupBy" ? "group by" : "aggregate"}, so the chart comes back empty.`,
|
|
2049
|
+
`Fix the field name, or add "${name}" to ${objectName}.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
2050
|
+
);
|
|
2051
|
+
};
|
|
2052
|
+
fieldRef(field, "field");
|
|
2053
|
+
fieldRef(groupByField, "groupBy");
|
|
2054
|
+
}
|
|
2055
|
+
const keys = chartAggregateResultKeys({ field, function: fn, groupBy });
|
|
2056
|
+
const columns = [keys.category, keys.value].filter((k) => !!k);
|
|
2057
|
+
if (columns.length === 0) return;
|
|
2058
|
+
const axisRef = (name, prop) => {
|
|
2059
|
+
if (!name) return;
|
|
2060
|
+
if (columns.includes(name)) return;
|
|
2061
|
+
if (keys.comparison && name === keys.comparison) return;
|
|
2062
|
+
push(
|
|
2063
|
+
"error",
|
|
2064
|
+
REACT_CHART_AXIS_UNKNOWN,
|
|
2065
|
+
`"${name}" is not a column this aggregate returns, so the axis plots nothing. Object-bound aggregate rows are keyed by the RAW FIELD NAMES (unlike a dataset, whose rows are keyed by measure name).`,
|
|
2066
|
+
`Result columns: ${columns.join(", ")}` + (keys.comparison ? ` (plus "${keys.comparison}" with a comparison overlay)` : "") + `. Bind ${prop} to one of them.`
|
|
2067
|
+
);
|
|
2068
|
+
};
|
|
2069
|
+
const xAxisRaw = values.get("xAxis");
|
|
2070
|
+
const categoryAxis = strOf(values.get("xAxisKey")) ?? strOf(xAxisRaw) ?? (isRec5(xAxisRaw) ? strOf(xAxisRaw.field) : void 0);
|
|
1607
2071
|
const categoryProp = values.has("xAxisKey") ? "xAxisKey" : "xAxis.field";
|
|
1608
2072
|
axisRef(categoryAxis, categoryProp);
|
|
1609
2073
|
const yAxisRaw = values.get("yAxis");
|
|
1610
2074
|
const yAxisList = Array.isArray(yAxisRaw) ? yAxisRaw : yAxisRaw !== void 0 ? [yAxisRaw] : [];
|
|
1611
2075
|
for (const a of yAxisList) {
|
|
1612
|
-
axisRef(strOf(a) ?? (
|
|
2076
|
+
axisRef(strOf(a) ?? (isRec5(a) ? strOf(a.field) : void 0), "yAxis[].field");
|
|
1613
2077
|
}
|
|
1614
2078
|
const series = values.get("series");
|
|
1615
2079
|
if (Array.isArray(series)) {
|
|
1616
2080
|
for (const s of series) {
|
|
1617
|
-
if (!
|
|
2081
|
+
if (!isRec5(s)) continue;
|
|
1618
2082
|
const dataKey = strOf(s.dataKey);
|
|
1619
2083
|
axisRef(dataKey ?? strOf(s.name), dataKey ? "series[].dataKey" : "series[].name");
|
|
1620
2084
|
}
|
|
@@ -1628,10 +2092,156 @@ function checkObjectChart(attrs, objectFields, findings) {
|
|
|
1628
2092
|
);
|
|
1629
2093
|
}
|
|
1630
2094
|
}
|
|
2095
|
+
var REACT_FIELD_SPECS = {
|
|
2096
|
+
ListView: {
|
|
2097
|
+
// `fields` is the React overlay's "limit/order the columns"; `columns` the
|
|
2098
|
+
// spec ListView prop. Both name columns on the bound object, and a page may
|
|
2099
|
+
// write either. `hiddenFields`/`fieldOrder`/`filterableFields` are schema
|
|
2100
|
+
// props outside the curated contract — unadvertised but honored by the
|
|
2101
|
+
// renderer, so a stale name there is drift just the same.
|
|
2102
|
+
fields: ["fields", "columns", "hiddenFields", "fieldOrder", "filterableFields"],
|
|
2103
|
+
sorts: ["sort"],
|
|
2104
|
+
nestedFields: ["userFilters", "grouping"],
|
|
2105
|
+
filterArrays: ["filters"]
|
|
2106
|
+
},
|
|
2107
|
+
ObjectForm: {
|
|
2108
|
+
fields: ["fields"],
|
|
2109
|
+
keyedByField: ["initialValues"],
|
|
2110
|
+
// `groups` is FormViewSchema's legacy alias for `sections`.
|
|
2111
|
+
sections: ["sections", "groups"]
|
|
2112
|
+
},
|
|
2113
|
+
ObjectChart: {
|
|
2114
|
+
// The axes are result columns (checkObjectChart owns them); `filter` is an
|
|
2115
|
+
// ordinary ObjectQL predicate over the bound object, like ListView's.
|
|
2116
|
+
filterArrays: ["filter"]
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
var PATH_SEP = " \u203A ";
|
|
2120
|
+
var SCHEMA_TYPE_BY_TAG = new Map(
|
|
2121
|
+
REACT_BLOCKS.map((b) => [b.tag, b.schemaType])
|
|
2122
|
+
);
|
|
2123
|
+
var FILTER_PROPS = new Set(
|
|
2124
|
+
Object.values(REACT_FIELD_SPECS).flatMap((s) => s.filterArrays ?? [])
|
|
2125
|
+
);
|
|
2126
|
+
function readableProps(values) {
|
|
2127
|
+
const out = {};
|
|
2128
|
+
for (const [k, v] of values) if (v !== NOT_STATIC) out[k] = v;
|
|
2129
|
+
return out;
|
|
2130
|
+
}
|
|
2131
|
+
function subformFieldRefs(value, basePath) {
|
|
2132
|
+
const child = [];
|
|
2133
|
+
const parent = [];
|
|
2134
|
+
if (!Array.isArray(value)) return { child, parent };
|
|
2135
|
+
for (let i = 0; i < value.length; i++) {
|
|
2136
|
+
const sub = value[i];
|
|
2137
|
+
if (!isRec5(sub)) continue;
|
|
2138
|
+
const at = (key) => `${basePath}[${i}].${key}`;
|
|
2139
|
+
child.push({
|
|
2140
|
+
objectName: strOf(sub.childObject),
|
|
2141
|
+
refs: [
|
|
2142
|
+
...fieldRefsFrom(sub.columns, at("columns")),
|
|
2143
|
+
...fieldRefsFrom(sub.relationshipField, at("relationshipField")),
|
|
2144
|
+
...fieldRefsFrom(sub.amountField, at("amountField"))
|
|
2145
|
+
]
|
|
2146
|
+
});
|
|
2147
|
+
parent.push(...fieldRefsFrom(sub.totalField, at("totalField")));
|
|
2148
|
+
}
|
|
2149
|
+
return { child, parent };
|
|
2150
|
+
}
|
|
2151
|
+
function filterFieldRefs(node, basePath, out) {
|
|
2152
|
+
if (!Array.isArray(node) || node.length === 0) return;
|
|
2153
|
+
const head = node[0];
|
|
2154
|
+
if (typeof head === "string" && (head.toLowerCase() === "and" || head.toLowerCase() === "or")) {
|
|
2155
|
+
for (let i = 1; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
2156
|
+
return;
|
|
2157
|
+
}
|
|
2158
|
+
if (Array.isArray(head)) {
|
|
2159
|
+
for (let i = 0; i < node.length; i++) filterFieldRefs(node[i], `${basePath}[${i}]`, out);
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
if (typeof head === "string" && head.length > 0 && node.length >= 2 && typeof node[1] === "string" && VALID_AST_OPERATORS.has(node[1].toLowerCase())) {
|
|
2163
|
+
out.push({ name: head, path: `${basePath}[0]` });
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function reactFieldRefs(spec, values, basePath) {
|
|
2167
|
+
const own = [];
|
|
2168
|
+
const queried = [];
|
|
2169
|
+
const readable = (key) => {
|
|
2170
|
+
const v = values.get(key);
|
|
2171
|
+
return v === NOT_STATIC ? void 0 : v;
|
|
2172
|
+
};
|
|
2173
|
+
const at = (key) => `${basePath}${PATH_SEP}${key}`;
|
|
2174
|
+
for (const key of spec.fields ?? []) {
|
|
2175
|
+
own.push(...fieldRefsFrom(readable(key), at(key)));
|
|
2176
|
+
}
|
|
2177
|
+
for (const key of spec.sorts ?? []) {
|
|
2178
|
+
own.push(...sortFieldRefs(readable(key), at(key)));
|
|
2179
|
+
}
|
|
2180
|
+
for (const key of spec.nestedFields ?? []) {
|
|
2181
|
+
const v = readable(key);
|
|
2182
|
+
if (isRec5(v)) own.push(...fieldRefsFrom(v.fields, at(`${key}.fields`)));
|
|
2183
|
+
}
|
|
2184
|
+
for (const key of spec.sections ?? []) {
|
|
2185
|
+
const v = readable(key);
|
|
2186
|
+
if (!Array.isArray(v)) continue;
|
|
2187
|
+
for (let i = 0; i < v.length; i++) {
|
|
2188
|
+
const section = v[i];
|
|
2189
|
+
if (!isRec5(section)) continue;
|
|
2190
|
+
own.push(...fieldRefsFrom(section.fields, at(`${key}[${i}].fields`)));
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
for (const key of spec.keyedByField ?? []) {
|
|
2194
|
+
const v = readable(key);
|
|
2195
|
+
if (!isRec5(v)) continue;
|
|
2196
|
+
for (const k of Object.keys(v)) own.push({ name: k, path: at(`${key}.${k}`) });
|
|
2197
|
+
}
|
|
2198
|
+
for (const key of spec.filterArrays ?? []) {
|
|
2199
|
+
filterFieldRefs(values.get(key), at(key), queried);
|
|
2200
|
+
}
|
|
2201
|
+
return { own, queried };
|
|
2202
|
+
}
|
|
2203
|
+
function checkBlockFieldProps(tag, values, objectFields, where, path) {
|
|
2204
|
+
const objectName = strOf(values.get("objectName"));
|
|
2205
|
+
const out = [];
|
|
2206
|
+
const spec = REACT_FIELD_SPECS[tag];
|
|
2207
|
+
if (spec) {
|
|
2208
|
+
const { own, queried } = reactFieldRefs(spec, values, path);
|
|
2209
|
+
out.push(...checkFieldRefs(own, objectName, objectFields, where));
|
|
2210
|
+
out.push(...checkFieldRefs(queried, objectName, objectFields, where, "queried"));
|
|
2211
|
+
}
|
|
2212
|
+
if (tag === "ObjectForm") {
|
|
2213
|
+
const raw = values.get("subforms");
|
|
2214
|
+
const subs = subformFieldRefs(raw === NOT_STATIC ? void 0 : raw, `${path}${PATH_SEP}subforms`);
|
|
2215
|
+
for (const sub of subs.child) {
|
|
2216
|
+
out.push(...checkFieldRefs(sub.refs, sub.objectName, objectFields, where));
|
|
2217
|
+
}
|
|
2218
|
+
out.push(...checkFieldRefs(subs.parent, objectName, objectFields, where));
|
|
2219
|
+
}
|
|
2220
|
+
const schemaType = tag === "Block" ? strOf(values.get("type")) : SCHEMA_TYPE_BY_TAG.get(tag);
|
|
2221
|
+
if (schemaType) {
|
|
2222
|
+
const props = readableProps(values);
|
|
2223
|
+
if (schemaType === RELATED_LIST_TYPE) {
|
|
2224
|
+
const split = relatedListFieldRefs(props, path, PATH_SEP);
|
|
2225
|
+
out.push(...checkFieldRefs(split.related, split.relatedObject, objectFields, where));
|
|
2226
|
+
out.push(...checkFieldRefs(split.picker, split.pickerObject, objectFields, where));
|
|
2227
|
+
} else if (COMPONENT_FIELD_SPECS[schemaType]) {
|
|
2228
|
+
out.push(
|
|
2229
|
+
...checkFieldRefs(
|
|
2230
|
+
componentFieldRefs(schemaType, props, path, PATH_SEP) ?? [],
|
|
2231
|
+
objectName,
|
|
2232
|
+
objectFields,
|
|
2233
|
+
where
|
|
2234
|
+
)
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return out;
|
|
2239
|
+
}
|
|
1631
2240
|
function validateReactPageProps(stack) {
|
|
1632
2241
|
const findings = [];
|
|
1633
2242
|
const objectFields = indexObjectFields(stack);
|
|
1634
|
-
const
|
|
2243
|
+
const searchTargets = indexObjectSearchTargets(stack);
|
|
2244
|
+
const pages = asArray12(stack.pages);
|
|
1635
2245
|
for (let p = 0; p < pages.length; p++) {
|
|
1636
2246
|
const page = pages[p];
|
|
1637
2247
|
if (!page || page.kind !== "react") continue;
|
|
@@ -1661,7 +2271,10 @@ function validateReactPageProps(stack) {
|
|
|
1661
2271
|
if (tsc.isJsxAttribute(a)) {
|
|
1662
2272
|
const propName = a.name.getText(sf);
|
|
1663
2273
|
used.add(propName);
|
|
1664
|
-
values.set(
|
|
2274
|
+
values.set(
|
|
2275
|
+
propName,
|
|
2276
|
+
FILTER_PROPS.has(propName) ? filterAttrValue(tsc, sf, a) : attrValue(tsc, sf, a)
|
|
2277
|
+
);
|
|
1665
2278
|
}
|
|
1666
2279
|
}
|
|
1667
2280
|
const where = `page "${name}" \u203A <${tag}>`;
|
|
@@ -1696,6 +2309,23 @@ function validateReactPageProps(stack) {
|
|
|
1696
2309
|
if (tag === "ObjectChart" && !hasSpread) {
|
|
1697
2310
|
checkObjectChart({ values, where, path }, objectFields, findings);
|
|
1698
2311
|
}
|
|
2312
|
+
if (tag === "ListView" && !hasSpread) {
|
|
2313
|
+
findings.push(
|
|
2314
|
+
...checkSearchableFieldList(
|
|
2315
|
+
values.get("searchableFields"),
|
|
2316
|
+
strOf(values.get("objectName")),
|
|
2317
|
+
searchTargets,
|
|
2318
|
+
where,
|
|
2319
|
+
`${path} \u203A searchableFields`,
|
|
2320
|
+
"searchableFields"
|
|
2321
|
+
)
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
if (!hasSpread) {
|
|
2325
|
+
findings.push(
|
|
2326
|
+
...checkBlockFieldProps(tag, values, objectFields, where, path)
|
|
2327
|
+
);
|
|
2328
|
+
}
|
|
1699
2329
|
}
|
|
1700
2330
|
}
|
|
1701
2331
|
tsc.forEachChild(node, visit);
|
|
@@ -1707,11 +2337,11 @@ function validateReactPageProps(stack) {
|
|
|
1707
2337
|
|
|
1708
2338
|
// src/validate-page-source-styling.ts
|
|
1709
2339
|
var PAGE_SOURCE_CLASSNAME = "page-source-className-tailwind";
|
|
1710
|
-
var
|
|
2340
|
+
var asArray13 = (v) => Array.isArray(v) ? v : [];
|
|
1711
2341
|
var CLASSNAME_ATTR = /\bclassName\s*=\s*["'{]/g;
|
|
1712
2342
|
function validatePageSourceStyling(stack) {
|
|
1713
2343
|
const findings = [];
|
|
1714
|
-
const pages =
|
|
2344
|
+
const pages = asArray13(stack.pages);
|
|
1715
2345
|
for (let p = 0; p < pages.length; p++) {
|
|
1716
2346
|
const page = pages[p];
|
|
1717
2347
|
if (!page) continue;
|
|
@@ -1740,7 +2370,7 @@ function validatePageSourceStyling(stack) {
|
|
|
1740
2370
|
import { objectTitleCompleteness } from "@objectstack/spec/data";
|
|
1741
2371
|
var TITLE_FORMAT_RETIRED = "title-format-retired";
|
|
1742
2372
|
var TITLE_UNRESOLVABLE = "title-unresolvable";
|
|
1743
|
-
function
|
|
2373
|
+
function asArray14(v) {
|
|
1744
2374
|
if (Array.isArray(v)) return v;
|
|
1745
2375
|
if (v && typeof v === "object") {
|
|
1746
2376
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1749,7 +2379,7 @@ function asArray12(v) {
|
|
|
1749
2379
|
}
|
|
1750
2380
|
function validateRecordTitle(stack) {
|
|
1751
2381
|
const findings = [];
|
|
1752
|
-
const objects =
|
|
2382
|
+
const objects = asArray14(stack.objects);
|
|
1753
2383
|
for (let i = 0; i < objects.length; i++) {
|
|
1754
2384
|
const obj = objects[i];
|
|
1755
2385
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
@@ -1785,7 +2415,7 @@ var FIELD_GROUP_UNDECLARED = "field-group-undeclared";
|
|
|
1785
2415
|
var FIELD_GROUP_EMPTY = "field-group-empty";
|
|
1786
2416
|
var FIELD_GROUP_SHADOWED = "field-group-shadowed";
|
|
1787
2417
|
var SEMANTIC_ROLE_FIELD_UNKNOWN = "semantic-role-field-unknown";
|
|
1788
|
-
function
|
|
2418
|
+
function asArray15(v) {
|
|
1789
2419
|
if (Array.isArray(v)) return v;
|
|
1790
2420
|
if (v && typeof v === "object") {
|
|
1791
2421
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1794,7 +2424,7 @@ function asArray13(v) {
|
|
|
1794
2424
|
}
|
|
1795
2425
|
function validateSemanticRoles(stack) {
|
|
1796
2426
|
const findings = [];
|
|
1797
|
-
const objects =
|
|
2427
|
+
const objects = asArray15(stack.objects);
|
|
1798
2428
|
for (let i = 0; i < objects.length; i++) {
|
|
1799
2429
|
const obj = objects[i];
|
|
1800
2430
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -1889,7 +2519,7 @@ function validateSemanticRoles(stack) {
|
|
|
1889
2519
|
// src/validate-form-layout.ts
|
|
1890
2520
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
1891
2521
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
1892
|
-
function
|
|
2522
|
+
function asArray16(v) {
|
|
1893
2523
|
if (Array.isArray(v)) return v;
|
|
1894
2524
|
if (v && typeof v === "object") {
|
|
1895
2525
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -1914,13 +2544,13 @@ function boundObject(view) {
|
|
|
1914
2544
|
function validateFormLayout(stack) {
|
|
1915
2545
|
const findings = [];
|
|
1916
2546
|
const objectFields = /* @__PURE__ */ new Map();
|
|
1917
|
-
for (const obj of
|
|
2547
|
+
for (const obj of asArray16(stack.objects)) {
|
|
1918
2548
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
1919
2549
|
if (!name) continue;
|
|
1920
2550
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
1921
2551
|
objectFields.set(name, new Set(fields));
|
|
1922
2552
|
}
|
|
1923
|
-
const views =
|
|
2553
|
+
const views = asArray16(stack.views);
|
|
1924
2554
|
for (let i = 0; i < views.length; i++) {
|
|
1925
2555
|
const view = views[i];
|
|
1926
2556
|
if (!view || typeof view !== "object") continue;
|
|
@@ -1970,7 +2600,7 @@ var VISIBILITY_ALIAS_DEPRECATED = "visibility-alias-deprecated";
|
|
|
1970
2600
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
1971
2601
|
var CANONICAL = "visibleWhen";
|
|
1972
2602
|
var ALIASES = ["visibleOn", "visibility"];
|
|
1973
|
-
function
|
|
2603
|
+
function asArray17(v) {
|
|
1974
2604
|
if (Array.isArray(v)) return v;
|
|
1975
2605
|
if (v && typeof v === "object") {
|
|
1976
2606
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2032,7 +2662,7 @@ function isFieldObject(entry) {
|
|
|
2032
2662
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
2033
2663
|
const layer = opts.layer ?? "runtime";
|
|
2034
2664
|
const findings = [];
|
|
2035
|
-
const views =
|
|
2665
|
+
const views = asArray17(stack.views);
|
|
2036
2666
|
for (let i = 0; i < views.length; i++) {
|
|
2037
2667
|
const view = views[i];
|
|
2038
2668
|
if (!view || typeof view !== "object") continue;
|
|
@@ -2055,7 +2685,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
2055
2685
|
}
|
|
2056
2686
|
}
|
|
2057
2687
|
}
|
|
2058
|
-
const pages =
|
|
2688
|
+
const pages = asArray17(stack.pages);
|
|
2059
2689
|
for (let i = 0; i < pages.length; i++) {
|
|
2060
2690
|
const page = pages[i];
|
|
2061
2691
|
if (!page || typeof page !== "object") continue;
|
|
@@ -2079,7 +2709,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
2079
2709
|
// src/validate-capability-references.ts
|
|
2080
2710
|
import { PLATFORM_CAPABILITY_NAMES } from "@objectstack/spec/security";
|
|
2081
2711
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
2082
|
-
function
|
|
2712
|
+
function asArray18(v) {
|
|
2083
2713
|
if (Array.isArray(v)) return v;
|
|
2084
2714
|
if (v && typeof v === "object") {
|
|
2085
2715
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2104,13 +2734,13 @@ function validateCapabilityReferences(stack) {
|
|
|
2104
2734
|
const findings = [];
|
|
2105
2735
|
if (!stack || typeof stack !== "object") return findings;
|
|
2106
2736
|
const known = new Set(PLATFORM_CAPABILITY_NAMES);
|
|
2107
|
-
for (const cap of
|
|
2737
|
+
for (const cap of asArray18(stack.capabilities)) {
|
|
2108
2738
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
2109
2739
|
}
|
|
2110
|
-
for (const ps of
|
|
2740
|
+
for (const ps of asArray18(stack.permissions)) {
|
|
2111
2741
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
2112
2742
|
}
|
|
2113
|
-
for (const seed of
|
|
2743
|
+
for (const seed of asArray18(stack.data)) {
|
|
2114
2744
|
if (seed.object !== "sys_capability") continue;
|
|
2115
2745
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
2116
2746
|
const name = rec?.name;
|
|
@@ -2129,7 +2759,7 @@ function validateCapabilityReferences(stack) {
|
|
|
2129
2759
|
hint
|
|
2130
2760
|
});
|
|
2131
2761
|
};
|
|
2132
|
-
const objects =
|
|
2762
|
+
const objects = asArray18(stack.objects);
|
|
2133
2763
|
for (let i = 0; i < objects.length; i++) {
|
|
2134
2764
|
const obj = objects[i];
|
|
2135
2765
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -2138,27 +2768,27 @@ function validateCapabilityReferences(stack) {
|
|
|
2138
2768
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
2139
2769
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
2140
2770
|
}
|
|
2141
|
-
const fields =
|
|
2771
|
+
const fields = asArray18(obj.fields);
|
|
2142
2772
|
for (const f of fields) {
|
|
2143
2773
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
2144
2774
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
2145
2775
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
2146
2776
|
}
|
|
2147
2777
|
}
|
|
2148
|
-
for (const [ai, action] of
|
|
2778
|
+
for (const [ai, action] of asArray18(obj.actions).entries()) {
|
|
2149
2779
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
2150
2780
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
2151
2781
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
2152
2782
|
}
|
|
2153
2783
|
}
|
|
2154
2784
|
}
|
|
2155
|
-
for (const [i, action] of
|
|
2785
|
+
for (const [i, action] of asArray18(stack.actions).entries()) {
|
|
2156
2786
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
2157
2787
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
2158
2788
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
2159
2789
|
}
|
|
2160
2790
|
}
|
|
2161
|
-
const apps =
|
|
2791
|
+
const apps = asArray18(stack.apps);
|
|
2162
2792
|
for (let i = 0; i < apps.length; i++) {
|
|
2163
2793
|
const app = apps[i];
|
|
2164
2794
|
if (!app || typeof app !== "object") continue;
|
|
@@ -2215,7 +2845,7 @@ var TYPE_FIX = {
|
|
|
2215
2845
|
business_unit: "department",
|
|
2216
2846
|
bu: "department"
|
|
2217
2847
|
};
|
|
2218
|
-
function
|
|
2848
|
+
function asArray19(v) {
|
|
2219
2849
|
if (Array.isArray(v)) return v;
|
|
2220
2850
|
if (v && typeof v === "object") {
|
|
2221
2851
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2225,15 +2855,15 @@ function asArray17(v) {
|
|
|
2225
2855
|
function validateApprovalApprovers(stack) {
|
|
2226
2856
|
const findings = [];
|
|
2227
2857
|
if (!stack || typeof stack !== "object") return findings;
|
|
2228
|
-
const flows =
|
|
2858
|
+
const flows = asArray19(stack.flows);
|
|
2229
2859
|
const validTypes = new Set(ApproverType.options);
|
|
2230
2860
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
2231
2861
|
const flow = flows[fi];
|
|
2232
2862
|
if (!flow || typeof flow !== "object") continue;
|
|
2233
2863
|
const flowName = typeof flow.name === "string" ? flow.name : `(flow ${fi})`;
|
|
2234
|
-
const
|
|
2235
|
-
for (let ni = 0; ni <
|
|
2236
|
-
const node =
|
|
2864
|
+
const walked = walkFlowNodes(flow, `flows[${fi}]`);
|
|
2865
|
+
for (let ni = 0; ni < walked.length; ni++) {
|
|
2866
|
+
const { node, path: nodePath } = walked[ni];
|
|
2237
2867
|
if (!node || node.type !== APPROVAL_NODE_TYPE) continue;
|
|
2238
2868
|
const nodeId = typeof node.id === "string" ? node.id : `(node ${ni})`;
|
|
2239
2869
|
const cfg = node.config ?? {};
|
|
@@ -2244,7 +2874,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2244
2874
|
if (!a || typeof a !== "object") continue;
|
|
2245
2875
|
const type = typeof a.type === "string" ? a.type : "";
|
|
2246
2876
|
const value = typeof a.value === "string" ? a.value : "";
|
|
2247
|
-
const path =
|
|
2877
|
+
const path = `${nodePath}.config.approvers[${ai}]`;
|
|
2248
2878
|
if (type && !validTypes.has(type)) {
|
|
2249
2879
|
const fix = TYPE_FIX[type];
|
|
2250
2880
|
findings.push({
|
|
@@ -2355,7 +2985,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2355
2985
|
severity: "info",
|
|
2356
2986
|
rule: APPROVAL_APPROVERS_MAY_RESOLVE_EMPTY,
|
|
2357
2987
|
where,
|
|
2358
|
-
path:
|
|
2988
|
+
path: `${nodePath}.config.approvers`,
|
|
2359
2989
|
message: `every approver on this node routes to a group (position/team/department) whose members are runtime data \u2014 if none is staffed, the request resolves to an empty slate and waits forever` + (locks ? `, and (lockRecord) the record stays locked with no in-product recovery.` : `.`),
|
|
2360
2990
|
hint: `Make sure at least one target is always staffed, or add a guaranteed-staffed fallback approver, e.g. { type: 'org_membership_level', value: 'owner' }. A request that still lands empty is recoverable only by a platform/tenant admin override (#3424).`
|
|
2361
2991
|
});
|
|
@@ -2368,7 +2998,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2368
2998
|
severity: "info",
|
|
2369
2999
|
rule: APPROVAL_EXPRESSION_NO_EMPTY_POLICY,
|
|
2370
3000
|
where,
|
|
2371
|
-
path:
|
|
3001
|
+
path: `${nodePath}.config`,
|
|
2372
3002
|
message: `this node resolves approvers from an expression but declares no onEmptyApprovers \u2014 an empty result falls back to the default ('admin_rescue': request opens, only a privileged admin can act).`,
|
|
2373
3003
|
hint: `Declare the empty-slate policy explicitly: onEmptyApprovers: 'admin_rescue' (hold for admin takeover), 'fail' (fail the node \u2014 config bug), or 'auto_approve' (wave through, output.autoApproved = true).`
|
|
2374
3004
|
});
|
|
@@ -2380,7 +3010,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2380
3010
|
severity: "error",
|
|
2381
3011
|
rule: APPROVAL_DECISION_OUTPUTS_RESERVED,
|
|
2382
3012
|
where,
|
|
2383
|
-
path:
|
|
3013
|
+
path: `${nodePath}.config.decisionOutputs`,
|
|
2384
3014
|
message: `decisionOutputs declares reserved key(s) \`${reserved.join("`, `")}\` \u2014 the resume envelope owns them, so every decide carrying them is rejected.`,
|
|
2385
3015
|
hint: `Rename the output key(s); any name other than 'decision'/'requestId' works.`
|
|
2386
3016
|
});
|
|
@@ -2393,7 +3023,7 @@ function validateApprovalApprovers(stack) {
|
|
|
2393
3023
|
severity: "warning",
|
|
2394
3024
|
rule: APPROVAL_ESCALATION_REASSIGN_NO_TARGET,
|
|
2395
3025
|
where,
|
|
2396
|
-
path:
|
|
3026
|
+
path: `${nodePath}.config.escalation.escalateTo`,
|
|
2397
3027
|
message: `escalation.action is 'reassign' but escalateTo is empty \u2014 at runtime the escalation degrades to a notify and the request stays with the original approvers.`,
|
|
2398
3028
|
hint: `Set escalateTo to a position machine name (expanded via sys_user_position, ADR-0090 D3) or a specific user id, or change action to 'notify'.`
|
|
2399
3029
|
});
|
|
@@ -2520,7 +3150,7 @@ var OWD_WIDTH = {
|
|
|
2520
3150
|
public_read: 1,
|
|
2521
3151
|
public_read_write: 2
|
|
2522
3152
|
};
|
|
2523
|
-
function
|
|
3153
|
+
function asArray20(v) {
|
|
2524
3154
|
if (Array.isArray(v)) return v;
|
|
2525
3155
|
if (v && typeof v === "object") {
|
|
2526
3156
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2546,7 +3176,7 @@ function refOf(def) {
|
|
|
2546
3176
|
return typeof r === "string" && r ? r : void 0;
|
|
2547
3177
|
}
|
|
2548
3178
|
function firstMasterDetailField(obj) {
|
|
2549
|
-
for (const f of
|
|
3179
|
+
for (const f of asArray20(obj.fields)) {
|
|
2550
3180
|
if (f.type === "master_detail") {
|
|
2551
3181
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
2552
3182
|
}
|
|
@@ -2559,8 +3189,8 @@ function grantsObjectAccess(p) {
|
|
|
2559
3189
|
function validateSecurityPosture(stack, opts) {
|
|
2560
3190
|
const findings = [];
|
|
2561
3191
|
if (!stack || typeof stack !== "object") return findings;
|
|
2562
|
-
const objects =
|
|
2563
|
-
const permissionSets =
|
|
3192
|
+
const objects = asArray20(stack.objects);
|
|
3193
|
+
const permissionSets = asArray20(stack.permissions);
|
|
2564
3194
|
for (let i = 0; i < objects.length; i++) {
|
|
2565
3195
|
const obj = objects[i];
|
|
2566
3196
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -2689,10 +3319,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2689
3319
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
2690
3320
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
2691
3321
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
2692
|
-
for (const f of
|
|
3322
|
+
for (const f of asArray20(obj.fields)) {
|
|
2693
3323
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
2694
3324
|
}
|
|
2695
|
-
for (const [ai, action] of
|
|
3325
|
+
for (const [ai, action] of asArray20(obj.actions).entries()) {
|
|
2696
3326
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
2697
3327
|
}
|
|
2698
3328
|
}
|
|
@@ -2701,19 +3331,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2701
3331
|
if (!ps || typeof ps !== "object") continue;
|
|
2702
3332
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
2703
3333
|
}
|
|
2704
|
-
for (const [i, pos] of
|
|
3334
|
+
for (const [i, pos] of asArray20(stack.positions).entries()) {
|
|
2705
3335
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
2706
3336
|
}
|
|
2707
|
-
for (const [i, app] of
|
|
3337
|
+
for (const [i, app] of asArray20(stack.apps).entries()) {
|
|
2708
3338
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
2709
3339
|
}
|
|
2710
|
-
for (const [i, book] of
|
|
3340
|
+
for (const [i, book] of asArray20(stack.books).entries()) {
|
|
2711
3341
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
2712
3342
|
}
|
|
2713
3343
|
const stackSetNames = new Set(
|
|
2714
3344
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
2715
3345
|
);
|
|
2716
|
-
for (const [i, book] of
|
|
3346
|
+
for (const [i, book] of asArray20(stack.books).entries()) {
|
|
2717
3347
|
const audience = book.audience;
|
|
2718
3348
|
if (!audience || typeof audience !== "object") continue;
|
|
2719
3349
|
const setName = audience.permissionSet;
|
|
@@ -2791,7 +3421,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2791
3421
|
}
|
|
2792
3422
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
2793
3423
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
2794
|
-
for (const [i, seed] of
|
|
3424
|
+
for (const [i, seed] of asArray20(stack.data).entries()) {
|
|
2795
3425
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
2796
3426
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
2797
3427
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -2835,7 +3465,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
2835
3465
|
var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
2836
3466
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
2837
3467
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
2838
|
-
function
|
|
3468
|
+
function asArray21(v) {
|
|
2839
3469
|
if (Array.isArray(v)) return v;
|
|
2840
3470
|
if (v && typeof v === "object") {
|
|
2841
3471
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -2856,9 +3486,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
2856
3486
|
function validateOrgAxisRedLines(stack) {
|
|
2857
3487
|
const findings = [];
|
|
2858
3488
|
const cfg = stack ?? {};
|
|
2859
|
-
const permissionSets =
|
|
3489
|
+
const permissionSets = asArray21(cfg.permissions ?? cfg.permissionSets);
|
|
2860
3490
|
permissionSets.forEach((ps, psIndex) => {
|
|
2861
|
-
|
|
3491
|
+
asArray21(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
2862
3492
|
for (const clause of ["using", "check"]) {
|
|
2863
3493
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
2864
3494
|
findings.push({
|
|
@@ -2872,10 +3502,10 @@ function validateOrgAxisRedLines(stack) {
|
|
|
2872
3502
|
}
|
|
2873
3503
|
});
|
|
2874
3504
|
});
|
|
2875
|
-
const objects =
|
|
3505
|
+
const objects = asArray21(cfg.objects);
|
|
2876
3506
|
objects.forEach((object, oIndex) => {
|
|
2877
3507
|
const objectName = str(object.name) || String(oIndex);
|
|
2878
|
-
|
|
3508
|
+
asArray21(object.rowLevelSecurity ?? object.rls).forEach((policy, pIndex) => {
|
|
2879
3509
|
for (const clause of ["using", "check"]) {
|
|
2880
3510
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
2881
3511
|
findings.push({
|
|
@@ -2889,7 +3519,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
2889
3519
|
}
|
|
2890
3520
|
});
|
|
2891
3521
|
});
|
|
2892
|
-
|
|
3522
|
+
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
2893
3523
|
const criteria = JSON.stringify(rule.criteria ?? rule.filter ?? "");
|
|
2894
3524
|
const sharedTo = JSON.stringify(rule.sharedTo ?? rule.recipient ?? "");
|
|
2895
3525
|
if (criteria.includes(ORG_PARENT_FIELD) || sharedTo.includes(ORG_PARENT_FIELD)) {
|
|
@@ -2906,7 +3536,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
2906
3536
|
const tenancyDisabledObjects = new Set(
|
|
2907
3537
|
objects.filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
2908
3538
|
);
|
|
2909
|
-
|
|
3539
|
+
asArray21(cfg.sharingRules ?? cfg.sharing).forEach((rule, rIndex) => {
|
|
2910
3540
|
const target = str(rule.object ?? rule.objectName);
|
|
2911
3541
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
2912
3542
|
const sharedTo = rule.sharedTo ?? rule.recipient;
|
|
@@ -2927,14 +3557,14 @@ function validateOrgAxisRedLines(stack) {
|
|
|
2927
3557
|
// src/validate-dashboard-action-refs.ts
|
|
2928
3558
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
2929
3559
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
2930
|
-
function
|
|
3560
|
+
function asArray22(v) {
|
|
2931
3561
|
if (Array.isArray(v)) return v;
|
|
2932
3562
|
if (v && typeof v === "object") {
|
|
2933
3563
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
2934
3564
|
}
|
|
2935
3565
|
return [];
|
|
2936
3566
|
}
|
|
2937
|
-
function
|
|
3567
|
+
function strName5(v) {
|
|
2938
3568
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
2939
3569
|
}
|
|
2940
3570
|
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
@@ -2951,7 +3581,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
|
|
|
2951
3581
|
views: "views"
|
|
2952
3582
|
};
|
|
2953
3583
|
function viewContainerName(item) {
|
|
2954
|
-
return
|
|
3584
|
+
return strName5(item.name) ?? strName5(item.id) ?? strName5(item.object) ?? strName5(item.list?.data && item.list.data.object) ?? strName5(item.form?.data && item.form.data.object);
|
|
2955
3585
|
}
|
|
2956
3586
|
function collectKnownTargets(stack) {
|
|
2957
3587
|
const actions = /* @__PURE__ */ new Set();
|
|
@@ -2961,22 +3591,22 @@ function collectKnownTargets(stack) {
|
|
|
2961
3591
|
const pages = /* @__PURE__ */ new Set();
|
|
2962
3592
|
const views = /* @__PURE__ */ new Set();
|
|
2963
3593
|
const collectNames = (v, into, name) => {
|
|
2964
|
-
for (const item of
|
|
3594
|
+
for (const item of asArray22(v)) {
|
|
2965
3595
|
if (!item || typeof item !== "object") continue;
|
|
2966
3596
|
const n = name(item);
|
|
2967
3597
|
if (n) into.add(n);
|
|
2968
3598
|
}
|
|
2969
3599
|
};
|
|
2970
|
-
collectNames(stack.actions, actions, (a) =>
|
|
2971
|
-
for (const obj of
|
|
3600
|
+
collectNames(stack.actions, actions, (a) => strName5(a.name));
|
|
3601
|
+
for (const obj of asArray22(stack.objects)) {
|
|
2972
3602
|
if (!obj || typeof obj !== "object") continue;
|
|
2973
|
-
const n =
|
|
3603
|
+
const n = strName5(obj.name);
|
|
2974
3604
|
if (n) objects.add(n);
|
|
2975
|
-
collectNames(obj.actions, actions, (a) =>
|
|
3605
|
+
collectNames(obj.actions, actions, (a) => strName5(a.name));
|
|
2976
3606
|
}
|
|
2977
|
-
collectNames(stack.reports, reports, (r) =>
|
|
2978
|
-
collectNames(stack.dashboards, dashboards, (d) =>
|
|
2979
|
-
collectNames(stack.pages, pages, (p) =>
|
|
3607
|
+
collectNames(stack.reports, reports, (r) => strName5(r.name));
|
|
3608
|
+
collectNames(stack.dashboards, dashboards, (d) => strName5(d.name));
|
|
3609
|
+
collectNames(stack.pages, pages, (p) => strName5(p.name));
|
|
2980
3610
|
collectNames(stack.views, views, viewContainerName);
|
|
2981
3611
|
for (const o of objects) views.add(o);
|
|
2982
3612
|
return { actions, objects, reports, dashboards, pages, views };
|
|
@@ -3008,14 +3638,14 @@ function resolveUrlRoute(target, known) {
|
|
|
3008
3638
|
function validateDashboardActionRefs(stack) {
|
|
3009
3639
|
const findings = [];
|
|
3010
3640
|
if (!stack || typeof stack !== "object") return findings;
|
|
3011
|
-
const dashboards =
|
|
3641
|
+
const dashboards = asArray22(stack.dashboards);
|
|
3012
3642
|
if (dashboards.length === 0) return findings;
|
|
3013
3643
|
const known = collectKnownTargets(stack);
|
|
3014
3644
|
const checkOne = (action, where, path) => {
|
|
3015
|
-
const target =
|
|
3645
|
+
const target = strName5(action.actionUrl);
|
|
3016
3646
|
if (!target) return;
|
|
3017
3647
|
if (target.includes("${")) return;
|
|
3018
|
-
const actionType =
|
|
3648
|
+
const actionType = strName5(action.actionType) ?? "url";
|
|
3019
3649
|
if (actionType === "script" || actionType === "modal") {
|
|
3020
3650
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
3021
3651
|
const kindWord = actionType === "script" ? "script" : "modal";
|
|
@@ -3046,25 +3676,25 @@ function validateDashboardActionRefs(stack) {
|
|
|
3046
3676
|
for (let di = 0; di < dashboards.length; di++) {
|
|
3047
3677
|
const dash = dashboards[di];
|
|
3048
3678
|
if (!dash || typeof dash !== "object") continue;
|
|
3049
|
-
const dashName =
|
|
3679
|
+
const dashName = strName5(dash.name) ?? `(dashboard ${di})`;
|
|
3050
3680
|
const dashPath = `dashboards[${di}]`;
|
|
3051
|
-
const headerActions =
|
|
3681
|
+
const headerActions = asArray22(dash.header?.actions);
|
|
3052
3682
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
3053
3683
|
const action = headerActions[ai];
|
|
3054
3684
|
if (!action || typeof action !== "object") continue;
|
|
3055
|
-
const label2 =
|
|
3685
|
+
const label2 = strName5(action.label) ?? strName5(action.actionUrl) ?? `#${ai}`;
|
|
3056
3686
|
checkOne(
|
|
3057
3687
|
action,
|
|
3058
3688
|
`dashboard "${dashName}" \xB7 header action "${label2}"`,
|
|
3059
3689
|
`${dashPath}.header.actions[${ai}].actionUrl`
|
|
3060
3690
|
);
|
|
3061
3691
|
}
|
|
3062
|
-
const widgets =
|
|
3692
|
+
const widgets = asArray22(dash.widgets);
|
|
3063
3693
|
for (let wi = 0; wi < widgets.length; wi++) {
|
|
3064
3694
|
const widget = widgets[wi];
|
|
3065
3695
|
if (!widget || typeof widget !== "object") continue;
|
|
3066
|
-
if (!
|
|
3067
|
-
const widgetId =
|
|
3696
|
+
if (!strName5(widget.actionUrl)) continue;
|
|
3697
|
+
const widgetId = strName5(widget.id) ?? `#${wi}`;
|
|
3068
3698
|
checkOne(
|
|
3069
3699
|
{ actionType: widget.actionType, actionUrl: widget.actionUrl },
|
|
3070
3700
|
`dashboard "${dashName}" \xB7 widget "${widgetId}" action`,
|
|
@@ -3079,7 +3709,7 @@ function validateDashboardActionRefs(stack) {
|
|
|
3079
3709
|
import { classifyFilterToken, CONTEXT_TOKENS } from "@objectstack/spec/data";
|
|
3080
3710
|
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
3081
3711
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
3082
|
-
function
|
|
3712
|
+
function asArray23(v) {
|
|
3083
3713
|
if (Array.isArray(v)) return v;
|
|
3084
3714
|
if (v && typeof v === "object") {
|
|
3085
3715
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3148,7 +3778,7 @@ function validateFilterTokens(stack) {
|
|
|
3148
3778
|
["apps", "app"]
|
|
3149
3779
|
];
|
|
3150
3780
|
for (const [key, kind] of surfaces) {
|
|
3151
|
-
const items =
|
|
3781
|
+
const items = asArray23(stack[key]);
|
|
3152
3782
|
items.forEach((item, i) => {
|
|
3153
3783
|
const name = label(item.name ?? item.id, `#${i}`);
|
|
3154
3784
|
if (kind === "dashboard") {
|
|
@@ -3182,14 +3812,14 @@ import {
|
|
|
3182
3812
|
var PLATFORM_NAMES = [...PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
3183
3813
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
3184
3814
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
3185
|
-
function
|
|
3815
|
+
function asArray24(v) {
|
|
3186
3816
|
if (Array.isArray(v)) return v;
|
|
3187
3817
|
if (v && typeof v === "object") {
|
|
3188
3818
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3189
3819
|
}
|
|
3190
3820
|
return [];
|
|
3191
3821
|
}
|
|
3192
|
-
function
|
|
3822
|
+
function strName6(v) {
|
|
3193
3823
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3194
3824
|
}
|
|
3195
3825
|
function isInterpolated(target) {
|
|
@@ -3197,11 +3827,11 @@ function isInterpolated(target) {
|
|
|
3197
3827
|
const open = target.indexOf("{");
|
|
3198
3828
|
return open !== -1 && target.indexOf("}", open + 2) !== -1;
|
|
3199
3829
|
}
|
|
3200
|
-
function
|
|
3830
|
+
function suggest3(target, known) {
|
|
3201
3831
|
let best;
|
|
3202
3832
|
let bestScore = Infinity;
|
|
3203
3833
|
for (const candidate of known) {
|
|
3204
|
-
const d =
|
|
3834
|
+
const d = distance2(target, candidate);
|
|
3205
3835
|
if (d < bestScore) {
|
|
3206
3836
|
bestScore = d;
|
|
3207
3837
|
best = candidate;
|
|
@@ -3210,7 +3840,7 @@ function suggest2(target, known) {
|
|
|
3210
3840
|
const limit = Math.max(2, Math.floor(target.length / 3));
|
|
3211
3841
|
return best && bestScore <= limit ? ` Did you mean "${best}"?` : "";
|
|
3212
3842
|
}
|
|
3213
|
-
function
|
|
3843
|
+
function distance2(a, b) {
|
|
3214
3844
|
const m = a.length;
|
|
3215
3845
|
const n = b.length;
|
|
3216
3846
|
if (m === 0) return n;
|
|
@@ -3229,14 +3859,14 @@ function distance(a, b) {
|
|
|
3229
3859
|
function validateObjectReferences(stack) {
|
|
3230
3860
|
const findings = [];
|
|
3231
3861
|
if (!stack || typeof stack !== "object") return findings;
|
|
3232
|
-
const objects =
|
|
3862
|
+
const objects = asArray24(stack.objects);
|
|
3233
3863
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
3234
3864
|
for (const obj of objects) {
|
|
3235
|
-
const n =
|
|
3865
|
+
const n = strName6(obj.name);
|
|
3236
3866
|
if (n) ownObjects.add(n);
|
|
3237
3867
|
}
|
|
3238
3868
|
const check = (target, where, path, subject, fix) => {
|
|
3239
|
-
const name =
|
|
3869
|
+
const name = strName6(target);
|
|
3240
3870
|
if (!name) return;
|
|
3241
3871
|
if (isInterpolated(name)) return;
|
|
3242
3872
|
if (ownObjects.has(name)) return;
|
|
@@ -3247,7 +3877,7 @@ function validateObjectReferences(stack) {
|
|
|
3247
3877
|
rule: OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
3248
3878
|
where,
|
|
3249
3879
|
path,
|
|
3250
|
-
message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` +
|
|
3880
|
+
message: `${subject} "${name}" carries a platform namespace prefix, but no platform package, official plugin, or cloud runtime object registers that name \u2014 and this stack does not define it either. If nothing provides it at runtime the reference resolves to nothing and fails silently.` + suggest3(name, PLATFORM_NAMES),
|
|
3251
3881
|
hint: `Check the spelling against the object the providing package actually registers (e.g. "sys_approval_request", not "sys_approval_process" \u2014 the process object was removed when approval became a flow node, ADR-0019). If a third-party package genuinely provides it, this warning is expected. ${fix}`
|
|
3252
3882
|
});
|
|
3253
3883
|
return;
|
|
@@ -3257,26 +3887,26 @@ function validateObjectReferences(stack) {
|
|
|
3257
3887
|
rule: OBJECT_REFERENCE_UNKNOWN,
|
|
3258
3888
|
where,
|
|
3259
3889
|
path,
|
|
3260
|
-
message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` +
|
|
3890
|
+
message: `${subject} "${name}" resolves to no object defined in this stack. The reference is inert at runtime \u2014 nothing reports the miss.` + suggest3(name, ownObjects),
|
|
3261
3891
|
hint: `Point it at one of this stack's objects, or at a platform object by its full name (the platform user object is "sys_user", not "user"). ${fix}` + (ownObjects.size > 0 ? ` Defined objects: ${[...ownObjects].sort().join(", ")}.` : "")
|
|
3262
3892
|
});
|
|
3263
3893
|
};
|
|
3264
3894
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
3265
|
-
const params =
|
|
3895
|
+
const params = asArray24(action.params);
|
|
3266
3896
|
for (let pi = 0; pi < params.length; pi++) {
|
|
3267
3897
|
const param = params[pi];
|
|
3268
3898
|
if (!param || typeof param !== "object") continue;
|
|
3269
|
-
const paramLabel =
|
|
3899
|
+
const paramLabel = strName6(param.name) ?? strName6(param.field) ?? `#${pi}`;
|
|
3270
3900
|
const where = `${actionLabel} \xB7 param "${paramLabel}"`;
|
|
3271
3901
|
check(
|
|
3272
|
-
|
|
3902
|
+
strName6(param.reference),
|
|
3273
3903
|
where,
|
|
3274
3904
|
`${actionPath}.params[${pi}].reference`,
|
|
3275
3905
|
"record-picker target",
|
|
3276
3906
|
"Without a resolvable target the picker degrades to a raw record-id text input."
|
|
3277
3907
|
);
|
|
3278
3908
|
check(
|
|
3279
|
-
|
|
3909
|
+
strName6(param.objectOverride),
|
|
3280
3910
|
where,
|
|
3281
3911
|
`${actionPath}.params[${pi}].objectOverride`,
|
|
3282
3912
|
"field-backed param object",
|
|
@@ -3284,70 +3914,70 @@ function validateObjectReferences(stack) {
|
|
|
3284
3914
|
);
|
|
3285
3915
|
}
|
|
3286
3916
|
};
|
|
3287
|
-
const globalActions =
|
|
3917
|
+
const globalActions = asArray24(stack.actions);
|
|
3288
3918
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
3289
3919
|
const action = globalActions[ai];
|
|
3290
3920
|
if (!action || typeof action !== "object") continue;
|
|
3291
|
-
checkActionParams2(action, `actions[${ai}]`, `action "${
|
|
3921
|
+
checkActionParams2(action, `actions[${ai}]`, `action "${strName6(action.name) ?? `#${ai}`}"`);
|
|
3292
3922
|
}
|
|
3293
3923
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
3294
3924
|
const obj = objects[oi];
|
|
3295
3925
|
if (!obj || typeof obj !== "object") continue;
|
|
3296
|
-
const objName =
|
|
3297
|
-
const objActions =
|
|
3926
|
+
const objName = strName6(obj.name) ?? `#${oi}`;
|
|
3927
|
+
const objActions = asArray24(obj.actions);
|
|
3298
3928
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
3299
3929
|
const action = objActions[ai];
|
|
3300
3930
|
if (!action || typeof action !== "object") continue;
|
|
3301
3931
|
checkActionParams2(
|
|
3302
3932
|
action,
|
|
3303
3933
|
`objects[${oi}].actions[${ai}]`,
|
|
3304
|
-
`object "${objName}" \xB7 action "${
|
|
3934
|
+
`object "${objName}" \xB7 action "${strName6(action.name) ?? `#${ai}`}"`
|
|
3305
3935
|
);
|
|
3306
3936
|
}
|
|
3307
3937
|
}
|
|
3308
|
-
const dashboards =
|
|
3938
|
+
const dashboards = asArray24(stack.dashboards);
|
|
3309
3939
|
for (let di = 0; di < dashboards.length; di++) {
|
|
3310
3940
|
const dash = dashboards[di];
|
|
3311
3941
|
if (!dash || typeof dash !== "object") continue;
|
|
3312
|
-
const dashName =
|
|
3313
|
-
const filters =
|
|
3942
|
+
const dashName = strName6(dash.name) ?? `#${di}`;
|
|
3943
|
+
const filters = asArray24(dash.globalFilters);
|
|
3314
3944
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
3315
3945
|
const filter = filters[fi];
|
|
3316
3946
|
if (!filter || typeof filter !== "object") continue;
|
|
3317
3947
|
const optionsFrom = filter.optionsFrom;
|
|
3318
3948
|
if (!optionsFrom || typeof optionsFrom !== "object") continue;
|
|
3319
3949
|
check(
|
|
3320
|
-
|
|
3321
|
-
`dashboard "${dashName}" \xB7 filter "${
|
|
3950
|
+
strName6(optionsFrom.object),
|
|
3951
|
+
`dashboard "${dashName}" \xB7 filter "${strName6(filter.name) ?? `#${fi}`}"`,
|
|
3322
3952
|
`dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
|
|
3323
3953
|
"filter options source",
|
|
3324
3954
|
"The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
|
|
3325
3955
|
);
|
|
3326
3956
|
}
|
|
3327
3957
|
}
|
|
3328
|
-
const apps =
|
|
3958
|
+
const apps = asArray24(stack.apps);
|
|
3329
3959
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
3330
3960
|
const app = apps[ai];
|
|
3331
3961
|
if (!app || typeof app !== "object") continue;
|
|
3332
|
-
const appName =
|
|
3962
|
+
const appName = strName6(app.name) ?? `#${ai}`;
|
|
3333
3963
|
const walkNav = (items, basePath) => {
|
|
3334
|
-
const navItems =
|
|
3964
|
+
const navItems = asArray24(items);
|
|
3335
3965
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
3336
3966
|
const nav = navItems[ni];
|
|
3337
3967
|
if (!nav || typeof nav !== "object") continue;
|
|
3338
|
-
const navId =
|
|
3968
|
+
const navId = strName6(nav.id) ?? `#${ni}`;
|
|
3339
3969
|
const where = `app "${appName}" \xB7 nav "${navId}"`;
|
|
3340
3970
|
const navPath = `${basePath}[${ni}]`;
|
|
3341
3971
|
check(
|
|
3342
|
-
|
|
3972
|
+
strName6(nav.requiresObject),
|
|
3343
3973
|
where,
|
|
3344
3974
|
`${navPath}.requiresObject`,
|
|
3345
3975
|
"capability gate object",
|
|
3346
3976
|
"The entry is hidden unless this object is registered, so a typo hides it permanently \u2014 and it suppresses the nav cross-reference check that would have caught the target."
|
|
3347
3977
|
);
|
|
3348
|
-
if (nav.requiresObject &&
|
|
3978
|
+
if (nav.requiresObject && strName6(nav.objectName)) {
|
|
3349
3979
|
check(
|
|
3350
|
-
|
|
3980
|
+
strName6(nav.objectName),
|
|
3351
3981
|
where,
|
|
3352
3982
|
`${navPath}.objectName`,
|
|
3353
3983
|
"navigation target",
|
|
@@ -3358,7 +3988,7 @@ function validateObjectReferences(stack) {
|
|
|
3358
3988
|
}
|
|
3359
3989
|
};
|
|
3360
3990
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
3361
|
-
const areas =
|
|
3991
|
+
const areas = asArray24(app.areas);
|
|
3362
3992
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
3363
3993
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3364
3994
|
}
|
|
@@ -3366,88 +3996,22 @@ function validateObjectReferences(stack) {
|
|
|
3366
3996
|
return findings;
|
|
3367
3997
|
}
|
|
3368
3998
|
|
|
3369
|
-
// src/page-walk.ts
|
|
3370
|
-
function isRec2(v) {
|
|
3371
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3372
|
-
}
|
|
3373
|
-
function strName3(v) {
|
|
3374
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3375
|
-
}
|
|
3376
|
-
var SOURCE_AUTHORED_KINDS = /* @__PURE__ */ new Set(["html", "react", "jsx"]);
|
|
3377
|
-
function isSourceAuthoredPage(page) {
|
|
3378
|
-
const kind = strName3(page.kind);
|
|
3379
|
-
return kind !== void 0 && SOURCE_AUTHORED_KINDS.has(kind);
|
|
3380
|
-
}
|
|
3381
|
-
function walkPageComponents(page, pagePath) {
|
|
3382
|
-
const out = [];
|
|
3383
|
-
if (!isRec2(page) || isSourceAuthoredPage(page)) return out;
|
|
3384
|
-
const pageObject = strName3(page.object);
|
|
3385
|
-
const visit = (node, path, inheritedObject) => {
|
|
3386
|
-
if (!isRec2(node)) return;
|
|
3387
|
-
const props = isRec2(node.properties) ? node.properties : void 0;
|
|
3388
|
-
const dataSource = isRec2(node.dataSource) ? node.dataSource : void 0;
|
|
3389
|
-
const objectName = strName3(dataSource?.object) ?? strName3(props?.object) ?? inheritedObject;
|
|
3390
|
-
out.push({ component: node, path, objectName });
|
|
3391
|
-
if (!props) return;
|
|
3392
|
-
if (Array.isArray(props.items)) {
|
|
3393
|
-
for (let i = 0; i < props.items.length; i++) {
|
|
3394
|
-
const item = props.items[i];
|
|
3395
|
-
if (!isRec2(item) || !Array.isArray(item.children)) continue;
|
|
3396
|
-
for (let c = 0; c < item.children.length; c++) {
|
|
3397
|
-
visit(item.children[c], `${path}.properties.items[${i}].children[${c}]`, objectName);
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
if (Array.isArray(props.children)) {
|
|
3402
|
-
for (let i = 0; i < props.children.length; i++) {
|
|
3403
|
-
visit(props.children[i], `${path}.properties.children[${i}]`, objectName);
|
|
3404
|
-
}
|
|
3405
|
-
}
|
|
3406
|
-
for (const key of ["body", "footer"]) {
|
|
3407
|
-
const slotList = props[key];
|
|
3408
|
-
if (!Array.isArray(slotList)) continue;
|
|
3409
|
-
for (let i = 0; i < slotList.length; i++) {
|
|
3410
|
-
visit(slotList[i], `${path}.properties.${key}[${i}]`, objectName);
|
|
3411
|
-
}
|
|
3412
|
-
}
|
|
3413
|
-
};
|
|
3414
|
-
const regions = Array.isArray(page.regions) ? page.regions : [];
|
|
3415
|
-
for (let r = 0; r < regions.length; r++) {
|
|
3416
|
-
const region = regions[r];
|
|
3417
|
-
if (!isRec2(region) || !Array.isArray(region.components)) continue;
|
|
3418
|
-
for (let c = 0; c < region.components.length; c++) {
|
|
3419
|
-
visit(region.components[c], `${pagePath}.regions[${r}].components[${c}]`, pageObject);
|
|
3420
|
-
}
|
|
3421
|
-
}
|
|
3422
|
-
const slots = isRec2(page.slots) ? page.slots : void 0;
|
|
3423
|
-
if (slots) {
|
|
3424
|
-
for (const [slot, value] of Object.entries(slots)) {
|
|
3425
|
-
const list3 = Array.isArray(value) ? value : [value];
|
|
3426
|
-
const indexed = Array.isArray(value);
|
|
3427
|
-
for (let i = 0; i < list3.length; i++) {
|
|
3428
|
-
visit(list3[i], `${pagePath}.slots.${slot}${indexed ? `[${i}]` : ""}`, pageObject);
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
return out;
|
|
3433
|
-
}
|
|
3434
|
-
|
|
3435
3999
|
// src/validate-action-name-refs.ts
|
|
3436
4000
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
3437
|
-
function
|
|
4001
|
+
function asArray25(v) {
|
|
3438
4002
|
if (Array.isArray(v)) return v;
|
|
3439
4003
|
if (v && typeof v === "object") {
|
|
3440
4004
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3441
4005
|
}
|
|
3442
4006
|
return [];
|
|
3443
4007
|
}
|
|
3444
|
-
function
|
|
4008
|
+
function strName7(v) {
|
|
3445
4009
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3446
4010
|
}
|
|
3447
4011
|
function strList(v) {
|
|
3448
4012
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
3449
4013
|
}
|
|
3450
|
-
function
|
|
4014
|
+
function distance3(a, b) {
|
|
3451
4015
|
const m = a.length;
|
|
3452
4016
|
const n = b.length;
|
|
3453
4017
|
if (m === 0) return n;
|
|
@@ -3463,11 +4027,11 @@ function distance2(a, b) {
|
|
|
3463
4027
|
}
|
|
3464
4028
|
return prev[n];
|
|
3465
4029
|
}
|
|
3466
|
-
function
|
|
4030
|
+
function suggest4(target, known) {
|
|
3467
4031
|
let best;
|
|
3468
4032
|
let bestScore = Infinity;
|
|
3469
4033
|
for (const candidate of known) {
|
|
3470
|
-
const d =
|
|
4034
|
+
const d = distance3(target, candidate);
|
|
3471
4035
|
if (d < bestScore) {
|
|
3472
4036
|
bestScore = d;
|
|
3473
4037
|
best = candidate;
|
|
@@ -3478,14 +4042,14 @@ function suggest3(target, known) {
|
|
|
3478
4042
|
}
|
|
3479
4043
|
function collectActionNames(stack) {
|
|
3480
4044
|
const names = /* @__PURE__ */ new Set();
|
|
3481
|
-
for (const action of
|
|
3482
|
-
const n =
|
|
4045
|
+
for (const action of asArray25(stack.actions)) {
|
|
4046
|
+
const n = strName7(action?.name);
|
|
3483
4047
|
if (n) names.add(n);
|
|
3484
4048
|
}
|
|
3485
|
-
for (const obj of
|
|
4049
|
+
for (const obj of asArray25(stack.objects)) {
|
|
3486
4050
|
if (!obj || typeof obj !== "object") continue;
|
|
3487
|
-
for (const action of
|
|
3488
|
-
const n =
|
|
4051
|
+
for (const action of asArray25(obj.actions)) {
|
|
4052
|
+
const n = strName7(action?.name);
|
|
3489
4053
|
if (n) names.add(n);
|
|
3490
4054
|
}
|
|
3491
4055
|
}
|
|
@@ -3502,257 +4066,85 @@ function validateActionNameRefs(stack) {
|
|
|
3502
4066
|
rule: ACTION_NAME_UNDEFINED,
|
|
3503
4067
|
where,
|
|
3504
4068
|
path,
|
|
3505
|
-
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` +
|
|
4069
|
+
message: `${surface} names action "${name}", which is defined by no action in this stack (neither \`stack.actions\` nor any object's \`actions\`). The button renders and does nothing when clicked \u2014 a dead affordance the runtime cannot dispatch.` + suggest4(name, known),
|
|
3506
4070
|
hint: `Define an action named "${name}" (in \`stack.actions\` or the object's \`actions\`) with the location this surface needs, remove the reference, or ignore this if the action is contributed by another installed package.` + (known.size > 0 ? ` Defined actions: ${[...known].sort().join(", ")}.` : "")
|
|
3507
4071
|
});
|
|
3508
4072
|
};
|
|
3509
|
-
const views =
|
|
4073
|
+
const views = asArray25(stack.views);
|
|
3510
4074
|
for (let vi = 0; vi < views.length; vi++) {
|
|
3511
4075
|
const view = views[vi];
|
|
3512
4076
|
if (!view || typeof view !== "object") continue;
|
|
3513
|
-
const viewName =
|
|
4077
|
+
const viewName = strName7(view.name) ?? strName7(view.object) ?? `#${vi}`;
|
|
3514
4078
|
const checkListContainer = (container, label2, path) => {
|
|
3515
4079
|
if (!container || typeof container !== "object") return;
|
|
3516
4080
|
const list3 = container;
|
|
3517
4081
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
3518
4082
|
const names = strList(list3[key]);
|
|
3519
4083
|
for (let ai = 0; ai < names.length; ai++) {
|
|
3520
|
-
check(
|
|
3521
|
-
names[ai],
|
|
3522
|
-
`view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
|
|
3523
|
-
`${path}.${key}[${ai}]`,
|
|
3524
|
-
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
3525
|
-
);
|
|
3526
|
-
}
|
|
3527
|
-
}
|
|
3528
|
-
};
|
|
3529
|
-
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
3530
|
-
const listViews = view.listViews;
|
|
3531
|
-
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
3532
|
-
for (const [key, lv] of Object.entries(listViews)) {
|
|
3533
|
-
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
}
|
|
3537
|
-
const pages = asArray23(stack.pages);
|
|
3538
|
-
for (let pi = 0; pi < pages.length; pi++) {
|
|
3539
|
-
const page = pages[pi];
|
|
3540
|
-
if (!page || typeof page !== "object") continue;
|
|
3541
|
-
const pageName = strName4(page.name) ?? `#${pi}`;
|
|
3542
|
-
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3543
|
-
const props = component.properties;
|
|
3544
|
-
if (!props || typeof props !== "object") continue;
|
|
3545
|
-
const names = strList(props.actionNames);
|
|
3546
|
-
for (let ai = 0; ai < names.length; ai++) {
|
|
3547
|
-
check(
|
|
3548
|
-
names[ai],
|
|
3549
|
-
`page "${pageName}" \xB7 component "${strName4(component.type) ?? "?"}"`,
|
|
3550
|
-
`${path}.properties.actionNames[${ai}]`,
|
|
3551
|
-
"Quick-actions bar"
|
|
3552
|
-
);
|
|
3553
|
-
}
|
|
3554
|
-
}
|
|
3555
|
-
}
|
|
3556
|
-
const apps = asArray23(stack.apps);
|
|
3557
|
-
for (let ai = 0; ai < apps.length; ai++) {
|
|
3558
|
-
const app = apps[ai];
|
|
3559
|
-
if (!app || typeof app !== "object") continue;
|
|
3560
|
-
const appName = strName4(app.name) ?? `#${ai}`;
|
|
3561
|
-
const walkNav = (items, basePath) => {
|
|
3562
|
-
const navItems = asArray23(items);
|
|
3563
|
-
for (let ni = 0; ni < navItems.length; ni++) {
|
|
3564
|
-
const nav = navItems[ni];
|
|
3565
|
-
if (!nav || typeof nav !== "object") continue;
|
|
3566
|
-
const navPath = `${basePath}[${ni}]`;
|
|
3567
|
-
const actionDef = nav.actionDef;
|
|
3568
|
-
const actionName = strName4(actionDef?.actionName);
|
|
3569
|
-
if (nav.type === "action" && actionName) {
|
|
3570
|
-
check(
|
|
3571
|
-
actionName,
|
|
3572
|
-
`app "${appName}" \xB7 nav "${strName4(nav.id) ?? `#${ni}`}"`,
|
|
3573
|
-
`${navPath}.actionDef.actionName`,
|
|
3574
|
-
"Navigation action item"
|
|
3575
|
-
);
|
|
3576
|
-
}
|
|
3577
|
-
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
3578
|
-
}
|
|
3579
|
-
};
|
|
3580
|
-
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
3581
|
-
const areas = asArray23(app.areas);
|
|
3582
|
-
for (let ri = 0; ri < areas.length; ri++) {
|
|
3583
|
-
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3586
|
-
return findings;
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
// src/validate-page-field-bindings.ts
|
|
3590
|
-
var PAGE_FIELD_UNKNOWN = "page-field-unknown";
|
|
3591
|
-
var SYSTEM_FIELDS4 = /* @__PURE__ */ new Set([
|
|
3592
|
-
"id",
|
|
3593
|
-
"created_at",
|
|
3594
|
-
"created_by",
|
|
3595
|
-
"updated_at",
|
|
3596
|
-
"updated_by",
|
|
3597
|
-
"owner_id",
|
|
3598
|
-
"organization_id",
|
|
3599
|
-
"tenant_id",
|
|
3600
|
-
"user_id",
|
|
3601
|
-
"deleted_at"
|
|
3602
|
-
]);
|
|
3603
|
-
function asArray24(v) {
|
|
3604
|
-
if (Array.isArray(v)) return v;
|
|
3605
|
-
if (v && typeof v === "object") {
|
|
3606
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3607
|
-
}
|
|
3608
|
-
return [];
|
|
3609
|
-
}
|
|
3610
|
-
function strName5(v) {
|
|
3611
|
-
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3612
|
-
}
|
|
3613
|
-
function isRec3(v) {
|
|
3614
|
-
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3615
|
-
}
|
|
3616
|
-
function fieldRefsFrom(value, basePath) {
|
|
3617
|
-
const out = [];
|
|
3618
|
-
const one = (v, path) => {
|
|
3619
|
-
const bare = strName5(v);
|
|
3620
|
-
if (bare) {
|
|
3621
|
-
out.push({ name: bare, path });
|
|
3622
|
-
return;
|
|
3623
|
-
}
|
|
3624
|
-
if (!isRec3(v)) return;
|
|
3625
|
-
const named = strName5(v.field) ?? strName5(v.name);
|
|
3626
|
-
if (named) out.push({ name: named, path: `${path}.${strName5(v.field) ? "field" : "name"}` });
|
|
3627
|
-
};
|
|
3628
|
-
if (Array.isArray(value)) {
|
|
3629
|
-
for (let i = 0; i < value.length; i++) one(value[i], `${basePath}[${i}]`);
|
|
3630
|
-
} else {
|
|
3631
|
-
one(value, basePath);
|
|
3632
|
-
}
|
|
3633
|
-
return out;
|
|
3634
|
-
}
|
|
3635
|
-
var COMPONENT_FIELD_SPECS = {
|
|
3636
|
-
"record:highlights": { props: ["fields"] },
|
|
3637
|
-
// `sections`/`hideFields` are not in RecordDetailsProps, but every real page
|
|
3638
|
-
// authors them (they survive because `properties` is unvalidated).
|
|
3639
|
-
"record:details": { props: ["fields", "hideFields"], nestedSections: ["sections"] },
|
|
3640
|
-
"record:path": { props: ["statusField"] },
|
|
3641
|
-
"element:number": { props: ["field"] },
|
|
3642
|
-
"element:filter": { props: ["fields"] },
|
|
3643
|
-
"element:form": { props: ["fields"] },
|
|
3644
|
-
// The schema says `displayField`; real pages author `labelField`. Accept both.
|
|
3645
|
-
"element:record_picker": { props: ["displayField", "labelField", "searchFields"] }
|
|
3646
|
-
};
|
|
3647
|
-
var RELATED_LIST_TYPE = "record:related_list";
|
|
3648
|
-
function validatePageFieldBindings(stack) {
|
|
3649
|
-
const findings = [];
|
|
3650
|
-
if (!stack || typeof stack !== "object") return findings;
|
|
3651
|
-
const objectFields = /* @__PURE__ */ new Map();
|
|
3652
|
-
for (const obj of asArray24(stack.objects)) {
|
|
3653
|
-
const name = strName5(obj.name);
|
|
3654
|
-
if (!name) continue;
|
|
3655
|
-
const names = /* @__PURE__ */ new Set();
|
|
3656
|
-
for (const f of asArray24(obj.fields)) {
|
|
3657
|
-
const fn = strName5(f.name);
|
|
3658
|
-
if (fn) names.add(fn);
|
|
3659
|
-
}
|
|
3660
|
-
objectFields.set(name, names);
|
|
3661
|
-
}
|
|
3662
|
-
const pages = asArray24(stack.pages);
|
|
3663
|
-
for (let pi = 0; pi < pages.length; pi++) {
|
|
3664
|
-
const page = pages[pi];
|
|
3665
|
-
if (!page || typeof page !== "object") continue;
|
|
3666
|
-
const pageName = strName5(page.name) ?? `#${pi}`;
|
|
3667
|
-
const checkRefs = (refs, objectName, where) => {
|
|
3668
|
-
if (!objectName) return;
|
|
3669
|
-
const known = objectFields.get(objectName);
|
|
3670
|
-
if (!known) return;
|
|
3671
|
-
for (const ref of refs) {
|
|
3672
|
-
if (ref.name.includes(".")) continue;
|
|
3673
|
-
if (known.has(ref.name) || SYSTEM_FIELDS4.has(ref.name)) continue;
|
|
3674
|
-
findings.push({
|
|
3675
|
-
severity: "warning",
|
|
3676
|
-
rule: PAGE_FIELD_UNKNOWN,
|
|
3677
|
-
where,
|
|
3678
|
-
path: ref.path,
|
|
3679
|
-
message: `field "${ref.name}" is not a field on object "${objectName}" \u2014 the component silently skips it, so it never renders.`,
|
|
3680
|
-
hint: `Fix the field name, or add "${ref.name}" to ${objectName}. References must match the object's field names exactly.` + (known.size > 0 ? ` Object fields: ${[...known].sort().join(", ")}.` : "")
|
|
3681
|
-
});
|
|
3682
|
-
}
|
|
3683
|
-
};
|
|
3684
|
-
for (const { component, path, objectName } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3685
|
-
const type = strName5(component.type);
|
|
3686
|
-
const props = isRec3(component.properties) ? component.properties : void 0;
|
|
3687
|
-
if (!type || !props) continue;
|
|
3688
|
-
const where = `page "${pageName}" \xB7 ${type}`;
|
|
3689
|
-
if (type === RELATED_LIST_TYPE) {
|
|
3690
|
-
const relatedObject = strName5(props.objectName);
|
|
3691
|
-
const relatedRefs = [
|
|
3692
|
-
...fieldRefsFrom(props.columns, `${path}.properties.columns`),
|
|
3693
|
-
...fieldRefsFrom(props.sort, `${path}.properties.sort`),
|
|
3694
|
-
...fieldRefsFrom(props.filter, `${path}.properties.filter`),
|
|
3695
|
-
...fieldRefsFrom(props.relationshipField, `${path}.properties.relationshipField`)
|
|
3696
|
-
];
|
|
3697
|
-
checkRefs(relatedRefs, relatedObject, where);
|
|
3698
|
-
checkRefs(
|
|
3699
|
-
fieldRefsFrom(props.relationshipValueField, `${path}.properties.relationshipValueField`),
|
|
3700
|
-
objectName,
|
|
3701
|
-
where
|
|
3702
|
-
);
|
|
3703
|
-
const add = isRec3(props.add) ? props.add : void 0;
|
|
3704
|
-
const picker = add && isRec3(add.picker) ? add.picker : void 0;
|
|
3705
|
-
if (picker) {
|
|
3706
|
-
checkRefs(
|
|
3707
|
-
[
|
|
3708
|
-
...fieldRefsFrom(picker.valueField, `${path}.properties.add.picker.valueField`),
|
|
3709
|
-
...fieldRefsFrom(picker.labelField, `${path}.properties.add.picker.labelField`)
|
|
3710
|
-
],
|
|
3711
|
-
strName5(picker.object),
|
|
3712
|
-
where
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
if (add) {
|
|
3716
|
-
checkRefs(
|
|
3717
|
-
fieldRefsFrom(add.linkField, `${path}.properties.add.linkField`),
|
|
3718
|
-
relatedObject,
|
|
3719
|
-
where
|
|
4084
|
+
check(
|
|
4085
|
+
names[ai],
|
|
4086
|
+
`view "${viewName}" \xB7 ${label2} \xB7 ${key}`,
|
|
4087
|
+
`${path}.${key}[${ai}]`,
|
|
4088
|
+
key === "bulkActions" ? "Bulk-action menu" : "Row-action menu"
|
|
3720
4089
|
);
|
|
3721
4090
|
}
|
|
3722
|
-
continue;
|
|
3723
4091
|
}
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
4092
|
+
};
|
|
4093
|
+
checkListContainer(view.list, "list", `views[${vi}].list`);
|
|
4094
|
+
const listViews = view.listViews;
|
|
4095
|
+
if (listViews && typeof listViews === "object" && !Array.isArray(listViews)) {
|
|
4096
|
+
for (const [key, lv] of Object.entries(listViews)) {
|
|
4097
|
+
checkListContainer(lv, `listViews.${key}`, `views[${vi}].listViews.${key}`);
|
|
3729
4098
|
}
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
const pages = asArray25(stack.pages);
|
|
4102
|
+
for (let pi = 0; pi < pages.length; pi++) {
|
|
4103
|
+
const page = pages[pi];
|
|
4104
|
+
if (!page || typeof page !== "object") continue;
|
|
4105
|
+
const pageName = strName7(page.name) ?? `#${pi}`;
|
|
4106
|
+
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
4107
|
+
const props = component.properties;
|
|
4108
|
+
if (!props || typeof props !== "object") continue;
|
|
4109
|
+
const names = strList(props.actionNames);
|
|
4110
|
+
for (let ai = 0; ai < names.length; ai++) {
|
|
4111
|
+
check(
|
|
4112
|
+
names[ai],
|
|
4113
|
+
`page "${pageName}" \xB7 component "${strName7(component.type) ?? "?"}"`,
|
|
4114
|
+
`${path}.properties.actionNames[${ai}]`,
|
|
4115
|
+
"Quick-actions bar"
|
|
4116
|
+
);
|
|
3739
4117
|
}
|
|
3740
|
-
checkRefs(refs, objectName, where);
|
|
3741
4118
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
4119
|
+
}
|
|
4120
|
+
const apps = asArray25(stack.apps);
|
|
4121
|
+
for (let ai = 0; ai < apps.length; ai++) {
|
|
4122
|
+
const app = apps[ai];
|
|
4123
|
+
if (!app || typeof app !== "object") continue;
|
|
4124
|
+
const appName = strName7(app.name) ?? `#${ai}`;
|
|
4125
|
+
const walkNav = (items, basePath) => {
|
|
4126
|
+
const navItems = asArray25(items);
|
|
4127
|
+
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4128
|
+
const nav = navItems[ni];
|
|
4129
|
+
if (!nav || typeof nav !== "object") continue;
|
|
4130
|
+
const navPath = `${basePath}[${ni}]`;
|
|
4131
|
+
const actionDef = nav.actionDef;
|
|
4132
|
+
const actionName = strName7(actionDef?.actionName);
|
|
4133
|
+
if (nav.type === "action" && actionName) {
|
|
4134
|
+
check(
|
|
4135
|
+
actionName,
|
|
4136
|
+
`app "${appName}" \xB7 nav "${strName7(nav.id) ?? `#${ni}`}"`,
|
|
4137
|
+
`${navPath}.actionDef.actionName`,
|
|
4138
|
+
"Navigation action item"
|
|
4139
|
+
);
|
|
4140
|
+
}
|
|
4141
|
+
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
3754
4142
|
}
|
|
3755
|
-
|
|
4143
|
+
};
|
|
4144
|
+
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4145
|
+
const areas = asArray25(app.areas);
|
|
4146
|
+
for (let ri = 0; ri < areas.length; ri++) {
|
|
4147
|
+
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
3756
4148
|
}
|
|
3757
4149
|
}
|
|
3758
4150
|
return findings;
|
|
@@ -3763,23 +4155,23 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
|
3763
4155
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
3764
4156
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
3765
4157
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
3766
|
-
function
|
|
4158
|
+
function asArray26(v) {
|
|
3767
4159
|
if (Array.isArray(v)) return v;
|
|
3768
4160
|
if (v && typeof v === "object") {
|
|
3769
4161
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3770
4162
|
}
|
|
3771
4163
|
return [];
|
|
3772
4164
|
}
|
|
3773
|
-
function
|
|
4165
|
+
function strName8(v) {
|
|
3774
4166
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3775
4167
|
}
|
|
3776
4168
|
function strList2(v) {
|
|
3777
4169
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
3778
4170
|
}
|
|
3779
|
-
function
|
|
4171
|
+
function isRec6(v) {
|
|
3780
4172
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3781
4173
|
}
|
|
3782
|
-
function
|
|
4174
|
+
function distance4(a, b) {
|
|
3783
4175
|
const m = a.length;
|
|
3784
4176
|
const n = b.length;
|
|
3785
4177
|
if (m === 0) return n;
|
|
@@ -3795,11 +4187,11 @@ function distance3(a, b) {
|
|
|
3795
4187
|
}
|
|
3796
4188
|
return prev[n];
|
|
3797
4189
|
}
|
|
3798
|
-
function
|
|
4190
|
+
function suggest5(target, known) {
|
|
3799
4191
|
let best;
|
|
3800
4192
|
let bestScore = Infinity;
|
|
3801
4193
|
for (const c of known) {
|
|
3802
|
-
const d =
|
|
4194
|
+
const d = distance4(target, c);
|
|
3803
4195
|
if (d < bestScore) {
|
|
3804
4196
|
bestScore = d;
|
|
3805
4197
|
best = c;
|
|
@@ -3814,17 +4206,17 @@ function list2(names) {
|
|
|
3814
4206
|
}
|
|
3815
4207
|
function indexDatasets(stack) {
|
|
3816
4208
|
const out = /* @__PURE__ */ new Map();
|
|
3817
|
-
for (const ds of
|
|
3818
|
-
const name =
|
|
4209
|
+
for (const ds of asArray26(stack.datasets)) {
|
|
4210
|
+
const name = strName8(ds.name);
|
|
3819
4211
|
if (!name) continue;
|
|
3820
4212
|
const dimensions = /* @__PURE__ */ new Set();
|
|
3821
|
-
for (const d of
|
|
3822
|
-
const n =
|
|
4213
|
+
for (const d of asArray26(ds.dimensions)) {
|
|
4214
|
+
const n = strName8(d.name);
|
|
3823
4215
|
if (n) dimensions.add(n);
|
|
3824
4216
|
}
|
|
3825
4217
|
const measures = /* @__PURE__ */ new Set();
|
|
3826
|
-
for (const m of
|
|
3827
|
-
const n =
|
|
4218
|
+
for (const m of asArray26(ds.measures)) {
|
|
4219
|
+
const n = strName8(m.name);
|
|
3828
4220
|
if (n) measures.add(n);
|
|
3829
4221
|
}
|
|
3830
4222
|
out.set(name, { dimensions, measures });
|
|
@@ -3847,7 +4239,7 @@ function validateChartBindings(stack) {
|
|
|
3847
4239
|
where: binding.where,
|
|
3848
4240
|
path: `${binding.path}.dataset`,
|
|
3849
4241
|
message: `binds dataset "${dsName}", which resolves to no declared dataset \u2014 the chart has no data to render.`,
|
|
3850
|
-
hint: `Declared datasets: ${list2(datasets.keys())}.${
|
|
4242
|
+
hint: `Declared datasets: ${list2(datasets.keys())}.${suggest5(dsName, datasets.keys())} Define it with defineDataset() or fix the reference (ADR-0021).`
|
|
3851
4243
|
});
|
|
3852
4244
|
return;
|
|
3853
4245
|
}
|
|
@@ -3859,7 +4251,7 @@ function validateChartBindings(stack) {
|
|
|
3859
4251
|
where: binding.where,
|
|
3860
4252
|
path,
|
|
3861
4253
|
message: `"${name}" is not a dimension declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by DIMENSION NAME, not the base field, so this axis renders with no categories.`,
|
|
3862
|
-
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${
|
|
4254
|
+
hint: `Dataset dimensions: ${list2(ds.dimensions)}.${suggest5(name, ds.dimensions)} Declare the dimension on the dataset, or bind an existing one.`
|
|
3863
4255
|
});
|
|
3864
4256
|
};
|
|
3865
4257
|
const measureRef = (name, path, selected2) => {
|
|
@@ -3870,7 +4262,7 @@ function validateChartBindings(stack) {
|
|
|
3870
4262
|
where: binding.where,
|
|
3871
4263
|
path,
|
|
3872
4264
|
message: `"${name}" is not a measure declared by dataset "${dsName}". Post-ADR-0021 result rows are keyed by MEASURE NAME (e.g. "sum_amount"), not the base field (e.g. "amount"), so this series comes back empty.`,
|
|
3873
|
-
hint: `Dataset measures: ${list2(ds.measures)}.${
|
|
4265
|
+
hint: `Dataset measures: ${list2(ds.measures)}.${suggest5(name, ds.measures)} Declare the measure on the dataset, or bind an existing one.`
|
|
3874
4266
|
});
|
|
3875
4267
|
return;
|
|
3876
4268
|
}
|
|
@@ -3902,29 +4294,29 @@ function validateChartBindings(stack) {
|
|
|
3902
4294
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
3903
4295
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
3904
4296
|
};
|
|
3905
|
-
const reports =
|
|
4297
|
+
const reports = asArray26(stack.reports);
|
|
3906
4298
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
3907
4299
|
const report = reports[ri];
|
|
3908
|
-
if (!
|
|
3909
|
-
const reportName =
|
|
4300
|
+
if (!isRec6(report)) continue;
|
|
4301
|
+
const reportName = strName8(report.name) ?? `#${ri}`;
|
|
3910
4302
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
3911
|
-
if (!
|
|
4303
|
+
if (!isRec6(chart)) return;
|
|
3912
4304
|
check({
|
|
3913
4305
|
dataset,
|
|
3914
4306
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
3915
4307
|
// it in lets the yAxis "declared but not selected" check work without
|
|
3916
4308
|
// reporting the selection itself twice.
|
|
3917
4309
|
values: { names: values, path: `${path}.values` },
|
|
3918
|
-
xAxis:
|
|
3919
|
-
yAxis:
|
|
3920
|
-
series:
|
|
4310
|
+
xAxis: strName8(chart.xAxis) ? { name: strName8(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
4311
|
+
yAxis: strName8(chart.yAxis) ? { name: strName8(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
4312
|
+
series: asArray26(chart.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
3921
4313
|
where,
|
|
3922
4314
|
path: `${path}.chart`
|
|
3923
4315
|
});
|
|
3924
4316
|
};
|
|
3925
4317
|
checkReportChart(
|
|
3926
4318
|
report.chart,
|
|
3927
|
-
|
|
4319
|
+
strName8(report.dataset),
|
|
3928
4320
|
strList2(report.values),
|
|
3929
4321
|
`report "${reportName}" \xB7 chart`,
|
|
3930
4322
|
`reports[${ri}]`
|
|
@@ -3932,45 +4324,45 @@ function validateChartBindings(stack) {
|
|
|
3932
4324
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
3933
4325
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
3934
4326
|
const block = blocks[bi];
|
|
3935
|
-
if (!
|
|
4327
|
+
if (!isRec6(block)) continue;
|
|
3936
4328
|
checkReportChart(
|
|
3937
4329
|
block.chart,
|
|
3938
|
-
|
|
4330
|
+
strName8(block.dataset),
|
|
3939
4331
|
strList2(block.values),
|
|
3940
|
-
`report "${reportName}" \xB7 block "${
|
|
4332
|
+
`report "${reportName}" \xB7 block "${strName8(block.name) ?? `#${bi}`}" chart`,
|
|
3941
4333
|
`reports[${ri}].blocks[${bi}]`
|
|
3942
4334
|
);
|
|
3943
4335
|
}
|
|
3944
4336
|
}
|
|
3945
4337
|
const checkListChart = (container, where, path) => {
|
|
3946
|
-
if (!
|
|
4338
|
+
if (!isRec6(container)) return;
|
|
3947
4339
|
const chart = container.chart;
|
|
3948
|
-
if (!
|
|
4340
|
+
if (!isRec6(chart)) return;
|
|
3949
4341
|
check({
|
|
3950
|
-
dataset:
|
|
4342
|
+
dataset: strName8(chart.dataset),
|
|
3951
4343
|
dimensions: { names: strList2(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
3952
4344
|
values: { names: strList2(chart.values), path: `${path}.chart.values` },
|
|
3953
4345
|
where,
|
|
3954
4346
|
path: `${path}.chart`
|
|
3955
4347
|
});
|
|
3956
4348
|
};
|
|
3957
|
-
const views =
|
|
4349
|
+
const views = asArray26(stack.views);
|
|
3958
4350
|
for (let vi = 0; vi < views.length; vi++) {
|
|
3959
4351
|
const view = views[vi];
|
|
3960
|
-
if (!
|
|
3961
|
-
const viewName =
|
|
4352
|
+
if (!isRec6(view)) continue;
|
|
4353
|
+
const viewName = strName8(view.name) ?? strName8(view.objectName) ?? `#${vi}`;
|
|
3962
4354
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
3963
|
-
if (
|
|
4355
|
+
if (isRec6(view.listViews)) {
|
|
3964
4356
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
3965
4357
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
3966
4358
|
}
|
|
3967
4359
|
}
|
|
3968
4360
|
}
|
|
3969
|
-
const objects =
|
|
4361
|
+
const objects = asArray26(stack.objects);
|
|
3970
4362
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
3971
4363
|
const obj = objects[oi];
|
|
3972
|
-
if (!
|
|
3973
|
-
const objName =
|
|
4364
|
+
if (!isRec6(obj) || !isRec6(obj.listViews)) continue;
|
|
4365
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
3974
4366
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
3975
4367
|
checkListChart(
|
|
3976
4368
|
lv,
|
|
@@ -3979,22 +4371,22 @@ function validateChartBindings(stack) {
|
|
|
3979
4371
|
);
|
|
3980
4372
|
}
|
|
3981
4373
|
}
|
|
3982
|
-
const pages =
|
|
4374
|
+
const pages = asArray26(stack.pages);
|
|
3983
4375
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
3984
4376
|
const page = pages[pi];
|
|
3985
|
-
if (!
|
|
3986
|
-
const pageName =
|
|
4377
|
+
if (!isRec6(page)) continue;
|
|
4378
|
+
const pageName = strName8(page.name) ?? `#${pi}`;
|
|
3987
4379
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
3988
|
-
const props =
|
|
3989
|
-
if (!props || !
|
|
3990
|
-
const axisRefs =
|
|
3991
|
-
const seriesRefs =
|
|
4380
|
+
const props = isRec6(component.properties) ? component.properties : void 0;
|
|
4381
|
+
if (!props || !strName8(props.dataset)) continue;
|
|
4382
|
+
const axisRefs = asArray26(props.yAxis).map((a, ai) => ({ name: strName8(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
4383
|
+
const seriesRefs = asArray26(props.series).map((s, si) => ({ name: strName8(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
3992
4384
|
check({
|
|
3993
|
-
dataset:
|
|
4385
|
+
dataset: strName8(props.dataset),
|
|
3994
4386
|
dimensions: { names: strList2(props.dimensions), path: `${path}.properties.dimensions` },
|
|
3995
4387
|
values: { names: strList2(props.values), path: `${path}.properties.values` },
|
|
3996
4388
|
series: [...axisRefs, ...seriesRefs],
|
|
3997
|
-
where: `page "${pageName}" \xB7 ${
|
|
4389
|
+
where: `page "${pageName}" \xB7 ${strName8(component.type) ?? "chart"}`,
|
|
3998
4390
|
path: `${path}.properties`
|
|
3999
4391
|
});
|
|
4000
4392
|
}
|
|
@@ -4006,7 +4398,7 @@ function validateChartBindings(stack) {
|
|
|
4006
4398
|
import { isPlatformProvidedObjectName as isPlatformProvidedObjectName2 } from "@objectstack/spec/system";
|
|
4007
4399
|
|
|
4008
4400
|
// src/build-access-matrix.ts
|
|
4009
|
-
function
|
|
4401
|
+
function asArray27(v) {
|
|
4010
4402
|
if (Array.isArray(v)) return v;
|
|
4011
4403
|
if (v && typeof v === "object") {
|
|
4012
4404
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4017,13 +4409,13 @@ function buildAccessMatrix(stack) {
|
|
|
4017
4409
|
const entries = [];
|
|
4018
4410
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
4019
4411
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
4020
|
-
for (const obj of
|
|
4412
|
+
for (const obj of asArray27(stack.objects)) {
|
|
4021
4413
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
4022
4414
|
if (!name) continue;
|
|
4023
4415
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
4024
4416
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
4025
4417
|
}
|
|
4026
|
-
for (const ps of
|
|
4418
|
+
for (const ps of asArray27(stack.permissions)) {
|
|
4027
4419
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
4028
4420
|
if (!psName) continue;
|
|
4029
4421
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -4096,34 +4488,34 @@ function diffAccessMatrix(before, after) {
|
|
|
4096
4488
|
|
|
4097
4489
|
// src/validate-nav-access.ts
|
|
4098
4490
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
4099
|
-
function
|
|
4491
|
+
function asArray28(v) {
|
|
4100
4492
|
if (Array.isArray(v)) return v;
|
|
4101
4493
|
if (v && typeof v === "object") {
|
|
4102
4494
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4103
4495
|
}
|
|
4104
4496
|
return [];
|
|
4105
4497
|
}
|
|
4106
|
-
function
|
|
4498
|
+
function strName9(v) {
|
|
4107
4499
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4108
4500
|
}
|
|
4109
4501
|
function collectNavExposures(stack) {
|
|
4110
4502
|
const out = [];
|
|
4111
|
-
const apps =
|
|
4503
|
+
const apps = asArray28(stack.apps);
|
|
4112
4504
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4113
4505
|
const app = apps[ai];
|
|
4114
4506
|
if (!app || typeof app !== "object") continue;
|
|
4115
|
-
const appName =
|
|
4507
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
4116
4508
|
const walk = (items, basePath) => {
|
|
4117
|
-
const navItems =
|
|
4509
|
+
const navItems = asArray28(items);
|
|
4118
4510
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4119
4511
|
const nav = navItems[ni];
|
|
4120
4512
|
if (!nav || typeof nav !== "object") continue;
|
|
4121
4513
|
const navPath = `${basePath}[${ni}]`;
|
|
4122
|
-
const objectName =
|
|
4514
|
+
const objectName = strName9(nav.objectName);
|
|
4123
4515
|
if (nav.type === "object" && objectName) {
|
|
4124
4516
|
out.push({
|
|
4125
4517
|
objectName,
|
|
4126
|
-
where: `app "${appName}" \xB7 nav "${
|
|
4518
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? `#${ni}`}"`,
|
|
4127
4519
|
path: `${navPath}.objectName`
|
|
4128
4520
|
});
|
|
4129
4521
|
}
|
|
@@ -4131,7 +4523,7 @@ function collectNavExposures(stack) {
|
|
|
4131
4523
|
}
|
|
4132
4524
|
};
|
|
4133
4525
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
4134
|
-
const areas =
|
|
4526
|
+
const areas = asArray28(app.areas);
|
|
4135
4527
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4136
4528
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4137
4529
|
}
|
|
@@ -4141,13 +4533,13 @@ function collectNavExposures(stack) {
|
|
|
4141
4533
|
function validateNavAccess(stack) {
|
|
4142
4534
|
const findings = [];
|
|
4143
4535
|
if (!stack || typeof stack !== "object") return findings;
|
|
4144
|
-
const permissionSets =
|
|
4536
|
+
const permissionSets = asArray28(stack.permissions);
|
|
4145
4537
|
if (permissionSets.length === 0) return findings;
|
|
4146
4538
|
const exposures = collectNavExposures(stack);
|
|
4147
4539
|
if (exposures.length === 0) return findings;
|
|
4148
4540
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4149
|
-
for (const obj of
|
|
4150
|
-
const n =
|
|
4541
|
+
for (const obj of asArray28(stack.objects)) {
|
|
4542
|
+
const n = strName9(obj.name);
|
|
4151
4543
|
if (n) ownObjects.add(n);
|
|
4152
4544
|
}
|
|
4153
4545
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -4179,18 +4571,18 @@ function validateNavAccess(stack) {
|
|
|
4179
4571
|
import { hasPlatformObjectPrefix as hasPlatformObjectPrefix2, isPlatformProvidedObjectName as isPlatformProvidedObjectName3 } from "@objectstack/spec/system";
|
|
4180
4572
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
4181
4573
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
4182
|
-
function
|
|
4574
|
+
function isRec7(v) {
|
|
4183
4575
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4184
4576
|
}
|
|
4185
|
-
function
|
|
4577
|
+
function asArray29(v) {
|
|
4186
4578
|
if (Array.isArray(v)) return v;
|
|
4187
|
-
if (
|
|
4579
|
+
if (isRec7(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec7(def) ? def : {} }));
|
|
4188
4580
|
return [];
|
|
4189
4581
|
}
|
|
4190
|
-
function
|
|
4582
|
+
function strName10(v) {
|
|
4191
4583
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4192
4584
|
}
|
|
4193
|
-
function
|
|
4585
|
+
function distance5(a, b) {
|
|
4194
4586
|
const m = a.length;
|
|
4195
4587
|
const n = b.length;
|
|
4196
4588
|
if (m === 0) return n;
|
|
@@ -4206,7 +4598,7 @@ function distance4(a, b) {
|
|
|
4206
4598
|
}
|
|
4207
4599
|
return prev[n];
|
|
4208
4600
|
}
|
|
4209
|
-
function
|
|
4601
|
+
function suggest6(target, known) {
|
|
4210
4602
|
const names = [...known];
|
|
4211
4603
|
const segmentMatch = names.find(
|
|
4212
4604
|
(candidate) => candidate.endsWith(`_${target}`) || candidate.startsWith(`${target}_`)
|
|
@@ -4215,7 +4607,7 @@ function suggest5(target, known) {
|
|
|
4215
4607
|
let best;
|
|
4216
4608
|
let bestScore = Infinity;
|
|
4217
4609
|
for (const candidate of names) {
|
|
4218
|
-
const d =
|
|
4610
|
+
const d = distance5(target, candidate);
|
|
4219
4611
|
if (d < bestScore) {
|
|
4220
4612
|
bestScore = d;
|
|
4221
4613
|
best = candidate;
|
|
@@ -4230,20 +4622,10 @@ function listNames(names, max = 12) {
|
|
|
4230
4622
|
const shown = all.slice(0, max).join(", ");
|
|
4231
4623
|
return all.length > max ? `${shown}, \u2026 (${all.length} total)` : shown;
|
|
4232
4624
|
}
|
|
4233
|
-
var
|
|
4234
|
-
|
|
4625
|
+
var IMPLICIT_FIELDS = /* @__PURE__ */ new Set([
|
|
4626
|
+
...SYSTEM_FIELDS,
|
|
4235
4627
|
"_id",
|
|
4236
4628
|
"name",
|
|
4237
|
-
"created_at",
|
|
4238
|
-
"created_by",
|
|
4239
|
-
"updated_at",
|
|
4240
|
-
"updated_by",
|
|
4241
|
-
"owner_id",
|
|
4242
|
-
"organization_id",
|
|
4243
|
-
"tenant_id",
|
|
4244
|
-
"user_id",
|
|
4245
|
-
"is_deleted",
|
|
4246
|
-
"deleted_at",
|
|
4247
4629
|
"space"
|
|
4248
4630
|
]);
|
|
4249
4631
|
function emptyFacts() {
|
|
@@ -4255,20 +4637,20 @@ function collectViewRecord(view, factsFor) {
|
|
|
4255
4637
|
const addView = (objectName, name) => {
|
|
4256
4638
|
if (objectName && name) factsFor(objectName).views.add(name);
|
|
4257
4639
|
};
|
|
4258
|
-
const listBinding =
|
|
4259
|
-
if (
|
|
4260
|
-
addView(recordObject ?? listBinding,
|
|
4640
|
+
const listBinding = isRec7(view.list) ? bindingOf(view.list) : void 0;
|
|
4641
|
+
if (isRec7(view.list)) addView(listBinding, strName10(view.list.name));
|
|
4642
|
+
addView(recordObject ?? listBinding, strName10(view.name));
|
|
4261
4643
|
for (const key of ["listViews", "formViews"]) {
|
|
4262
4644
|
const container = view[key];
|
|
4263
|
-
if (!
|
|
4645
|
+
if (!isRec7(container)) continue;
|
|
4264
4646
|
for (const [subKey, sub] of Object.entries(container)) {
|
|
4265
|
-
if (!
|
|
4647
|
+
if (!isRec7(sub)) continue;
|
|
4266
4648
|
const binding = bindingOf(sub) ?? listBinding;
|
|
4267
4649
|
addView(binding, subKey);
|
|
4268
|
-
addView(binding,
|
|
4650
|
+
addView(binding, strName10(sub.name));
|
|
4269
4651
|
if (binding) {
|
|
4270
|
-
for (const section of
|
|
4271
|
-
const sectionName =
|
|
4652
|
+
for (const section of asArray29(sub.sections)) {
|
|
4653
|
+
const sectionName = strName10(section.name);
|
|
4272
4654
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
4273
4655
|
}
|
|
4274
4656
|
}
|
|
@@ -4276,14 +4658,14 @@ function collectViewRecord(view, factsFor) {
|
|
|
4276
4658
|
}
|
|
4277
4659
|
const sectionBinding = recordObject ?? listBinding;
|
|
4278
4660
|
if (sectionBinding) {
|
|
4279
|
-
for (const section of
|
|
4280
|
-
const sectionName =
|
|
4661
|
+
for (const section of asArray29(view.sections)) {
|
|
4662
|
+
const sectionName = strName10(section.name);
|
|
4281
4663
|
if (sectionName) factsFor(sectionBinding).sections.add(sectionName);
|
|
4282
4664
|
}
|
|
4283
4665
|
}
|
|
4284
4666
|
}
|
|
4285
4667
|
function viewObjectName(view) {
|
|
4286
|
-
return
|
|
4668
|
+
return strName10(view.objectName) ?? strName10(view.object) ?? (isRec7(view.data) ? strName10(view.data.object) : void 0);
|
|
4287
4669
|
}
|
|
4288
4670
|
function readOptions(field) {
|
|
4289
4671
|
const raw = field.options;
|
|
@@ -4295,14 +4677,14 @@ function readOptions(field) {
|
|
|
4295
4677
|
values.add(opt);
|
|
4296
4678
|
continue;
|
|
4297
4679
|
}
|
|
4298
|
-
if (!
|
|
4299
|
-
const value =
|
|
4680
|
+
if (!isRec7(opt)) continue;
|
|
4681
|
+
const value = strName10(opt.value);
|
|
4300
4682
|
if (!value) continue;
|
|
4301
4683
|
values.add(value);
|
|
4302
|
-
const label2 =
|
|
4684
|
+
const label2 = strName10(opt.label);
|
|
4303
4685
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
4304
4686
|
}
|
|
4305
|
-
} else if (
|
|
4687
|
+
} else if (isRec7(raw)) {
|
|
4306
4688
|
for (const [value, label2] of Object.entries(raw)) {
|
|
4307
4689
|
values.add(value);
|
|
4308
4690
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -4322,48 +4704,48 @@ function buildUniverse(stack) {
|
|
|
4322
4704
|
}
|
|
4323
4705
|
return facts;
|
|
4324
4706
|
};
|
|
4325
|
-
for (const obj of
|
|
4326
|
-
const objectName =
|
|
4707
|
+
for (const obj of asArray29(stack.objects)) {
|
|
4708
|
+
const objectName = strName10(obj.name);
|
|
4327
4709
|
if (!objectName) continue;
|
|
4328
4710
|
const facts = factsFor(objectName);
|
|
4329
|
-
for (const field of
|
|
4330
|
-
const fieldName =
|
|
4711
|
+
for (const field of asArray29(obj.fields)) {
|
|
4712
|
+
const fieldName = strName10(field.name);
|
|
4331
4713
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
4332
4714
|
}
|
|
4333
|
-
for (const action of
|
|
4334
|
-
const actionName =
|
|
4715
|
+
for (const action of asArray29(obj.actions)) {
|
|
4716
|
+
const actionName = strName10(action.name);
|
|
4335
4717
|
if (actionName) facts.actions.set(actionName, action);
|
|
4336
4718
|
}
|
|
4337
|
-
for (const view of
|
|
4338
|
-
collectViewRecord({ ...view, object:
|
|
4719
|
+
for (const view of asArray29(obj.views)) {
|
|
4720
|
+
collectViewRecord({ ...view, object: strName10(view.object) ?? objectName }, factsFor);
|
|
4339
4721
|
}
|
|
4340
4722
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
4341
|
-
for (const group of
|
|
4342
|
-
const key =
|
|
4723
|
+
for (const group of asArray29(obj.fieldGroups)) {
|
|
4724
|
+
const key = strName10(group.key) ?? strName10(group.name);
|
|
4343
4725
|
if (key) facts.sections.add(key);
|
|
4344
4726
|
}
|
|
4345
4727
|
}
|
|
4346
|
-
for (const view of
|
|
4728
|
+
for (const view of asArray29(stack.views)) {
|
|
4347
4729
|
collectViewRecord(view, factsFor);
|
|
4348
4730
|
}
|
|
4349
|
-
const pages =
|
|
4731
|
+
const pages = asArray29(stack.pages);
|
|
4350
4732
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
4351
4733
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
4352
4734
|
if (!walked.objectName) continue;
|
|
4353
|
-
const props =
|
|
4735
|
+
const props = isRec7(walked.component.properties) ? walked.component.properties : void 0;
|
|
4354
4736
|
if (!props) continue;
|
|
4355
|
-
for (const section of
|
|
4356
|
-
const sectionName =
|
|
4737
|
+
for (const section of asArray29(props.sections)) {
|
|
4738
|
+
const sectionName = strName10(section.name);
|
|
4357
4739
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
4358
4740
|
}
|
|
4359
4741
|
}
|
|
4360
4742
|
}
|
|
4361
4743
|
const globalActions = /* @__PURE__ */ new Map();
|
|
4362
4744
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
4363
|
-
for (const action of
|
|
4364
|
-
const actionName =
|
|
4745
|
+
for (const action of asArray29(stack.actions)) {
|
|
4746
|
+
const actionName = strName10(action.name);
|
|
4365
4747
|
if (!actionName) continue;
|
|
4366
|
-
const owner =
|
|
4748
|
+
const owner = strName10(action.objectName) ?? strName10(action.object);
|
|
4367
4749
|
if (owner) {
|
|
4368
4750
|
factsFor(owner).actions.set(actionName, action);
|
|
4369
4751
|
actionOwners.set(actionName, owner);
|
|
@@ -4377,41 +4759,41 @@ function buildUniverse(stack) {
|
|
|
4377
4759
|
}
|
|
4378
4760
|
}
|
|
4379
4761
|
const apps = /* @__PURE__ */ new Map();
|
|
4380
|
-
for (const app of
|
|
4381
|
-
const appName =
|
|
4762
|
+
for (const app of asArray29(stack.apps)) {
|
|
4763
|
+
const appName = strName10(app.name);
|
|
4382
4764
|
if (!appName) continue;
|
|
4383
4765
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
4384
4766
|
const walkNav = (items) => {
|
|
4385
|
-
for (const item of
|
|
4386
|
-
const id =
|
|
4767
|
+
for (const item of asArray29(items)) {
|
|
4768
|
+
const id = strName10(item.id);
|
|
4387
4769
|
if (id) navIds.add(id);
|
|
4388
4770
|
if (item.children) walkNav(item.children);
|
|
4389
4771
|
}
|
|
4390
4772
|
};
|
|
4391
4773
|
walkNav(app.navigation);
|
|
4392
|
-
for (const area of
|
|
4393
|
-
const areaId =
|
|
4774
|
+
for (const area of asArray29(app.areas)) {
|
|
4775
|
+
const areaId = strName10(area.id);
|
|
4394
4776
|
if (areaId) navIds.add(areaId);
|
|
4395
4777
|
walkNav(area.navigation);
|
|
4396
4778
|
}
|
|
4397
4779
|
apps.set(appName, navIds);
|
|
4398
4780
|
}
|
|
4399
4781
|
const dashboards = /* @__PURE__ */ new Map();
|
|
4400
|
-
for (const dash of
|
|
4401
|
-
const dashName =
|
|
4782
|
+
for (const dash of asArray29(stack.dashboards)) {
|
|
4783
|
+
const dashName = strName10(dash.name);
|
|
4402
4784
|
if (!dashName) continue;
|
|
4403
4785
|
const widgets = /* @__PURE__ */ new Set();
|
|
4404
|
-
for (const widget of
|
|
4405
|
-
const id =
|
|
4786
|
+
for (const widget of asArray29(dash.widgets)) {
|
|
4787
|
+
const id = strName10(widget.id) ?? strName10(widget.name);
|
|
4406
4788
|
if (id) widgets.add(id);
|
|
4407
4789
|
}
|
|
4408
4790
|
const actions = /* @__PURE__ */ new Set();
|
|
4409
4791
|
const headerActions = [
|
|
4410
|
-
...
|
|
4411
|
-
...
|
|
4792
|
+
...asArray29(isRec7(dash.header) ? dash.header.actions : void 0),
|
|
4793
|
+
...asArray29(dash.actions)
|
|
4412
4794
|
];
|
|
4413
4795
|
for (const action of headerActions) {
|
|
4414
|
-
const key =
|
|
4796
|
+
const key = strName10(action.actionUrl) ?? strName10(action.url) ?? strName10(action.name);
|
|
4415
4797
|
if (key) actions.add(key);
|
|
4416
4798
|
}
|
|
4417
4799
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -4423,7 +4805,7 @@ function localePath(bundleIndex, locale) {
|
|
|
4423
4805
|
}
|
|
4424
4806
|
function validateTranslationReferences(stack) {
|
|
4425
4807
|
const findings = [];
|
|
4426
|
-
if (!
|
|
4808
|
+
if (!isRec7(stack)) return findings;
|
|
4427
4809
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
4428
4810
|
if (bundles.length === 0) return findings;
|
|
4429
4811
|
const universe = buildUniverse(stack);
|
|
@@ -4432,13 +4814,13 @@ function validateTranslationReferences(stack) {
|
|
|
4432
4814
|
};
|
|
4433
4815
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
4434
4816
|
const bundle = bundles[bi];
|
|
4435
|
-
if (!
|
|
4817
|
+
if (!isRec7(bundle)) continue;
|
|
4436
4818
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
4437
|
-
if (!
|
|
4819
|
+
if (!isRec7(rawData)) continue;
|
|
4438
4820
|
const base = localePath(bi, locale);
|
|
4439
4821
|
const inLocale = `locale "${locale}"`;
|
|
4440
4822
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
4441
|
-
if (!
|
|
4823
|
+
if (!isRec7(rawNode)) continue;
|
|
4442
4824
|
const objPath = `${base}.objects.${objectName}`;
|
|
4443
4825
|
const facts = universe.objects.get(objectName);
|
|
4444
4826
|
if (!facts) {
|
|
@@ -4446,7 +4828,7 @@ function validateTranslationReferences(stack) {
|
|
|
4446
4828
|
orphan(
|
|
4447
4829
|
`${inLocale} \xB7 object "${objectName}"`,
|
|
4448
4830
|
objPath,
|
|
4449
|
-
hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` +
|
|
4831
|
+
hasPlatformObjectPrefix2(objectName) ? `Translations are keyed to "${objectName}", which carries a platform namespace prefix but is registered by no platform package, official plugin, or cloud runtime object \u2014 and this stack does not define it either. Nothing resolves these keys.` + suggest6(objectName, universe.objects.keys()) : `Translations are keyed to "${objectName}", which no object in this stack defines. The resolver looks up keys derived from the metadata, so this whole subtree is dead weight \u2014 every label it carries renders untranslated.` + suggest6(objectName, universe.objects.keys()),
|
|
4450
4832
|
`Rename the key to the object it was written for, drop it, or ignore this if the object is contributed by another installed package.` + (universe.objects.size > 0 ? ` Defined objects: ${listNames(universe.objects.keys())}.` : "")
|
|
4451
4833
|
);
|
|
4452
4834
|
continue;
|
|
@@ -4455,16 +4837,16 @@ function validateTranslationReferences(stack) {
|
|
|
4455
4837
|
const fieldPath = `${objPath}.fields.${fieldName}`;
|
|
4456
4838
|
const field = facts.fields.get(fieldName);
|
|
4457
4839
|
if (!field) {
|
|
4458
|
-
if (
|
|
4840
|
+
if (IMPLICIT_FIELDS.has(fieldName)) continue;
|
|
4459
4841
|
orphan(
|
|
4460
4842
|
`${inLocale} \xB7 object "${objectName}" \xB7 field "${fieldName}"`,
|
|
4461
4843
|
fieldPath,
|
|
4462
|
-
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` +
|
|
4844
|
+
`Translations are keyed to field "${fieldName}", which object "${objectName}" does not declare. The label renders untranslated in this locale \u2014 and because every neighbouring field DOES resolve, the hole reads as a styling quirk rather than a missing translation.` + suggest6(fieldName, facts.fields.keys()),
|
|
4463
4845
|
`Point the key at a declared field, or drop it if the field was removed or renamed.` + (facts.fields.size > 0 ? ` Declared fields: ${listNames(facts.fields.keys())}.` : "")
|
|
4464
4846
|
);
|
|
4465
4847
|
continue;
|
|
4466
4848
|
}
|
|
4467
|
-
if (!
|
|
4849
|
+
if (!isRec7(rawField)) continue;
|
|
4468
4850
|
checkOptionKeys(findings, {
|
|
4469
4851
|
optionMap: rawField.options,
|
|
4470
4852
|
field,
|
|
@@ -4479,7 +4861,7 @@ function validateTranslationReferences(stack) {
|
|
|
4479
4861
|
orphan(
|
|
4480
4862
|
`${inLocale} \xB7 object "${objectName}" \xB7 view "${viewName}"`,
|
|
4481
4863
|
`${objPath}._views.${viewName}`,
|
|
4482
|
-
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` +
|
|
4864
|
+
`Translations are keyed to view "${viewName}", which no view of object "${objectName}" declares. The view tab keeps its source-locale label.` + suggest6(viewName, facts.views),
|
|
4483
4865
|
`Match the key to the view's \`name\` (not its label), or drop it.` + (facts.views.size > 0 ? ` Declared views: ${listNames(facts.views)}.` : "")
|
|
4484
4866
|
);
|
|
4485
4867
|
}
|
|
@@ -4488,7 +4870,7 @@ function validateTranslationReferences(stack) {
|
|
|
4488
4870
|
orphan(
|
|
4489
4871
|
`${inLocale} \xB7 object "${objectName}" \xB7 section "${sectionName}"`,
|
|
4490
4872
|
`${objPath}._sections.${sectionName}`,
|
|
4491
|
-
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` +
|
|
4873
|
+
`Translations are keyed to section "${sectionName}", which nothing on object "${objectName}" declares \u2014 no \`fieldGroups[].key\`, no named form-view section, no named \`record:details\` section. The section heading stays in the source locale.` + suggest6(sectionName, facts.sections),
|
|
4492
4874
|
`Sections are translatable only through a STABLE NAME: give the group/section a \`key\`/\`name\` and use it here, or drop the translation.` + (facts.sections.size > 0 ? ` Declared sections: ${listNames(facts.sections)}.` : ` Object "${objectName}" declares no named section at all.`)
|
|
4493
4875
|
);
|
|
4494
4876
|
}
|
|
@@ -4499,7 +4881,7 @@ function validateTranslationReferences(stack) {
|
|
|
4499
4881
|
orphan(
|
|
4500
4882
|
`${inLocale} \xB7 object "${objectName}" \xB7 action "${actionName}"`,
|
|
4501
4883
|
actionPath,
|
|
4502
|
-
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` +
|
|
4884
|
+
`Translations are keyed to action "${actionName}", which is defined by neither object "${objectName}"'s \`actions\` nor a \`stack.actions\` entry bound to it. The button keeps its source-locale label.` + suggest6(actionName, facts.actions.keys()),
|
|
4503
4885
|
`Match the key to a defined action name, move it under the object that owns the action, or drop it.` + (facts.actions.size > 0 ? ` Actions on this object: ${listNames(facts.actions.keys())}.` : "")
|
|
4504
4886
|
);
|
|
4505
4887
|
continue;
|
|
@@ -4521,7 +4903,7 @@ function validateTranslationReferences(stack) {
|
|
|
4521
4903
|
orphan(
|
|
4522
4904
|
`${inLocale} \xB7 global action "${actionName}"`,
|
|
4523
4905
|
actionPath,
|
|
4524
|
-
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` +
|
|
4906
|
+
owner ? `Action "${actionName}" is bound to object "${owner}", so the resolver looks it up under \`objects.${owner}._actions.${actionName}\` \u2014 never under \`globalActions\`, which is only consulted for object-less actions. This key is never read.` : `Translations are keyed to global action "${actionName}", which no object-less action in this stack defines. The button keeps its source-locale label.` + suggest6(actionName, universe.globalActions.keys()),
|
|
4525
4907
|
owner ? `Move these keys under \`objects.${owner}._actions.${actionName}\`.` : `Match the key to an object-less action's name, or drop it.` + (universe.globalActions.size > 0 ? ` Object-less actions: ${listNames(universe.globalActions.keys())}.` : "")
|
|
4526
4908
|
);
|
|
4527
4909
|
continue;
|
|
@@ -4541,18 +4923,18 @@ function validateTranslationReferences(stack) {
|
|
|
4541
4923
|
orphan(
|
|
4542
4924
|
`${inLocale} \xB7 app "${appName}"`,
|
|
4543
4925
|
appPath,
|
|
4544
|
-
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` +
|
|
4926
|
+
`Translations are keyed to app "${appName}", which this stack does not define. The app launcher shows the source-locale label.` + suggest6(appName, universe.apps.keys()),
|
|
4545
4927
|
`Match the key to an app's \`name\`, or drop it.` + (universe.apps.size > 0 ? ` Defined apps: ${listNames(universe.apps.keys())}.` : "")
|
|
4546
4928
|
);
|
|
4547
4929
|
continue;
|
|
4548
4930
|
}
|
|
4549
|
-
if (!
|
|
4931
|
+
if (!isRec7(rawApp)) continue;
|
|
4550
4932
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
4551
4933
|
if (navIds.has(navId)) continue;
|
|
4552
4934
|
orphan(
|
|
4553
4935
|
`${inLocale} \xB7 app "${appName}" \xB7 navigation "${navId}"`,
|
|
4554
4936
|
`${appPath}.navigation.${navId}`,
|
|
4555
|
-
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` +
|
|
4937
|
+
`Translations are keyed to navigation item "${navId}", which app "${appName}" does not declare. The menu entry keeps its source-locale label.` + suggest6(navId, navIds),
|
|
4556
4938
|
`Match the key to the navigation item's \`id\`, or drop it.` + (navIds.size > 0 ? ` Declared navigation ids: ${listNames(navIds)}.` : "")
|
|
4557
4939
|
);
|
|
4558
4940
|
}
|
|
@@ -4564,18 +4946,18 @@ function validateTranslationReferences(stack) {
|
|
|
4564
4946
|
orphan(
|
|
4565
4947
|
`${inLocale} \xB7 dashboard "${dashName}"`,
|
|
4566
4948
|
dashPath,
|
|
4567
|
-
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` +
|
|
4949
|
+
`Translations are keyed to dashboard "${dashName}", which this stack does not define. The dashboard title stays in the source locale.` + suggest6(dashName, universe.dashboards.keys()),
|
|
4568
4950
|
`Match the key to a dashboard's \`name\`, or drop it.` + (universe.dashboards.size > 0 ? ` Defined dashboards: ${listNames(universe.dashboards.keys())}.` : "")
|
|
4569
4951
|
);
|
|
4570
4952
|
continue;
|
|
4571
4953
|
}
|
|
4572
|
-
if (!
|
|
4954
|
+
if (!isRec7(rawDash)) continue;
|
|
4573
4955
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
4574
4956
|
if (dash.widgets.has(widgetId)) continue;
|
|
4575
4957
|
orphan(
|
|
4576
4958
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 widget "${widgetId}"`,
|
|
4577
4959
|
`${dashPath}.widgets.${widgetId}`,
|
|
4578
|
-
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` +
|
|
4960
|
+
`Translations are keyed to widget "${widgetId}", which dashboard "${dashName}" does not declare. The widget title stays in the source locale.` + suggest6(widgetId, dash.widgets),
|
|
4579
4961
|
`Match the key to the widget's \`id\`, or drop it.` + (dash.widgets.size > 0 ? ` Declared widget ids: ${listNames(dash.widgets)}.` : "")
|
|
4580
4962
|
);
|
|
4581
4963
|
}
|
|
@@ -4584,7 +4966,7 @@ function validateTranslationReferences(stack) {
|
|
|
4584
4966
|
orphan(
|
|
4585
4967
|
`${inLocale} \xB7 dashboard "${dashName}" \xB7 action "${actionKey}"`,
|
|
4586
4968
|
`${dashPath}.actions.${actionKey}`,
|
|
4587
|
-
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` +
|
|
4969
|
+
`Translations are keyed to header action "${actionKey}", which dashboard "${dashName}" does not declare. The button keeps its source-locale label.` + suggest6(actionKey, dash.actions),
|
|
4588
4970
|
`Header-action translations are keyed by the action's \`actionUrl\`, not its label.` + (dash.actions.size > 0 ? ` Declared header actions: ${listNames(dash.actions)}.` : "")
|
|
4589
4971
|
);
|
|
4590
4972
|
}
|
|
@@ -4594,7 +4976,7 @@ function validateTranslationReferences(stack) {
|
|
|
4594
4976
|
return findings;
|
|
4595
4977
|
}
|
|
4596
4978
|
function asRecord(v) {
|
|
4597
|
-
return
|
|
4979
|
+
return isRec7(v) ? v : {};
|
|
4598
4980
|
}
|
|
4599
4981
|
function checkOptionKeys(findings, ctx) {
|
|
4600
4982
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -4606,7 +4988,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
4606
4988
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
4607
4989
|
where: ctx.where,
|
|
4608
4990
|
path: ctx.path,
|
|
4609
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
4991
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName10(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
4610
4992
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
4611
4993
|
});
|
|
4612
4994
|
return;
|
|
@@ -4619,17 +5001,17 @@ function checkOptionKeys(findings, ctx) {
|
|
|
4619
5001
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
4620
5002
|
where: ctx.where,
|
|
4621
5003
|
path: `${ctx.path}.${key}`,
|
|
4622
|
-
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` +
|
|
5004
|
+
message: byLabel ? `Option translation is keyed by the DISPLAY LABEL "${key}" instead of the stored value "${byLabel}". The resolver looks the option up by value, so this entry is never found and the option renders with its source-locale label.` : `Option translation is keyed by "${key}", which is not one of the values declared by field "${ctx.objectName}.${ctx.fieldName}". The option renders untranslated.` + suggest6(key, declared.values),
|
|
4623
5005
|
hint: byLabel ? `Rename the key to "${byLabel}".` : `Option keys are the stored \`value\`, not the label and not a variant spelling (\`direct_mail\`, not \`direct-mail\`). Declared values: ${listNames(declared.values)}.`
|
|
4624
5006
|
});
|
|
4625
5007
|
}
|
|
4626
5008
|
}
|
|
4627
5009
|
function checkActionParams(findings, ctx) {
|
|
4628
|
-
const rawParams = Object.keys(asRecord(
|
|
5010
|
+
const rawParams = Object.keys(asRecord(isRec7(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
4629
5011
|
if (rawParams.length === 0) return;
|
|
4630
5012
|
const declared = /* @__PURE__ */ new Set();
|
|
4631
|
-
for (const param of
|
|
4632
|
-
const name =
|
|
5013
|
+
for (const param of asArray29(ctx.action.params)) {
|
|
5014
|
+
const name = strName10(param.name) ?? strName10(param.field);
|
|
4633
5015
|
if (name) declared.add(name);
|
|
4634
5016
|
}
|
|
4635
5017
|
for (const paramName of rawParams) {
|
|
@@ -4639,7 +5021,7 @@ function checkActionParams(findings, ctx) {
|
|
|
4639
5021
|
rule: TRANSLATION_TARGET_UNKNOWN,
|
|
4640
5022
|
where: `${ctx.where} \xB7 param "${paramName}"`,
|
|
4641
5023
|
path: `${ctx.path}.params.${paramName}`,
|
|
4642
|
-
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` +
|
|
5024
|
+
message: `Translations are keyed to parameter "${paramName}", which ${ctx.subject} does not declare. The parameter's label and help text render untranslated in the action dialog.` + suggest6(paramName, declared),
|
|
4643
5025
|
hint: `Match the key to a declared param \`name\`, or drop it.` + (declared.size > 0 ? ` Declared params: ${listNames(declared)}.` : "")
|
|
4644
5026
|
});
|
|
4645
5027
|
}
|
|
@@ -4647,14 +5029,14 @@ function checkActionParams(findings, ctx) {
|
|
|
4647
5029
|
|
|
4648
5030
|
// src/validate-ai-surface-affinity.ts
|
|
4649
5031
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
4650
|
-
function
|
|
5032
|
+
function asArray30(v) {
|
|
4651
5033
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4652
5034
|
if (v && typeof v === "object") {
|
|
4653
5035
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4654
5036
|
}
|
|
4655
5037
|
return [];
|
|
4656
5038
|
}
|
|
4657
|
-
function
|
|
5039
|
+
function strName11(v) {
|
|
4658
5040
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4659
5041
|
}
|
|
4660
5042
|
function surfaceOf(v) {
|
|
@@ -4664,18 +5046,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4664
5046
|
const findings = [];
|
|
4665
5047
|
if (!stack || typeof stack !== "object") return findings;
|
|
4666
5048
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
4667
|
-
for (const skill of
|
|
4668
|
-
const n =
|
|
5049
|
+
for (const skill of asArray30(stack.skills)) {
|
|
5050
|
+
const n = strName11(skill.name);
|
|
4669
5051
|
if (n) skillsByName.set(n, skill);
|
|
4670
5052
|
}
|
|
4671
|
-
const agents =
|
|
5053
|
+
const agents = asArray30(stack.agents);
|
|
4672
5054
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4673
5055
|
const agent = agents[ai];
|
|
4674
|
-
const agentName =
|
|
5056
|
+
const agentName = strName11(agent.name) ?? `#${ai}`;
|
|
4675
5057
|
const agentSurface = surfaceOf(agent.surface);
|
|
4676
5058
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
4677
5059
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
4678
|
-
const ref =
|
|
5060
|
+
const ref = strName11(skillRefs[si]);
|
|
4679
5061
|
if (!ref) continue;
|
|
4680
5062
|
const skill = skillsByName.get(ref);
|
|
4681
5063
|
if (!skill) continue;
|
|
@@ -4697,17 +5079,17 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
4697
5079
|
// src/validate-ai-tool-references.ts
|
|
4698
5080
|
import { PLATFORM_PROVIDED_TOOL_NAMES, PLATFORM_TOOL_FAMILY_PREFIXES } from "@objectstack/spec/system";
|
|
4699
5081
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
4700
|
-
function
|
|
5082
|
+
function asArray31(v) {
|
|
4701
5083
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4702
5084
|
if (v && typeof v === "object") {
|
|
4703
5085
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4704
5086
|
}
|
|
4705
5087
|
return [];
|
|
4706
5088
|
}
|
|
4707
|
-
function
|
|
5089
|
+
function strName12(v) {
|
|
4708
5090
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4709
5091
|
}
|
|
4710
|
-
function
|
|
5092
|
+
function distance6(a, b) {
|
|
4711
5093
|
const m = a.length;
|
|
4712
5094
|
const n = b.length;
|
|
4713
5095
|
if (m === 0) return n;
|
|
@@ -4723,14 +5105,14 @@ function distance5(a, b) {
|
|
|
4723
5105
|
}
|
|
4724
5106
|
return prev[n];
|
|
4725
5107
|
}
|
|
4726
|
-
function
|
|
5108
|
+
function suggest7(target, known) {
|
|
4727
5109
|
for (const prefix of PLATFORM_TOOL_FAMILY_PREFIXES) {
|
|
4728
5110
|
if (known.has(`${prefix}${target}`)) return ` Did you mean "${prefix}${target}"?`;
|
|
4729
5111
|
}
|
|
4730
5112
|
let best;
|
|
4731
5113
|
let bestScore = Infinity;
|
|
4732
5114
|
for (const candidate of known) {
|
|
4733
|
-
const d =
|
|
5115
|
+
const d = distance6(target, candidate);
|
|
4734
5116
|
if (d < bestScore) {
|
|
4735
5117
|
bestScore = d;
|
|
4736
5118
|
best = candidate;
|
|
@@ -4745,26 +5127,26 @@ function materialisesAsTool(action) {
|
|
|
4745
5127
|
if (!ai || typeof ai !== "object") return false;
|
|
4746
5128
|
const aiRec = ai;
|
|
4747
5129
|
if (aiRec.exposed !== true) return false;
|
|
4748
|
-
if (!
|
|
4749
|
-
const type =
|
|
5130
|
+
if (!strName12(aiRec.description)) return false;
|
|
5131
|
+
const type = strName12(action.type);
|
|
4750
5132
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
4751
5133
|
if (type === "script") return Boolean(action.target || action.body);
|
|
4752
5134
|
return Boolean(action.target);
|
|
4753
5135
|
}
|
|
4754
5136
|
function collectToolUniverse(stack) {
|
|
4755
5137
|
const universe = new Set(PLATFORM_PROVIDED_TOOL_NAMES);
|
|
4756
|
-
for (const tool of
|
|
4757
|
-
const n =
|
|
5138
|
+
for (const tool of asArray31(stack.tools)) {
|
|
5139
|
+
const n = strName12(tool.name);
|
|
4758
5140
|
if (n) universe.add(n);
|
|
4759
5141
|
}
|
|
4760
5142
|
const addActionFamily = (actions) => {
|
|
4761
|
-
for (const action of
|
|
4762
|
-
const n =
|
|
5143
|
+
for (const action of asArray31(actions)) {
|
|
5144
|
+
const n = strName12(action.name);
|
|
4763
5145
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
4764
5146
|
}
|
|
4765
5147
|
};
|
|
4766
5148
|
addActionFamily(stack.actions);
|
|
4767
|
-
for (const obj of
|
|
5149
|
+
for (const obj of asArray31(stack.objects)) {
|
|
4768
5150
|
addActionFamily(obj.actions);
|
|
4769
5151
|
}
|
|
4770
5152
|
return universe;
|
|
@@ -4772,13 +5154,13 @@ function collectToolUniverse(stack) {
|
|
|
4772
5154
|
function collectUnexposedActionNames(stack) {
|
|
4773
5155
|
const names = /* @__PURE__ */ new Set();
|
|
4774
5156
|
const scan = (actions) => {
|
|
4775
|
-
for (const action of
|
|
4776
|
-
const n =
|
|
5157
|
+
for (const action of asArray31(actions)) {
|
|
5158
|
+
const n = strName12(action.name);
|
|
4777
5159
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
4778
5160
|
}
|
|
4779
5161
|
};
|
|
4780
5162
|
scan(stack.actions);
|
|
4781
|
-
for (const obj of
|
|
5163
|
+
for (const obj of asArray31(stack.objects)) scan(obj.actions);
|
|
4782
5164
|
return names;
|
|
4783
5165
|
}
|
|
4784
5166
|
function validateAiToolReferences(stack) {
|
|
@@ -4796,13 +5178,13 @@ function validateAiToolReferences(stack) {
|
|
|
4796
5178
|
}
|
|
4797
5179
|
return universe.has(ref);
|
|
4798
5180
|
};
|
|
4799
|
-
const skills =
|
|
5181
|
+
const skills = asArray31(stack.skills);
|
|
4800
5182
|
for (let si = 0; si < skills.length; si++) {
|
|
4801
5183
|
const skill = skills[si];
|
|
4802
|
-
const skillName =
|
|
5184
|
+
const skillName = strName12(skill.name) ?? `#${si}`;
|
|
4803
5185
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
4804
5186
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
4805
|
-
const ref =
|
|
5187
|
+
const ref = strName12(refs[ti]);
|
|
4806
5188
|
if (!ref || resolves(ref)) continue;
|
|
4807
5189
|
const isPattern = ref.endsWith("*");
|
|
4808
5190
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -4811,7 +5193,7 @@ function validateAiToolReferences(stack) {
|
|
|
4811
5193
|
rule: AI_SKILL_TOOL_UNRESOLVED,
|
|
4812
5194
|
where: `skill "${skillName}" \xB7 tools`,
|
|
4813
5195
|
path: `skills[${si}].tools[${ti}]`,
|
|
4814
|
-
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` +
|
|
5196
|
+
message: isPattern ? `Skill "${skillName}" subscribes to tool family "${ref}", which matches nothing this stack can resolve (no declared tool, no platform tool, and no AI-exposed declarative action materialises into it). The subscription contributes zero tools at runtime.` : unexposed ? `Skill "${skillName}" references tool "${ref}", but the action "${unexposed}" does not become an AI tool: the runtime materialises \`action_<name>\` only for an action that opts in with \`ai.exposed: true\` + \`ai.description\` (ADR-0011) AND has a headless path (type \`script\`/\`api\`/\`flow\` with a target or body \u2014 \`url\`/\`modal\`/\`form\` are UI-only). The reference is dropped at runtime, so the skill promises a capability the agent cannot call.` : `Skill "${skillName}" references tool "${ref}", which resolves to nothing this stack can see: not a \`stack.tools\` record, not a platform-registered tool, and not a materialised action tool (\`action_<name>\`). The runtime silently drops the reference, so the skill's instructions claim a capability the agent does not have \u2014 the assistant will improvise or fail when asked to use it.` + suggest7(ref, universe),
|
|
4815
5197
|
hint: unexposed ? `Either opt "${unexposed}" in \u2014 set \`ai: { exposed: true, description: '\u2026' }\` (\u226540 chars, LLM-facing) and give it a headless type \u2014 or drop the reference and have the skill's instructions recommend the UI action instead. A \`modal\`/\`form\`/\`url\` action stays human-driven by design; that is a legitimate answer, not a gap.` : `Back "${ref}" with a real executable: declare a declarative action (or flow), opt it in with \`ai.exposed: true\` + \`ai.description\`, and reference its materialised tool (\`action_<name>\` \u2014 the ADR-0109 default path, no tool record needed); or reference a platform tool by its registered name; or remove the reference and the instructions that mention it. Ignore this only if a runtime plugin outside the platform registry provides "${ref}". Family prefixes materialised by the runtime: ${PLATFORM_TOOL_FAMILY_PREFIXES.join(", ")}.`
|
|
4816
5198
|
});
|
|
4817
5199
|
}
|
|
@@ -4821,24 +5203,24 @@ function validateAiToolReferences(stack) {
|
|
|
4821
5203
|
|
|
4822
5204
|
// src/validate-ai-agent-authoring.ts
|
|
4823
5205
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
4824
|
-
function
|
|
5206
|
+
function asArray32(v) {
|
|
4825
5207
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
4826
5208
|
if (v && typeof v === "object") {
|
|
4827
5209
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4828
5210
|
}
|
|
4829
5211
|
return [];
|
|
4830
5212
|
}
|
|
4831
|
-
function
|
|
5213
|
+
function strName13(v) {
|
|
4832
5214
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4833
5215
|
}
|
|
4834
5216
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
4835
5217
|
function validateAiAgentAuthoring(stack) {
|
|
4836
5218
|
const findings = [];
|
|
4837
5219
|
if (!stack || typeof stack !== "object") return findings;
|
|
4838
|
-
const agents =
|
|
5220
|
+
const agents = asArray32(stack.agents);
|
|
4839
5221
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
4840
5222
|
const agent = agents[ai];
|
|
4841
|
-
const name =
|
|
5223
|
+
const name = strName13(agent.name) ?? `#${ai}`;
|
|
4842
5224
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
4843
5225
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
4844
5226
|
findings.push({
|
|
@@ -4853,9 +5235,494 @@ function validateAiAgentAuthoring(stack) {
|
|
|
4853
5235
|
return findings;
|
|
4854
5236
|
}
|
|
4855
5237
|
|
|
5238
|
+
// src/validate-hook-body-writes.ts
|
|
5239
|
+
import { createRequire as createRequire3 } from "module";
|
|
5240
|
+
import { findClosestMatches, formatSuggestion } from "@objectstack/spec/shared";
|
|
5241
|
+
var cachedTs2 = null;
|
|
5242
|
+
function loadTypeScript2() {
|
|
5243
|
+
if (cachedTs2) return cachedTs2;
|
|
5244
|
+
const anchor = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5245
|
+
try {
|
|
5246
|
+
cachedTs2 = createRequire3(anchor)("typescript");
|
|
5247
|
+
} catch (err) {
|
|
5248
|
+
throw new Error(
|
|
5249
|
+
`@objectstack/lint: checking an L2 (language:'js') hook body requires the "typescript" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "typescript" in the image; it is only loaded when a hook with a JS body is validated.`
|
|
5250
|
+
);
|
|
5251
|
+
}
|
|
5252
|
+
return cachedTs2;
|
|
5253
|
+
}
|
|
5254
|
+
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
5255
|
+
var HOOK_BODY_WRITE_PATTERNS = [
|
|
5256
|
+
{
|
|
5257
|
+
id: "input-property-assign",
|
|
5258
|
+
syntax: "ctx.input.<field> = \u2026 | ctx.input['<field>'] \u27E8op\u27E9= \u2026",
|
|
5259
|
+
example: {
|
|
5260
|
+
// Compound (`+=`) and logical (`??=`) assignment operators write their
|
|
5261
|
+
// LHS exactly like `=` does — the example pins the whole operator range.
|
|
5262
|
+
source: "ctx.input.total = 0; ctx.input['status'] ??= 'open'; ctx.input.retries += 1;",
|
|
5263
|
+
writes: [{ field: "total" }, { field: "status" }, { field: "retries" }]
|
|
5264
|
+
}
|
|
5265
|
+
},
|
|
5266
|
+
{
|
|
5267
|
+
id: "input-object-assign",
|
|
5268
|
+
syntax: "Object.assign(ctx.input, { <field>: \u2026 })",
|
|
5269
|
+
example: {
|
|
5270
|
+
source: "Object.assign(ctx.input, { total: 5, 'status': 'open', discount });",
|
|
5271
|
+
writes: [{ field: "total" }, { field: "status" }, { field: "discount" }]
|
|
5272
|
+
}
|
|
5273
|
+
},
|
|
5274
|
+
{
|
|
5275
|
+
// ACTION-only shape (the hook sandbox context has no `ctx.record` at all).
|
|
5276
|
+
// Declared here because this ledger is the extractor's shape inventory, not
|
|
5277
|
+
// any one rule's; every consumer declares which shapes it consumes.
|
|
5278
|
+
id: "record-property-assign",
|
|
5279
|
+
syntax: "ctx.record.<field> = \u2026 | ctx.record['<field>'] \u27E8op\u27E9= \u2026",
|
|
5280
|
+
example: {
|
|
5281
|
+
source: "ctx.record.stage = 'won'; ctx.record['amount'] += 1;",
|
|
5282
|
+
writes: [{ field: "stage" }, { field: "amount" }]
|
|
5283
|
+
}
|
|
5284
|
+
},
|
|
5285
|
+
{
|
|
5286
|
+
id: "api-crud-literal",
|
|
5287
|
+
syntax: "ctx.api.object('<object>').insert({\u2026}) | .create({\u2026}) | .update({\u2026}) | .updateById(id, {\u2026})",
|
|
5288
|
+
example: {
|
|
5289
|
+
// Real ObjectRepository signatures: the record payload is argument 0 for
|
|
5290
|
+
// insert/create/update and argument 1 for updateById. (`update(data)` —
|
|
5291
|
+
// NOT `update(id, data)`; the id travels inside the payload/options.)
|
|
5292
|
+
source: "await ctx.api.object('audit_log').insert({ event: 'won' }); await ctx.api.object('crm_deal').updateById(id, { stage: 'won' });",
|
|
5293
|
+
writes: [
|
|
5294
|
+
{ field: "event", object: "audit_log" },
|
|
5295
|
+
{ field: "stage", object: "crm_deal" }
|
|
5296
|
+
]
|
|
5297
|
+
}
|
|
5298
|
+
}
|
|
5299
|
+
];
|
|
5300
|
+
var HOOK_BODY_WRITE_PATTERN_IDS = [
|
|
5301
|
+
"input-property-assign",
|
|
5302
|
+
"input-object-assign",
|
|
5303
|
+
"api-crud-literal"
|
|
5304
|
+
];
|
|
5305
|
+
var HOOK_BODY_WRITE_EXCLUSIONS = [
|
|
5306
|
+
{
|
|
5307
|
+
id: "record-property-assign",
|
|
5308
|
+
reason: "a hook sandbox context has no `ctx.record` at all \u2014 `buildSandboxContext` never sets it (a hook\u2019s record IS `ctx.input`), so the expression throws at run time rather than silently no-op\u2019ing. A loud failure the author sees on the first run is not this advisory rule\u2019s business"
|
|
5309
|
+
}
|
|
5310
|
+
];
|
|
5311
|
+
var HOOK_APPLICABLE_IDS = new Set(HOOK_BODY_WRITE_PATTERN_IDS);
|
|
5312
|
+
var API_WRITE_METHODS = /* @__PURE__ */ new Map([
|
|
5313
|
+
["insert", 0],
|
|
5314
|
+
["create", 0],
|
|
5315
|
+
["update", 0],
|
|
5316
|
+
["updateById", 1]
|
|
5317
|
+
]);
|
|
5318
|
+
var INPUT_ENVELOPE_KEYS = /* @__PURE__ */ new Set(["id", "options", "ast", "data"]);
|
|
5319
|
+
var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
5320
|
+
...SYSTEM_FIELDS,
|
|
5321
|
+
"_id",
|
|
5322
|
+
"name",
|
|
5323
|
+
"space",
|
|
5324
|
+
"owner",
|
|
5325
|
+
"record_type"
|
|
5326
|
+
]);
|
|
5327
|
+
var isRec8 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5328
|
+
function asArray33(v) {
|
|
5329
|
+
if (Array.isArray(v)) return v.filter((x) => isRec8(x));
|
|
5330
|
+
if (isRec8(v)) {
|
|
5331
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5332
|
+
name,
|
|
5333
|
+
...isRec8(def) ? def : {}
|
|
5334
|
+
}));
|
|
5335
|
+
}
|
|
5336
|
+
return [];
|
|
5337
|
+
}
|
|
5338
|
+
function indexObjectFields2(stack) {
|
|
5339
|
+
const out = /* @__PURE__ */ new Map();
|
|
5340
|
+
for (const obj of asArray33(stack.objects)) {
|
|
5341
|
+
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
5342
|
+
if (!name) continue;
|
|
5343
|
+
const names = /* @__PURE__ */ new Set();
|
|
5344
|
+
for (const f of asArray33(obj.fields)) {
|
|
5345
|
+
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
5346
|
+
}
|
|
5347
|
+
out.set(name, names);
|
|
5348
|
+
}
|
|
5349
|
+
return out;
|
|
5350
|
+
}
|
|
5351
|
+
function judgeableFieldsOf(index, objectName) {
|
|
5352
|
+
const declared = index.get(objectName);
|
|
5353
|
+
if (!declared || declared.size === 0) return void 0;
|
|
5354
|
+
return declared;
|
|
5355
|
+
}
|
|
5356
|
+
function extractHookBodyWrites(source) {
|
|
5357
|
+
return extractHookBodyWriteSet(source).writes;
|
|
5358
|
+
}
|
|
5359
|
+
function extractHookBodyWriteSet(source) {
|
|
5360
|
+
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
5361
|
+
return { writes: [], ctxRecordEscapes: false };
|
|
5362
|
+
}
|
|
5363
|
+
const tsc = loadTypeScript2();
|
|
5364
|
+
const sf = tsc.createSourceFile(
|
|
5365
|
+
"hook-body.ts",
|
|
5366
|
+
`async function __body(ctx) {
|
|
5367
|
+
${source}
|
|
5368
|
+
}`,
|
|
5369
|
+
tsc.ScriptTarget.Latest,
|
|
5370
|
+
/* setParentNodes */
|
|
5371
|
+
false,
|
|
5372
|
+
tsc.ScriptKind.TS
|
|
5373
|
+
);
|
|
5374
|
+
const writes = [];
|
|
5375
|
+
const recordRefs = [];
|
|
5376
|
+
const consumedRecordRefs = /* @__PURE__ */ new Set();
|
|
5377
|
+
const isCtxDot = (node, prop) => tsc.isPropertyAccessExpression(node) && tsc.isIdentifier(node.expression) && node.expression.text === "ctx" && node.name.text === prop;
|
|
5378
|
+
const fieldOfCtxLhs = (lhs, prop) => {
|
|
5379
|
+
if (tsc.isPropertyAccessExpression(lhs) && tsc.isIdentifier(lhs.name) && isCtxDot(lhs.expression, prop)) {
|
|
5380
|
+
return lhs.name.text;
|
|
5381
|
+
}
|
|
5382
|
+
if (tsc.isElementAccessExpression(lhs) && isCtxDot(lhs.expression, prop)) {
|
|
5383
|
+
const arg = lhs.argumentExpression;
|
|
5384
|
+
if (tsc.isStringLiteral(arg) || tsc.isNoSubstitutionTemplateLiteral(arg)) return arg.text;
|
|
5385
|
+
}
|
|
5386
|
+
return void 0;
|
|
5387
|
+
};
|
|
5388
|
+
const literalObjectKeys = (node) => {
|
|
5389
|
+
if (!tsc.isObjectLiteralExpression(node)) return [];
|
|
5390
|
+
const keys = [];
|
|
5391
|
+
for (const p of node.properties) {
|
|
5392
|
+
if (tsc.isPropertyAssignment(p)) {
|
|
5393
|
+
if (tsc.isIdentifier(p.name) || tsc.isStringLiteral(p.name)) keys.push(p.name.text);
|
|
5394
|
+
} else if (tsc.isShorthandPropertyAssignment(p)) {
|
|
5395
|
+
keys.push(p.name.text);
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
return keys;
|
|
5399
|
+
};
|
|
5400
|
+
const visit = (node) => {
|
|
5401
|
+
if (tsc.isBinaryExpression(node) && node.operatorToken.kind >= tsc.SyntaxKind.FirstAssignment && node.operatorToken.kind <= tsc.SyntaxKind.LastAssignment) {
|
|
5402
|
+
const inputField = fieldOfCtxLhs(node.left, "input");
|
|
5403
|
+
if (inputField !== void 0 && !INPUT_ENVELOPE_KEYS.has(inputField)) {
|
|
5404
|
+
writes.push({ patternId: "input-property-assign", field: inputField });
|
|
5405
|
+
}
|
|
5406
|
+
const recordField = fieldOfCtxLhs(node.left, "record");
|
|
5407
|
+
if (recordField !== void 0) {
|
|
5408
|
+
writes.push({ patternId: "record-property-assign", field: recordField });
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
if (tsc.isPropertyAccessExpression(node) || tsc.isElementAccessExpression(node)) {
|
|
5412
|
+
if (isCtxDot(node.expression, "record")) consumedRecordRefs.add(node.expression);
|
|
5413
|
+
}
|
|
5414
|
+
if (tsc.isBinaryExpression(node)) {
|
|
5415
|
+
const op = node.operatorToken.kind;
|
|
5416
|
+
if ((op === tsc.SyntaxKind.AmpersandAmpersandToken || op === tsc.SyntaxKind.BarBarToken || op === tsc.SyntaxKind.QuestionQuestionToken) && isCtxDot(node.left, "record")) {
|
|
5417
|
+
consumedRecordRefs.add(node.left);
|
|
5418
|
+
}
|
|
5419
|
+
}
|
|
5420
|
+
if (tsc.isPrefixUnaryExpression(node) && node.operator === tsc.SyntaxKind.ExclamationToken) {
|
|
5421
|
+
if (isCtxDot(node.operand, "record")) consumedRecordRefs.add(node.operand);
|
|
5422
|
+
}
|
|
5423
|
+
if (tsc.isTypeOfExpression(node) && isCtxDot(node.expression, "record")) {
|
|
5424
|
+
consumedRecordRefs.add(node.expression);
|
|
5425
|
+
}
|
|
5426
|
+
if ((tsc.isIfStatement(node) || tsc.isWhileStatement(node) || tsc.isDoStatement(node)) && isCtxDot(node.expression, "record")) {
|
|
5427
|
+
consumedRecordRefs.add(node.expression);
|
|
5428
|
+
}
|
|
5429
|
+
if (tsc.isConditionalExpression(node) && isCtxDot(node.condition, "record")) {
|
|
5430
|
+
consumedRecordRefs.add(node.condition);
|
|
5431
|
+
}
|
|
5432
|
+
if (isCtxDot(node, "record")) recordRefs.push(node);
|
|
5433
|
+
if (tsc.isCallExpression(node)) {
|
|
5434
|
+
const callee = node.expression;
|
|
5435
|
+
if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.expression) && callee.expression.text === "Object" && callee.name.text === "assign" && node.arguments.length >= 2 && isCtxDot(node.arguments[0], "input")) {
|
|
5436
|
+
for (const arg of node.arguments.slice(1)) {
|
|
5437
|
+
for (const field of literalObjectKeys(arg)) {
|
|
5438
|
+
if (!INPUT_ENVELOPE_KEYS.has(field)) {
|
|
5439
|
+
writes.push({ patternId: "input-object-assign", field });
|
|
5440
|
+
}
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
if (tsc.isPropertyAccessExpression(callee) && tsc.isIdentifier(callee.name)) {
|
|
5445
|
+
const payloadIndex = API_WRITE_METHODS.get(callee.name.text);
|
|
5446
|
+
const recv = callee.expression;
|
|
5447
|
+
if (payloadIndex !== void 0 && tsc.isCallExpression(recv) && tsc.isPropertyAccessExpression(recv.expression) && recv.expression.name.text === "object" && isCtxDot(recv.expression.expression, "api") && recv.arguments.length === 1) {
|
|
5448
|
+
const objArg = recv.arguments[0];
|
|
5449
|
+
const objectName = tsc.isStringLiteral(objArg) || tsc.isNoSubstitutionTemplateLiteral(objArg) ? objArg.text : void 0;
|
|
5450
|
+
const payload = node.arguments[payloadIndex];
|
|
5451
|
+
if (objectName && payload !== void 0) {
|
|
5452
|
+
for (const field of literalObjectKeys(payload)) {
|
|
5453
|
+
writes.push({
|
|
5454
|
+
patternId: "api-crud-literal",
|
|
5455
|
+
object: objectName,
|
|
5456
|
+
method: callee.name.text,
|
|
5457
|
+
field
|
|
5458
|
+
});
|
|
5459
|
+
}
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
}
|
|
5463
|
+
}
|
|
5464
|
+
tsc.forEachChild(node, visit);
|
|
5465
|
+
};
|
|
5466
|
+
visit(sf);
|
|
5467
|
+
return {
|
|
5468
|
+
writes,
|
|
5469
|
+
ctxRecordEscapes: recordRefs.some((ref) => !consumedRecordRefs.has(ref))
|
|
5470
|
+
};
|
|
5471
|
+
}
|
|
5472
|
+
function validateHookBodyWrites(stack) {
|
|
5473
|
+
const findings = [];
|
|
5474
|
+
const hooks = asArray33(stack.hooks);
|
|
5475
|
+
if (hooks.length === 0) return findings;
|
|
5476
|
+
let objectFields = null;
|
|
5477
|
+
hooks.forEach((hook, hookIndex) => {
|
|
5478
|
+
const body = hook.body;
|
|
5479
|
+
if (!isRec8(body) || body.language !== "js") return;
|
|
5480
|
+
const source = body.source;
|
|
5481
|
+
if (typeof source !== "string" || source.trim() === "") return;
|
|
5482
|
+
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
5483
|
+
if (writes.length === 0) return;
|
|
5484
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5485
|
+
const hookName = typeof hook.name === "string" && hook.name ? hook.name : `#${hookIndex}`;
|
|
5486
|
+
const targets = (Array.isArray(hook.object) ? hook.object : [hook.object]).filter(
|
|
5487
|
+
(o) => typeof o === "string" && o.trim() !== ""
|
|
5488
|
+
);
|
|
5489
|
+
const targetSets = targets.map((t) => judgeableFieldsOf(objectFields, t));
|
|
5490
|
+
const inputJudgeable = targets.length > 0 && !targets.includes("*") && targetSets.every((s) => s !== void 0);
|
|
5491
|
+
const where = `hook "${hookName}" \u203A body`;
|
|
5492
|
+
const path = `hooks[${hookIndex}].body.source`;
|
|
5493
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5494
|
+
for (const w of writes) {
|
|
5495
|
+
const dedupeKey = `${w.object ?? ""}\0${w.field}`;
|
|
5496
|
+
if (reported.has(dedupeKey)) continue;
|
|
5497
|
+
if (w.object === void 0) {
|
|
5498
|
+
if (!inputJudgeable) continue;
|
|
5499
|
+
if (IMPLICIT_FIELDS2.has(w.field)) continue;
|
|
5500
|
+
if (targetSets.some((s) => s.has(w.field))) continue;
|
|
5501
|
+
reported.add(dedupeKey);
|
|
5502
|
+
const objDesc = targets.length === 1 ? `object '${targets[0]}'` : `none of its target objects (${targets.join(", ")})`;
|
|
5503
|
+
const declares = targets.length === 1 ? "declares no such field" : "declare that field";
|
|
5504
|
+
findings.push({
|
|
5505
|
+
severity: "warning",
|
|
5506
|
+
rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5507
|
+
where,
|
|
5508
|
+
path,
|
|
5509
|
+
message: `body writes '${w.field}' to its input, but ${objDesc} ${declares}. The sandboxed script runs clean and the value is copied back onto the record payload unfiltered \u2014 on a SQL driver the stray column then fails the WHOLE write with a driver-level error far from here; on a schemaless driver (memory, MongoDB) it is persisted as an undeclared key (#4271).`,
|
|
5510
|
+
hint: fixHint(w.field, unionCandidates(targetSets))
|
|
5511
|
+
});
|
|
5512
|
+
} else {
|
|
5513
|
+
const known = judgeableFieldsOf(objectFields, w.object);
|
|
5514
|
+
if (!known) continue;
|
|
5515
|
+
if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
|
|
5516
|
+
reported.add(dedupeKey);
|
|
5517
|
+
findings.push({
|
|
5518
|
+
severity: "warning",
|
|
5519
|
+
rule: HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
5520
|
+
where,
|
|
5521
|
+
path,
|
|
5522
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole call then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
|
|
5523
|
+
hint: fixHint(w.field, [...known])
|
|
5524
|
+
});
|
|
5525
|
+
}
|
|
5526
|
+
}
|
|
5527
|
+
});
|
|
5528
|
+
return findings;
|
|
5529
|
+
}
|
|
5530
|
+
function unionCandidates(targetSets) {
|
|
5531
|
+
const out = /* @__PURE__ */ new Set();
|
|
5532
|
+
for (const s of targetSets) for (const f of s ?? []) out.add(f);
|
|
5533
|
+
return [...out];
|
|
5534
|
+
}
|
|
5535
|
+
function fixHint(field, declared) {
|
|
5536
|
+
const suggestion = formatSuggestion(findClosestMatches(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5537
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in HOOK_BODY_WRITE_PATTERNS are checked \u2014 computed keys, spreads and aliased input are not \u2014 and this warning never blocks a build.`;
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
// src/validate-action-body-writes.ts
|
|
5541
|
+
import { findClosestMatches as findClosestMatches2, formatSuggestion as formatSuggestion2 } from "@objectstack/spec/shared";
|
|
5542
|
+
var ACTION_BODY_WRITE_UNKNOWN_FIELD = "action-body-write-unknown-field";
|
|
5543
|
+
var ACTION_RECORD_WRITE_DISCARDED = "action-record-write-discarded";
|
|
5544
|
+
var ACTION_BODY_WRITE_PATTERN_IDS = ["api-crud-literal"];
|
|
5545
|
+
var ACTION_RECORD_WRITE_PATTERN_IDS = ["record-property-assign"];
|
|
5546
|
+
var ACTION_BODY_WRITE_EXCLUSIONS = [
|
|
5547
|
+
{
|
|
5548
|
+
id: "input-property-assign",
|
|
5549
|
+
reason: "an action's ctx.input is its params bag (`input: unwrapProxyToPlain(actionCtx?.params)`), not a record \u2014 `ctx.input.<name>` writes a declared PARAMETER, which object fields cannot judge"
|
|
5550
|
+
},
|
|
5551
|
+
{
|
|
5552
|
+
id: "input-object-assign",
|
|
5553
|
+
reason: "same surface as input-property-assign \u2014 Object.assign(ctx.input, \u2026) targets the params bag"
|
|
5554
|
+
}
|
|
5555
|
+
];
|
|
5556
|
+
var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_BODY_WRITE_PATTERN_IDS.includes(p.id));
|
|
5557
|
+
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
5558
|
+
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
5559
|
+
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
5560
|
+
var isRec9 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5561
|
+
function asArray34(v) {
|
|
5562
|
+
if (Array.isArray(v)) return v.filter((x) => isRec9(x));
|
|
5563
|
+
if (isRec9(v)) {
|
|
5564
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5565
|
+
name,
|
|
5566
|
+
...isRec9(def) ? def : {}
|
|
5567
|
+
}));
|
|
5568
|
+
}
|
|
5569
|
+
return [];
|
|
5570
|
+
}
|
|
5571
|
+
function actionObjectBinding(action, parentObject) {
|
|
5572
|
+
if (typeof action.object === "string" && action.object) return action.object;
|
|
5573
|
+
if (typeof action.objectName === "string" && action.objectName) return action.objectName;
|
|
5574
|
+
return parentObject;
|
|
5575
|
+
}
|
|
5576
|
+
function collectActionBodies(stack) {
|
|
5577
|
+
const sites = [];
|
|
5578
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5579
|
+
const collect = (actions, pathPrefix, parentObject) => {
|
|
5580
|
+
asArray34(actions).forEach((action, index) => {
|
|
5581
|
+
const body = action.body;
|
|
5582
|
+
if (!isRec9(body) || body.language !== "js") return;
|
|
5583
|
+
const source = body.source;
|
|
5584
|
+
if (typeof source !== "string" || source.trim() === "") return;
|
|
5585
|
+
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
5586
|
+
const key = `${actionObjectBinding(action, parentObject) ?? ""}\0${name}\0${source}`;
|
|
5587
|
+
if (seen.has(key)) return;
|
|
5588
|
+
seen.add(key);
|
|
5589
|
+
sites.push({ name, source, path: `${pathPrefix}[${index}].body.source` });
|
|
5590
|
+
});
|
|
5591
|
+
};
|
|
5592
|
+
collect(stack.actions, "actions");
|
|
5593
|
+
asArray34(stack.objects).forEach((obj, objIndex) => {
|
|
5594
|
+
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
5595
|
+
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
5596
|
+
});
|
|
5597
|
+
return sites;
|
|
5598
|
+
}
|
|
5599
|
+
function validateActionBodyWrites(stack) {
|
|
5600
|
+
const findings = [];
|
|
5601
|
+
if (!isRec9(stack)) return findings;
|
|
5602
|
+
const sites = collectActionBodies(stack);
|
|
5603
|
+
if (sites.length === 0) return findings;
|
|
5604
|
+
let objectFields = null;
|
|
5605
|
+
for (const site of sites) {
|
|
5606
|
+
if (!/\bapi\b/.test(site.source) && !/\brecord\b/.test(site.source)) continue;
|
|
5607
|
+
const { writes: allWrites, ctxRecordEscapes } = extractHookBodyWriteSet(site.source);
|
|
5608
|
+
const writes = allWrites.filter((w) => APPLICABLE_IDS.has(w.patternId));
|
|
5609
|
+
const recordWrites = allWrites.filter((w) => RECORD_WRITE_IDS.has(w.patternId));
|
|
5610
|
+
if (writes.length === 0 && recordWrites.length === 0) continue;
|
|
5611
|
+
const where = `action "${site.name}" \u203A body`;
|
|
5612
|
+
if (recordWrites.length > 0 && !ctxRecordEscapes) {
|
|
5613
|
+
const reportedFields = /* @__PURE__ */ new Set();
|
|
5614
|
+
for (const w of recordWrites) {
|
|
5615
|
+
if (reportedFields.has(w.field)) continue;
|
|
5616
|
+
reportedFields.add(w.field);
|
|
5617
|
+
findings.push({
|
|
5618
|
+
severity: "warning",
|
|
5619
|
+
rule: ACTION_RECORD_WRITE_DISCARDED,
|
|
5620
|
+
where,
|
|
5621
|
+
path: site.path,
|
|
5622
|
+
message: `body assigns ctx.record.${w.field}, but an action's ctx.record is a plain snapshot the runtime never writes back \u2014 the action returns success and the assignment is discarded, whether or not '${w.field}' is a declared field (#4345).`,
|
|
5623
|
+
hint: `To persist it, write through the API: ctx.api.object('<object>').updateById(ctx.recordId, { ${w.field}: \u2026 }). Reported only because ctx.record is never passed anywhere in this body \u2014 mutating the snapshot and then handing it to an API write is a live payload and is not flagged. This warning never blocks a build.`
|
|
5624
|
+
});
|
|
5625
|
+
}
|
|
5626
|
+
}
|
|
5627
|
+
if (writes.length === 0) continue;
|
|
5628
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5629
|
+
const reported = /* @__PURE__ */ new Set();
|
|
5630
|
+
for (const w of writes) {
|
|
5631
|
+
if (w.object === void 0) continue;
|
|
5632
|
+
const dedupeKey = `${w.object}\0${w.field}`;
|
|
5633
|
+
if (reported.has(dedupeKey)) continue;
|
|
5634
|
+
const known = judgeableFieldsOf(objectFields, w.object);
|
|
5635
|
+
if (!known) continue;
|
|
5636
|
+
if (IMPLICIT_FIELDS2.has(w.field) || known.has(w.field)) continue;
|
|
5637
|
+
reported.add(dedupeKey);
|
|
5638
|
+
findings.push({
|
|
5639
|
+
severity: "warning",
|
|
5640
|
+
rule: ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
5641
|
+
where,
|
|
5642
|
+
path: site.path,
|
|
5643
|
+
message: `body calls ctx.api.object('${w.object}').${w.method ?? "update"}(\u2026) writing '${w.field}', but object '${w.object}' declares no such field. The write-path validator skips the unknown key \u2014 on a SQL driver the whole action then fails with a driver-level error far from here; on a schemaless driver (memory, MongoDB) the stray key is persisted (#4271).`,
|
|
5644
|
+
hint: fixHint2(w.field, [...known])
|
|
5645
|
+
});
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5648
|
+
return findings;
|
|
5649
|
+
}
|
|
5650
|
+
function fixHint2(field, declared) {
|
|
5651
|
+
const suggestion = formatSuggestion2(findClosestMatches2(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5652
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. Only the literal write patterns in ACTION_BODY_WRITE_PATTERNS are checked \u2014 an action's ctx.input is its params bag, so it is not a record-write surface and is never resolved against fields \u2014 and this warning never blocks a build.`;
|
|
5653
|
+
}
|
|
5654
|
+
|
|
5655
|
+
// src/validate-flow-node-writes.ts
|
|
5656
|
+
import { findClosestMatches as findClosestMatches3, formatSuggestion as formatSuggestion3 } from "@objectstack/spec/shared";
|
|
5657
|
+
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
5658
|
+
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
5659
|
+
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
5660
|
+
var isRec10 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5661
|
+
function asArray35(v) {
|
|
5662
|
+
if (Array.isArray(v)) return v.filter((x) => isRec10(x));
|
|
5663
|
+
if (isRec10(v)) {
|
|
5664
|
+
return Object.entries(v).map(([name, def]) => ({
|
|
5665
|
+
name,
|
|
5666
|
+
...isRec10(def) ? def : {}
|
|
5667
|
+
}));
|
|
5668
|
+
}
|
|
5669
|
+
return [];
|
|
5670
|
+
}
|
|
5671
|
+
function readLiteralObjectName2(config) {
|
|
5672
|
+
const raw = config.objectName ?? config.object;
|
|
5673
|
+
if (typeof raw !== "string" || raw.includes("{")) return void 0;
|
|
5674
|
+
return raw || void 0;
|
|
5675
|
+
}
|
|
5676
|
+
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
5677
|
+
function validateFlowNodeWrites(stack) {
|
|
5678
|
+
const findings = [];
|
|
5679
|
+
if (!isRec10(stack)) return findings;
|
|
5680
|
+
const flows = asArray35(stack.flows);
|
|
5681
|
+
if (flows.length === 0) return findings;
|
|
5682
|
+
let objectFields = null;
|
|
5683
|
+
flows.forEach((flow, flowIndex) => {
|
|
5684
|
+
const flowName = typeof flow.name === "string" && flow.name ? flow.name : `#${flowIndex}`;
|
|
5685
|
+
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
5686
|
+
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
5687
|
+
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
5688
|
+
const config = isRec10(node.config) ? node.config : void 0;
|
|
5689
|
+
if (!config) return;
|
|
5690
|
+
const fields = config.fields;
|
|
5691
|
+
if (!isRec10(fields)) return;
|
|
5692
|
+
const written = Object.keys(fields);
|
|
5693
|
+
if (written.length === 0) return;
|
|
5694
|
+
const objectName = readLiteralObjectName2(config);
|
|
5695
|
+
if (!objectName) return;
|
|
5696
|
+
objectFields ?? (objectFields = indexObjectFields2(stack));
|
|
5697
|
+
const known = judgeableFieldsOf(objectFields, objectName);
|
|
5698
|
+
if (!known) return;
|
|
5699
|
+
const nodeName = flowNodeLabel(node, walkIndex);
|
|
5700
|
+
const nodeWhere = regionTrail ? `${regionTrail} \u203A node "${nodeName}"` : `node "${nodeName}"`;
|
|
5701
|
+
for (const fieldName of written) {
|
|
5702
|
+
if (known.has(fieldName) || IMPLICIT_FIELDS2.has(fieldName)) continue;
|
|
5703
|
+
if (fieldName.includes(".")) continue;
|
|
5704
|
+
findings.push({
|
|
5705
|
+
severity: "error",
|
|
5706
|
+
rule: FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
5707
|
+
where: `flow "${flowName}" \u203A ${nodeWhere}`,
|
|
5708
|
+
path: `${nodePath}.config.fields.${fieldName}`,
|
|
5709
|
+
message: `${node.type} writes '${fieldName}', but object '${objectName}' declares no such field. Nothing between the node and storage removes the key: on a SQL datasource the driver rejects the whole statement ('no such column'), so the correctly named fields in this same payload never land either${node.type === "create_record" ? " and the record is never created at all" : ""}; on a schemaless one the stray key is persisted into a column no read surface returns.`,
|
|
5710
|
+
hint: fixHint3(fieldName, [...known])
|
|
5711
|
+
});
|
|
5712
|
+
}
|
|
5713
|
+
});
|
|
5714
|
+
});
|
|
5715
|
+
return findings;
|
|
5716
|
+
}
|
|
5717
|
+
function fixHint3(field, declared) {
|
|
5718
|
+
const suggestion = formatSuggestion3(findClosestMatches3(field, [...declared, ...IMPLICIT_FIELDS2]));
|
|
5719
|
+
return (suggestion ? `${suggestion} ` : "") + `Fix the field name, or declare '${field}' on the object. This gates the build rather than warning: the key is literal and so is the object, so unlike the hook/action body rules there is nothing here that could have been mis-extracted.`;
|
|
5720
|
+
}
|
|
5721
|
+
|
|
4856
5722
|
// src/reference-integrity-suite.ts
|
|
4857
5723
|
var REFERENCE_INTEGRITY_RULES = [
|
|
4858
5724
|
{ name: "validateObjectReferences", run: validateObjectReferences },
|
|
5725
|
+
{ name: "validateSearchableFields", run: validateSearchableFields },
|
|
4859
5726
|
{ name: "validateActionNameRefs", run: validateActionNameRefs },
|
|
4860
5727
|
{ name: "validatePageFieldBindings", run: validatePageFieldBindings },
|
|
4861
5728
|
{ name: "validateChartBindings", run: validateChartBindings },
|
|
@@ -4864,7 +5731,70 @@ var REFERENCE_INTEGRITY_RULES = [
|
|
|
4864
5731
|
{ name: "validateFlowTemplatePaths", run: validateFlowTemplatePaths },
|
|
4865
5732
|
{ name: "validateAiSurfaceAffinity", run: validateAiSurfaceAffinity },
|
|
4866
5733
|
{ name: "validateAiToolReferences", run: validateAiToolReferences },
|
|
4867
|
-
{ name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring }
|
|
5734
|
+
{ name: "validateAiAgentAuthoring", run: validateAiAgentAuthoring },
|
|
5735
|
+
// Field names WRITTEN by an L2 hook body (`ctx.input.x = …`,
|
|
5736
|
+
// `ctx.api.object('y').update({ x })`), resolved against the target object's
|
|
5737
|
+
// declared fields — the write-side counterpart of validateFlowTemplatePaths'
|
|
5738
|
+
// read-side membership (#4271). Lazy: only a hook that actually carries a
|
|
5739
|
+
// `language:'js'` body loads the TypeScript parser.
|
|
5740
|
+
{ name: "validateHookBodyWrites", run: validateHookBodyWrites },
|
|
5741
|
+
// The same check on the other surface that carries a `HookBodySchema` body:
|
|
5742
|
+
// action bodies, run by the same sandbox. Only the `ctx.api` write family
|
|
5743
|
+
// carries over — an action's `ctx.input` is its params bag, not a record
|
|
5744
|
+
// (see that module's ledger). Lazy on the same terms.
|
|
5745
|
+
//
|
|
5746
|
+
// The first member here to emit more than one rule id (`validateReactPageProps`
|
|
5747
|
+
// below is the other, and carries the most). Besides resolving `ctx.api`
|
|
5748
|
+
// writes against declared fields (`action-body-write-unknown-field`), it
|
|
5749
|
+
// reports a `ctx.record` write that can reach nothing
|
|
5750
|
+
// (`action-record-write-discarded`, #4345) — not a resolution question, so
|
|
5751
|
+
// by the charter above it does not belong in the suite. It rides along
|
|
5752
|
+
// anyway because it falls out of the SAME parse of the SAME source: a
|
|
5753
|
+
// separate member would parse every action body twice to say two things
|
|
5754
|
+
// about one walk, and hand-wiring it into the CLI instead is exactly the
|
|
5755
|
+
// drift this suite exists to end — which `validateReadonlyFlowWrites` was
|
|
5756
|
+
// the standing proof of, until it joined the suite below.
|
|
5757
|
+
{ name: "validateActionBodyWrites", run: validateActionBodyWrites },
|
|
5758
|
+
// The third surface that writes a record field set: a flow `update_record`
|
|
5759
|
+
// node's `config.fields`. Same question as the two rules above, but the map
|
|
5760
|
+
// is structural metadata rather than parsed JS, so a finding is a certainty
|
|
5761
|
+
// and gates (`error`) — see that module for why, and why the docs' long-
|
|
5762
|
+
// standing "prefer a flow node, it's checked" advice was the least true of
|
|
5763
|
+
// the three until it landed.
|
|
5764
|
+
{ name: "validateFlowNodeWrites", run: validateFlowNodeWrites },
|
|
5765
|
+
// The OTHER question about that same `config.fields` map: not "does this
|
|
5766
|
+
// field exist?" but "is it writable?" — a `runAs:'user'` update_record
|
|
5767
|
+
// writing a static-`readonly` field is stripped by the engine and the step
|
|
5768
|
+
// still reports success (#2948/#3425). It walks the identical map the rule
|
|
5769
|
+
// above walks, so the two splitting call sites was never defensible: hand-
|
|
5770
|
+
// wired into `validate` and `compile` only, it left `os lint` PASSING a flow
|
|
5771
|
+
// `os validate` refuses — and this one gates, so the divergence shipped a
|
|
5772
|
+
// build the other command would have stopped. Joining the suite is the whole
|
|
5773
|
+
// fix; the two hand-wired call sites are deleted with it (#4345 follow-up).
|
|
5774
|
+
{ name: "validateReadonlyFlowWrites", run: validateReadonlyFlowWrites },
|
|
5775
|
+
// The `kind:'react'` page surface. Every prop a react block binds BY FIELD
|
|
5776
|
+
// NAME is resolved against the object it names (#4340) — `<ListView columns>`,
|
|
5777
|
+
// `<ObjectForm fields>`, the `record:*` family through the SAME
|
|
5778
|
+
// `COMPONENT_FIELD_SPECS` table `validatePageFieldBindings` walks one surface
|
|
5779
|
+
// over, plus `<ObjectChart>`'s aggregate/axes (#3701/#3729) and
|
|
5780
|
+
// `searchableFields` (#4329). Squarely the charter's question, on the surface
|
|
5781
|
+
// where it had no answer at all.
|
|
5782
|
+
//
|
|
5783
|
+
// It was hand-wired into `os validate` ALONE, so `os lint` and `os compile`
|
|
5784
|
+
// accepted a react page whose every field binding was stale — including the
|
|
5785
|
+
// gating ones (a missing required binding, a filter position naming no field:
|
|
5786
|
+
// the predicate can never match and the list comes back empty). That is
|
|
5787
|
+
// `validateReadonlyFlowWrites`' divergence again, one surface over, and it is
|
|
5788
|
+
// the reason this entry exists rather than a fourth hand-wiring.
|
|
5789
|
+
//
|
|
5790
|
+
// Like `validateActionBodyWrites` above, it emits ids that are not resolution
|
|
5791
|
+
// questions — `react-prop-missing-required` and `react-prop-typo` are shape,
|
|
5792
|
+
// and by the charter belong outside. They ride along for the same reason: they
|
|
5793
|
+
// fall out of the SAME TypeScript parse of the SAME page source, and splitting
|
|
5794
|
+
// them into a second member would parse every react page twice to say two
|
|
5795
|
+
// things about one walk. Lazy on the same terms as the hook/action body rules
|
|
5796
|
+
// — only a page that is actually `kind:'react'` loads the compiler.
|
|
5797
|
+
{ name: "validateReactPageProps", run: validateReactPageProps }
|
|
4868
5798
|
];
|
|
4869
5799
|
function validateReferenceIntegrity(stack) {
|
|
4870
5800
|
const findings = [];
|
|
@@ -4874,7 +5804,14 @@ function validateReferenceIntegrity(stack) {
|
|
|
4874
5804
|
return findings;
|
|
4875
5805
|
}
|
|
4876
5806
|
export {
|
|
5807
|
+
ACTION_BODY_WRITE_EXCLUSIONS,
|
|
5808
|
+
ACTION_BODY_WRITE_PATTERNS,
|
|
5809
|
+
ACTION_BODY_WRITE_PATTERN_IDS,
|
|
5810
|
+
ACTION_BODY_WRITE_UNKNOWN_FIELD,
|
|
4877
5811
|
ACTION_NAME_UNDEFINED,
|
|
5812
|
+
ACTION_RECORD_WRITE_DISCARDED,
|
|
5813
|
+
ACTION_RECORD_WRITE_PATTERNS,
|
|
5814
|
+
ACTION_RECORD_WRITE_PATTERN_IDS,
|
|
4878
5815
|
AGENT_AUTHORING_WITHDRAWN,
|
|
4879
5816
|
AI_SKILL_SURFACE_MISMATCH,
|
|
4880
5817
|
AI_SKILL_TOOL_UNRESOLVED,
|
|
@@ -4901,13 +5838,20 @@ export {
|
|
|
4901
5838
|
FIELD_GROUP_UNDECLARED,
|
|
4902
5839
|
FILTER_TOKEN_UNKNOWN,
|
|
4903
5840
|
FLOW_DRAFT_STATUS_AMBIGUOUS,
|
|
5841
|
+
FLOW_NODE_WRITE_UNKNOWN_FIELD,
|
|
4904
5842
|
FLOW_TEMPLATE_LOOKUP_TRAVERSAL,
|
|
4905
5843
|
FLOW_TEMPLATE_UNKNOWN_FIELD,
|
|
4906
5844
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
4907
5845
|
FLOW_UPDATE_READONLY_FIELD,
|
|
4908
5846
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
5847
|
+
FLOW_WRITE_NODE_TYPES,
|
|
5848
|
+
FLOW_WRITE_NODE_TYPES_DEFERRED,
|
|
4909
5849
|
FORM_COLSPAN_ABSOLUTE,
|
|
4910
5850
|
FORM_FIELD_UNKNOWN,
|
|
5851
|
+
HOOK_BODY_WRITE_EXCLUSIONS,
|
|
5852
|
+
HOOK_BODY_WRITE_PATTERNS,
|
|
5853
|
+
HOOK_BODY_WRITE_PATTERN_IDS,
|
|
5854
|
+
HOOK_BODY_WRITE_UNKNOWN_FIELD,
|
|
4911
5855
|
LIST_VIEW_FILTERS_IN_VIEWS_MODE,
|
|
4912
5856
|
MEASURE_AGGREGATE_INCOHERENT,
|
|
4913
5857
|
NAV_OBJECT_UNGRANTED,
|
|
@@ -4921,6 +5865,7 @@ export {
|
|
|
4921
5865
|
REACT_CHART_AXIS_UNKNOWN,
|
|
4922
5866
|
REACT_CHART_FIELD_UNKNOWN,
|
|
4923
5867
|
REFERENCE_INTEGRITY_RULES,
|
|
5868
|
+
SEARCHABLE_FIELD_UNKNOWN,
|
|
4924
5869
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
4925
5870
|
SECURITY_BOOK_AUDIENCE_UNKNOWN_SET,
|
|
4926
5871
|
SECURITY_DELEGATION_MISSING_REASON,
|
|
@@ -4953,6 +5898,9 @@ export {
|
|
|
4953
5898
|
WIDGET_MEASURE_UNKNOWN,
|
|
4954
5899
|
buildAccessMatrix,
|
|
4955
5900
|
diffAccessMatrix,
|
|
5901
|
+
extractHookBodyWriteSet,
|
|
5902
|
+
extractHookBodyWrites,
|
|
5903
|
+
validateActionBodyWrites,
|
|
4956
5904
|
validateActionNameRefs,
|
|
4957
5905
|
validateAiAgentAuthoring,
|
|
4958
5906
|
validateAiSurfaceAffinity,
|
|
@@ -4962,9 +5910,11 @@ export {
|
|
|
4962
5910
|
validateChartBindings,
|
|
4963
5911
|
validateDashboardActionRefs,
|
|
4964
5912
|
validateFilterTokens,
|
|
5913
|
+
validateFlowNodeWrites,
|
|
4965
5914
|
validateFlowTemplatePaths,
|
|
4966
5915
|
validateFlowTriggerReadiness,
|
|
4967
5916
|
validateFormLayout,
|
|
5917
|
+
validateHookBodyWrites,
|
|
4968
5918
|
validateJsxPages,
|
|
4969
5919
|
validateListViewMode,
|
|
4970
5920
|
validateNavAccess,
|
|
@@ -4978,6 +5928,7 @@ export {
|
|
|
4978
5928
|
validateRecordTitle,
|
|
4979
5929
|
validateReferenceIntegrity,
|
|
4980
5930
|
validateResponsiveStyles,
|
|
5931
|
+
validateSearchableFields,
|
|
4981
5932
|
validateSecurityPosture,
|
|
4982
5933
|
validateSeedReplaySafety,
|
|
4983
5934
|
validateSeedStateMachine,
|