@objectstack/lint 17.0.0-rc.4 → 17.0.0-rc.6
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 +1428 -0
- package/dist/index.cjs +1787 -624
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +324 -123
- package/dist/index.d.ts +324 -123
- package/dist/index.js +1765 -610
- package/dist/index.js.map +1 -1
- package/dist/{runtime-Cs64ShwN.d.cts → runtime-B50yywI_.d.cts} +49 -4
- package/dist/{runtime-Cs64ShwN.d.ts → runtime-B50yywI_.d.ts} +49 -4
- package/dist/runtime.cjs +1214 -428
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +1 -1
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1204 -407
- package/dist/runtime.js.map +1 -1
- package/package.json +6 -5
package/dist/index.cjs
CHANGED
|
@@ -61,10 +61,13 @@ __export(index_exports, {
|
|
|
61
61
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED: () => DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
62
62
|
DASHBOARD_ACTION_TARGET_UNDEFINED: () => DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
63
63
|
DASHBOARD_FILTER_FIELD_UNKNOWN: () => DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
64
|
+
DEFAULT_AGENT_OUTSIDE_ROSTER: () => DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
64
65
|
EXPRESSION_INVALID: () => EXPRESSION_INVALID,
|
|
65
66
|
FIELD_GROUP_EMPTY: () => FIELD_GROUP_EMPTY,
|
|
66
67
|
FIELD_GROUP_SHADOWED: () => FIELD_GROUP_SHADOWED,
|
|
67
68
|
FIELD_GROUP_UNDECLARED: () => FIELD_GROUP_UNDECLARED,
|
|
69
|
+
FILTER_EMPTY_COMBINATOR: () => FILTER_EMPTY_COMBINATOR,
|
|
70
|
+
FILTER_EMPTY_NODE: () => FILTER_EMPTY_NODE,
|
|
68
71
|
FILTER_TOKEN_UNKNOWN: () => FILTER_TOKEN_UNKNOWN,
|
|
69
72
|
FLOW_APPROVAL_REVISE_DEAD_END: () => FLOW_APPROVAL_REVISE_DEAD_END,
|
|
70
73
|
FLOW_APPROVAL_REVISE_DISABLED: () => FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -91,6 +94,7 @@ __export(index_exports, {
|
|
|
91
94
|
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE: () => FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
92
95
|
FLOW_TRIGGER_UNKNOWN_EVENT: () => FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
93
96
|
FLOW_TRIGGER_UNKNOWN_OBJECT: () => FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
97
|
+
FLOW_TRIGGER_UNROUTABLE: () => FLOW_TRIGGER_UNROUTABLE,
|
|
94
98
|
FLOW_UPDATE_READONLY_FIELD: () => FLOW_UPDATE_READONLY_FIELD,
|
|
95
99
|
FLOW_UPDATE_READONLY_WHEN_FIELD: () => FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
96
100
|
FLOW_WRITE_NODE_TYPES: () => FLOW_WRITE_NODE_TYPES,
|
|
@@ -111,19 +115,25 @@ __export(index_exports, {
|
|
|
111
115
|
NULL_GUARD_HINT: () => NULL_GUARD_HINT,
|
|
112
116
|
OBJECT_REFERENCE_UNKNOWN: () => OBJECT_REFERENCE_UNKNOWN,
|
|
113
117
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM: () => OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
118
|
+
OPEN_VOCABULARY_PROBES: () => OPEN_VOCABULARY_PROBES,
|
|
114
119
|
ORG_AXIS_CROSS_ORG_BU_GRANT: () => ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
115
120
|
ORG_AXIS_PERMISSION_INHERITANCE: () => ORG_AXIS_PERMISSION_INHERITANCE,
|
|
116
121
|
PAGE_FIELD_UNKNOWN: () => PAGE_FIELD_UNKNOWN,
|
|
117
122
|
PAGE_SOURCE_CLASSNAME: () => PAGE_SOURCE_CLASSNAME,
|
|
123
|
+
PREDICATE_PATH_UNRESOLVED: () => PREDICATE_PATH_UNRESOLVED,
|
|
124
|
+
PREDICATE_PATH_UNROOTED: () => PREDICATE_PATH_UNROOTED,
|
|
125
|
+
PRE_SEAL_PHASES: () => PRE_SEAL_PHASES,
|
|
118
126
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT: () => REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
119
127
|
REACT_CHART_AGGREGATE_INVALID: () => REACT_CHART_AGGREGATE_INVALID,
|
|
120
128
|
REACT_CHART_AXIS_UNKNOWN: () => REACT_CHART_AXIS_UNKNOWN,
|
|
121
129
|
REACT_CHART_DRILLDOWN_INVALID: () => REACT_CHART_DRILLDOWN_INVALID,
|
|
122
130
|
REACT_CHART_FIELD_UNKNOWN: () => REACT_CHART_FIELD_UNKNOWN,
|
|
123
131
|
REFERENCE_INTEGRITY_RULES: () => REFERENCE_INTEGRITY_RULES,
|
|
132
|
+
RLS_PREDICATE_OVER_BUDGET: () => RLS_PREDICATE_OVER_BUDGET,
|
|
124
133
|
RLS_PREDICATE_UNENFORCEABLE: () => RLS_PREDICATE_UNENFORCEABLE,
|
|
125
134
|
RLS_PREDICATE_UNPARSEABLE: () => RLS_PREDICATE_UNPARSEABLE,
|
|
126
135
|
RUNTIME_AJV_OPTIONS: () => RUNTIME_AJV_OPTIONS,
|
|
136
|
+
SEAL_MARKERS: () => SEAL_MARKERS,
|
|
127
137
|
SEARCHABLE_FIELD_UNKNOWN: () => SEARCHABLE_FIELD_UNKNOWN,
|
|
128
138
|
SEARCHABLE_FIELD_UNSEARCHABLE: () => SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
129
139
|
SECURITY_ANCHOR_HIGH_PRIVILEGE: () => SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
@@ -143,6 +153,9 @@ __export(index_exports, {
|
|
|
143
153
|
SEMANTIC_ROLE_FIELD_UNKNOWN: () => SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
144
154
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION: () => SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
145
155
|
SHARING_RULE_UNLOWERABLE_CONDITION: () => SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
156
|
+
STARTUP_OPEN_VOCABULARY_VERDICT: () => STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
157
|
+
STARTUP_VERDICT_ASSERTIVE_WORDING: () => STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
158
|
+
STARTUP_VERDICT_HINT: () => STARTUP_VERDICT_HINT,
|
|
146
159
|
STYLE_CLASSNAME_TAILWIND: () => STYLE_CLASSNAME_TAILWIND,
|
|
147
160
|
STYLE_NODE_MISSING_ID: () => STYLE_NODE_MISSING_ID,
|
|
148
161
|
STYLE_RESPONSIVE_NO_BASE: () => STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -164,7 +177,9 @@ __export(index_exports, {
|
|
|
164
177
|
VIEW_KEY_COLLISION: () => VIEW_KEY_COLLISION,
|
|
165
178
|
VIEW_REF_FORM_TARGET_KIND: () => VIEW_REF_FORM_TARGET_KIND,
|
|
166
179
|
VIEW_REF_FORM_TARGET_MISSING: () => VIEW_REF_FORM_TARGET_MISSING,
|
|
167
|
-
|
|
180
|
+
VISIBILITY_BARE_IDENTIFIER: () => VISIBILITY_BARE_IDENTIFIER,
|
|
181
|
+
VISIBILITY_PREDICATE_OVER_BUDGET: () => VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
182
|
+
VISIBILITY_PREDICATE_SYNTAX: () => VISIBILITY_PREDICATE_SYNTAX,
|
|
168
183
|
VISIBILITY_ROOT_MISLAYERED: () => VISIBILITY_ROOT_MISLAYERED,
|
|
169
184
|
WIDGET_DATASET_UNKNOWN: () => WIDGET_DATASET_UNKNOWN,
|
|
170
185
|
WIDGET_DIMENSION_UNKNOWN: () => WIDGET_DIMENSION_UNKNOWN,
|
|
@@ -176,6 +191,7 @@ __export(index_exports, {
|
|
|
176
191
|
diffAccessMatrix: () => diffAccessMatrix,
|
|
177
192
|
extractHookBodyWriteSet: () => extractHookBodyWriteSet,
|
|
178
193
|
extractHookBodyWrites: () => extractHookBodyWrites,
|
|
194
|
+
findStartupRegistryVerdicts: () => findStartupRegistryVerdicts,
|
|
179
195
|
findUnguardedNullableOperands: () => findUnguardedNullableOperands,
|
|
180
196
|
isSourceAuthoredPage: () => isSourceAuthoredPage,
|
|
181
197
|
lintAutonumberFormats: () => lintAutonumberFormats,
|
|
@@ -205,6 +221,7 @@ __export(index_exports, {
|
|
|
205
221
|
validateChartBindings: () => validateChartBindings,
|
|
206
222
|
validateComponentProps: () => validateComponentProps,
|
|
207
223
|
validateDashboardActionRefs: () => validateDashboardActionRefs,
|
|
224
|
+
validateEmptyCombinators: () => validateEmptyCombinators,
|
|
208
225
|
validateFilterTokens: () => validateFilterTokens,
|
|
209
226
|
validateFlowNodeWrites: () => validateFlowNodeWrites,
|
|
210
227
|
validateFlowTemplatePaths: () => validateFlowTemplatePaths,
|
|
@@ -220,6 +237,7 @@ __export(index_exports, {
|
|
|
220
237
|
validateOrgAxisRedLines: () => validateOrgAxisRedLines,
|
|
221
238
|
validatePageFieldBindings: () => validatePageFieldBindings,
|
|
222
239
|
validatePageSourceStyling: () => validatePageSourceStyling,
|
|
240
|
+
validatePredicatePathRefs: () => validatePredicatePathRefs,
|
|
223
241
|
validateReactPageProps: () => validateReactPageProps,
|
|
224
242
|
validateReactPages: () => validateReactPages,
|
|
225
243
|
validateReadonlyFlowWrites: () => validateReadonlyFlowWrites,
|
|
@@ -941,6 +959,38 @@ function validateStackExpressions(stack) {
|
|
|
941
959
|
for (const e of res.errors) issues.push({ where, message: e.message, source: e.source, severity: "error" });
|
|
942
960
|
for (const w of res.warnings) issues.push({ where, message: w.message, source: w.source, severity: "warning" });
|
|
943
961
|
};
|
|
962
|
+
const FIELD_RULE_BOUND_ROOTS = ["record", "previous", "parent"];
|
|
963
|
+
const FIELD_RULE_USER_ROOTS = ["current_user", "user", "ctx", "os"];
|
|
964
|
+
const FIELD_RULE_SLOT_CONSEQUENCE_GENERIC = "the predicate faults, and a faulting rule never produces the verdict you declared \u2014 each slot resolves the fault to its own fallback, and none of those fallbacks is yours";
|
|
965
|
+
const FIELD_RULE_SLOT_CONSEQUENCE = {
|
|
966
|
+
visibleWhen: "the predicate faults and the renderer falls back to VISIBLE (`resolveFieldRuleState` evaluates visibility with `fallback: true`, and no server-side gate evaluates a field-level `visibleWhen` at all), leaving the field the test was meant to hide showing for everyone (#6146)",
|
|
967
|
+
readonlyWhen: "the predicate faults \u2014 and the two ends fault in OPPOSITE directions. The server treats the field as LOCKED (`isReadonlyWhenLocked` will not waive a declared lock it could not evaluate, #4889) and drops your value from the payload, while the form still renders the field editable (`fallback: false`). Per ADR-0057 D10 the server is the one that decides: the field looks writable, the save reports success, and the value silently never lands",
|
|
968
|
+
requiredWhen: "the predicate faults and the requirement is never enforced anywhere \u2014 the server logs it and SKIPS the check (fail-open, #4977 deliberately did not take #4889's carve-out) and the form does not mark the field required either, so a record saves with the field empty",
|
|
969
|
+
// Listed rather than left to the `??` below, so the map covers every slot
|
|
970
|
+
// the field walk passes and the default stays unreachable. `FieldSchema`
|
|
971
|
+
// declares this key only as a `retiredKey`, which rejects it by name, so
|
|
972
|
+
// there is no fourth runtime to measure — the honest clause is the generic
|
|
973
|
+
// one, not a fabricated fourth cell (#6716).
|
|
974
|
+
conditionalRequired: FIELD_RULE_SLOT_CONSEQUENCE_GENERIC
|
|
975
|
+
};
|
|
976
|
+
const checkFieldRuleRoot = (where, slot, raw) => {
|
|
977
|
+
const source = celSourceOf(raw);
|
|
978
|
+
if (!source) return;
|
|
979
|
+
const roots = (0, import_formula2.collectCelRootIdentifiers)(source);
|
|
980
|
+
if (!roots.ok) return;
|
|
981
|
+
const kept = import_formula2.SCOPE_ROOTS.filter(
|
|
982
|
+
(r) => !FIELD_RULE_BOUND_ROOTS.includes(r) && roots.roots.includes(r)
|
|
983
|
+
);
|
|
984
|
+
if (kept.length === 0) return;
|
|
985
|
+
const root = FIELD_RULE_USER_ROOTS.find((r) => kept.includes(r)) ?? kept[0];
|
|
986
|
+
const prescription = FIELD_RULE_USER_ROOTS.includes(root) ? `To gate the CHOICES of a select by user, move the predicate to the option's own \`visibleWhen\` (\`options: [{ \u2026, visibleWhen: \u2026 }]\`) \u2014 per-option is the one \`*When\` surface that binds \`current_user\` and its ADR-0068 aliases. To hide the FIELD by role, declare field-level security on a permission set (\`fields: { '<object>.<field>': { readable: false } }\`), which the server enforces. To gate on record state, rewrite the predicate against \`record\`.` : root === "data" ? `\`data\` is the root of a METADATA form (a \`*.form\` module \u2014 the metadata row being edited); this is an OBJECT field, whose runtime form binds the row as \`record\` \u2014 one key name, two form kinds, two roots. Rewrite \`data.<key>\` as \`record.<field>\`.` : `\`${root}\` is declared platform-wide and bound at OTHER evaluation sites (flow, automation, screen and action predicates), never at the field level. Rewrite the predicate against \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item), or move the decision to a surface that binds \`${root}\`.`;
|
|
987
|
+
issues.push({
|
|
988
|
+
where,
|
|
989
|
+
message: `\`${slot}\` reads \`${root}\`, but a field-level conditional rule binds only \`record\` (plus \`previous\`, and \`parent\` on a master-detail line item) \u2014 \`${root}\` is unbound here, so ${FIELD_RULE_SLOT_CONSEQUENCE[slot] ?? FIELD_RULE_SLOT_CONSEQUENCE_GENERIC}. ` + prescription,
|
|
990
|
+
source,
|
|
991
|
+
severity: "error"
|
|
992
|
+
});
|
|
993
|
+
};
|
|
944
994
|
const checkDeclaredPredicate = (where, raw) => {
|
|
945
995
|
if (raw == null) return;
|
|
946
996
|
const res = (0, import_formula2.validateExpression)("predicate", raw);
|
|
@@ -973,7 +1023,14 @@ function validateStackExpressions(stack) {
|
|
|
973
1023
|
if (retired.length > 0) {
|
|
974
1024
|
issues.push({
|
|
975
1025
|
where: `${at} \xB7 node '${node.id}' (script) callable`,
|
|
976
|
-
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) +
|
|
1026
|
+
message: `script node carries \`${retired.map((k) => `config.${k}`).join("`, `")}\` \u2014 retired in @objectstack/spec 17 (#4343). The built-in 'email'/'slack' actions were logger-backed stubs that delivered nothing, and inline \`config.script\` was never executed. ` + (action && action !== "invoke_function" && !["email", "slack"].includes(action) ? `\`actionType: '${action}'\` named a registered function \u2014 move it to \`function: '${action}'\`. ` : `Use a \`notify\` node for mail, a \`connector_action\` (Slack connector) or \`http\` node for Slack, and a registered function for logic. `) + // #6856 route D (maintainer-ruled): the house sentence names the TOOL's
|
|
1027
|
+
// behaviour, never the retired key's fate — "rewrite it" reads two ways
|
|
1028
|
+
// over a branch that DELETES the key (template/recipients/variables/script),
|
|
1029
|
+
// "rewrite existing sources" only one. Plain-quoted (not a template literal)
|
|
1030
|
+
// so this site is a member of `retired-key-migrate-sentence.test.ts`'s
|
|
1031
|
+
// widened scan (#7030) on the same textual shape as the spec corpus — no
|
|
1032
|
+
// interpolation lives in this clause, so nothing is lost switching quote style.
|
|
1033
|
+
"Run `os migrate meta --from 16` to rewrite existing sources automatically.",
|
|
977
1034
|
source: JSON.stringify({ id: node.id, type: node.type, config: cfg })
|
|
978
1035
|
});
|
|
979
1036
|
} else if (!fn) {
|
|
@@ -1007,13 +1064,27 @@ function validateStackExpressions(stack) {
|
|
|
1007
1064
|
for (const [fname, f] of fieldList) {
|
|
1008
1065
|
for (const key of ["requiredWhen", "readonlyWhen", "conditionalRequired", "visibleWhen"]) {
|
|
1009
1066
|
check(`object '${objectName}' \xB7 field '${fname}' ${key}`, f[key], objectName, "record");
|
|
1067
|
+
checkFieldRuleRoot(`object '${objectName}' \xB7 field '${fname}' ${key}`, key, f[key]);
|
|
1068
|
+
}
|
|
1069
|
+
for (const [oi, opt] of asArray2(f.options).entries()) {
|
|
1070
|
+
const label2 = typeof opt.value === "string" ? `'${opt.value}'` : `#${oi}`;
|
|
1071
|
+
check(
|
|
1072
|
+
`object '${objectName}' \xB7 field '${fname}' option ${label2} visibleWhen`,
|
|
1073
|
+
opt.visibleWhen,
|
|
1074
|
+
objectName,
|
|
1075
|
+
"record"
|
|
1076
|
+
);
|
|
1010
1077
|
}
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1078
|
+
for (const [slot, raw, consequence2] of [
|
|
1079
|
+
["readonlyWhen", f.readonlyWhen, `the field would be locked on every write`],
|
|
1080
|
+
["requiredWhen", f.requiredWhen, `the requirement would never be enforced \u2014 the predicate faults, the server logs and skips it, and the field stays optional in the database`]
|
|
1081
|
+
]) {
|
|
1082
|
+
const source = celSourceOf(raw);
|
|
1083
|
+
if (masters === 1 || !source || !readsParentRoot(source)) continue;
|
|
1013
1084
|
issues.push({
|
|
1014
|
-
where: `object '${objectName}' \xB7 field '${fname}'
|
|
1015
|
-
message:
|
|
1016
|
-
source
|
|
1085
|
+
where: `object '${objectName}' \xB7 field '${fname}' ${slot}`,
|
|
1086
|
+
message: `\`${slot}\` reads \`parent\`, but object '${objectName}' declares ${masters === 0 ? "no" : `${masters}`} \`master_detail\` relationship${masters === 1 ? "" : "s"} \u2014 so the server has no header record to bind as \`parent\` and ${consequence2}. ` + (masters === 0 ? `Declare the owning relationship as \`Field.masterDetail('<master>')\`, or rewrite the predicate against \`record\`.` : `\`parent\` needs exactly one master; name the header explicitly through \`record.<fk>\` state instead, or model the extra relationship as a \`lookup\`.`),
|
|
1087
|
+
source,
|
|
1017
1088
|
severity: "error"
|
|
1018
1089
|
});
|
|
1019
1090
|
}
|
|
@@ -1106,6 +1177,348 @@ function validateStackExpressions(stack) {
|
|
|
1106
1177
|
return issues;
|
|
1107
1178
|
}
|
|
1108
1179
|
|
|
1180
|
+
// src/lint-startup-registry-verdict.ts
|
|
1181
|
+
var import_node_module = require("module");
|
|
1182
|
+
var import_meta = {};
|
|
1183
|
+
var cachedTs = null;
|
|
1184
|
+
function loadTypeScript() {
|
|
1185
|
+
if (cachedTs) return cachedTs;
|
|
1186
|
+
const anchor = typeof import_meta !== "undefined" && import_meta.url ? import_meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
1187
|
+
try {
|
|
1188
|
+
cachedTs = (0, import_node_module.createRequire)(anchor)("typescript");
|
|
1189
|
+
} catch (err) {
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`@objectstack/lint: checking plugin source for startup registry verdicts 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 plugin source is actually checked.`
|
|
1192
|
+
);
|
|
1193
|
+
}
|
|
1194
|
+
return cachedTs;
|
|
1195
|
+
}
|
|
1196
|
+
var STARTUP_OPEN_VOCABULARY_VERDICT = "startup-open-vocabulary-verdict";
|
|
1197
|
+
var STARTUP_VERDICT_ASSERTIVE_WORDING = "startup-verdict-assertive-wording";
|
|
1198
|
+
var OPEN_VOCABULARY_PROBES = /* @__PURE__ */ new Map([
|
|
1199
|
+
[
|
|
1200
|
+
"getRegisteredNodeTypes",
|
|
1201
|
+
"ADR-0018 keeps the flow node-type vocabulary open and runtime-extensible \u2014 a plugin registers its executor from its own init()/start(), which can be after this line (#4771)"
|
|
1202
|
+
],
|
|
1203
|
+
[
|
|
1204
|
+
"knownNodeTypes",
|
|
1205
|
+
"ADR-0018 keeps the flow node-type vocabulary open and runtime-extensible \u2014 a plugin registers its executor from its own init()/start(), which can be after this line (#4771)"
|
|
1206
|
+
],
|
|
1207
|
+
[
|
|
1208
|
+
"getUnknownNodeTypeAudit",
|
|
1209
|
+
'the unknown-node-type audit is a snapshot of an OPEN vocabulary a plugin can still extend during boot; it is read-only by design and answers "unknown as of now", never "unknown for this deployment" (#4771)'
|
|
1210
|
+
],
|
|
1211
|
+
[
|
|
1212
|
+
"getActionDescriptors",
|
|
1213
|
+
'ADR-0018 action descriptors are published by plugins during boot, so a type missing from them here means "not published YET" (#4771)'
|
|
1214
|
+
],
|
|
1215
|
+
[
|
|
1216
|
+
"getRegisteredExecutors",
|
|
1217
|
+
"executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
|
|
1218
|
+
],
|
|
1219
|
+
[
|
|
1220
|
+
"listExecutors",
|
|
1221
|
+
"executors are contributed by plugins during boot \u2014 an executor missing here may simply be a plugin that has not started"
|
|
1222
|
+
],
|
|
1223
|
+
[
|
|
1224
|
+
"getRegisteredTools",
|
|
1225
|
+
"AI tools are contributed by plugins during boot, so the registered set is not final until every plugin has started"
|
|
1226
|
+
],
|
|
1227
|
+
[
|
|
1228
|
+
"listConnectors",
|
|
1229
|
+
"connectors are contributed by plugins during boot (StackSchema.connectors documents provider-bound instances registered by plugins), so the list is not final until every plugin has started"
|
|
1230
|
+
],
|
|
1231
|
+
[
|
|
1232
|
+
"getRegisteredConnectors",
|
|
1233
|
+
"connectors are contributed by plugins during boot, so the registered set is not final until every plugin has started"
|
|
1234
|
+
],
|
|
1235
|
+
[
|
|
1236
|
+
"listCapabilities",
|
|
1237
|
+
"ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
|
|
1238
|
+
],
|
|
1239
|
+
[
|
|
1240
|
+
"getRegisteredCapabilities",
|
|
1241
|
+
"ADR-0066 capabilities are declared by every installed package, so the set is not final until every plugin has contributed its declarations"
|
|
1242
|
+
],
|
|
1243
|
+
[
|
|
1244
|
+
"listProviders",
|
|
1245
|
+
"providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
|
|
1246
|
+
],
|
|
1247
|
+
[
|
|
1248
|
+
"getRegisteredProviders",
|
|
1249
|
+
"providers are contributed by plugins during boot \u2014 a provider missing here may simply be a plugin that has not started"
|
|
1250
|
+
]
|
|
1251
|
+
]);
|
|
1252
|
+
var PRE_SEAL_PHASES = /* @__PURE__ */ new Map([
|
|
1253
|
+
["constructor", "the constructor runs at composition time \u2014 before ANY plugin has been initialized"],
|
|
1254
|
+
["init", "another plugin's init() may not have run yet (ADR-0116), so the vocabulary is still filling"],
|
|
1255
|
+
[
|
|
1256
|
+
"start",
|
|
1257
|
+
"sibling plugins' start() have not all run, and ADR-0018 lets a plugin register its contributions from start() \u2014 the vocabulary is still filling"
|
|
1258
|
+
]
|
|
1259
|
+
]);
|
|
1260
|
+
var SEAL_MARKERS = ["seal"];
|
|
1261
|
+
var VERDICT_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal"]);
|
|
1262
|
+
var ALL_LEVELS = /* @__PURE__ */ new Set(["warn", "error", "fatal", "info", "debug", "trace", "log"]);
|
|
1263
|
+
var PERSISTENCE_CALLEES = /* @__PURE__ */ new Set(["insert", "insertOne", "update", "updateOne", "upsert", "save", "saveMetaItem"]);
|
|
1264
|
+
var ASSERTIVE_PHRASES = [
|
|
1265
|
+
"will fail",
|
|
1266
|
+
"fails at execution",
|
|
1267
|
+
"nothing will register",
|
|
1268
|
+
"no plugin provides",
|
|
1269
|
+
"is not installed",
|
|
1270
|
+
"is not configured",
|
|
1271
|
+
"you need",
|
|
1272
|
+
"you must install",
|
|
1273
|
+
"must be provisioned"
|
|
1274
|
+
];
|
|
1275
|
+
var HEDGE_PHRASES = [
|
|
1276
|
+
"not yet",
|
|
1277
|
+
"yet been registered",
|
|
1278
|
+
"so far",
|
|
1279
|
+
"as of",
|
|
1280
|
+
"may still",
|
|
1281
|
+
"might still",
|
|
1282
|
+
"still be registered",
|
|
1283
|
+
"has started",
|
|
1284
|
+
"have started"
|
|
1285
|
+
];
|
|
1286
|
+
var STARTUP_VERDICT_HINT = 'Take one of the three shapes the fixes took: (1) resolve where the value is USED, not where you start \u2014 a lazy accessor or a `kernel:ready`/`kernel:bootstrapped` hook sees a provider that registered later (`createLazyCacheRateLimitStorage()` in plugin-auth, #4772); (2) seal the vocabulary, then judge \u2014 have the host declare the moment it can no longer grow and draw the conclusion there (`AutomationEngine.sealNodeTypeVocabulary()`, called at `kernel:bootstrapped`, #4771); (3) order the verdict AFTER the mutation it describes, so it cannot attest to a state this same boot goes on to contradict (the ADR-0104 attestation, #4769). If the conclusion must stay here, keep the two worlds apart in the wording: "no executor registered YET (as of plugin start)" is true; "will fail at execution time" is a claim about a world that has not finished forming.';
|
|
1287
|
+
function isFunctionLike(t, node) {
|
|
1288
|
+
return t.isFunctionDeclaration(node) || t.isFunctionExpression(node) || t.isArrowFunction(node) || t.isMethodDeclaration(node) || t.isConstructorDeclaration(node) || t.isGetAccessorDeclaration(node) || t.isSetAccessorDeclaration(node);
|
|
1289
|
+
}
|
|
1290
|
+
function walkSameTick(t, node, visit) {
|
|
1291
|
+
node.forEachChild((child) => {
|
|
1292
|
+
if (isFunctionLike(t, child) || t.isClassDeclaration(child) || t.isClassExpression(child)) return;
|
|
1293
|
+
visit(child);
|
|
1294
|
+
walkSameTick(t, child, visit);
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
function walkAll(t, node, visit) {
|
|
1298
|
+
node.forEachChild((child) => {
|
|
1299
|
+
visit(child);
|
|
1300
|
+
walkAll(t, child, visit);
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
function calleeName(t, node) {
|
|
1304
|
+
if (!t.isCallExpression(node)) return void 0;
|
|
1305
|
+
const expr = node.expression;
|
|
1306
|
+
if (t.isIdentifier(expr)) return expr.text;
|
|
1307
|
+
if (t.isPropertyAccessExpression(expr) && t.isIdentifier(expr.name)) return expr.name.text;
|
|
1308
|
+
return void 0;
|
|
1309
|
+
}
|
|
1310
|
+
function loggerLevel(t, node) {
|
|
1311
|
+
if (!t.isCallExpression(node)) return void 0;
|
|
1312
|
+
const expr = node.expression;
|
|
1313
|
+
if (!t.isPropertyAccessExpression(expr) || !t.isIdentifier(expr.name)) return void 0;
|
|
1314
|
+
const level = expr.name.text;
|
|
1315
|
+
if (!ALL_LEVELS.has(level)) return void 0;
|
|
1316
|
+
const receiver = expr.expression;
|
|
1317
|
+
let receiverName;
|
|
1318
|
+
if (t.isIdentifier(receiver)) receiverName = receiver.text;
|
|
1319
|
+
else if (t.isPropertyAccessExpression(receiver) && t.isIdentifier(receiver.name)) receiverName = receiver.name.text;
|
|
1320
|
+
if (!receiverName) return void 0;
|
|
1321
|
+
return /^(logger|log|console)$/i.test(receiverName) ? level : void 0;
|
|
1322
|
+
}
|
|
1323
|
+
function literalText(t, node) {
|
|
1324
|
+
const parts = [];
|
|
1325
|
+
const take = (n) => {
|
|
1326
|
+
if (t.isStringLiteralLike(n)) parts.push(n.text);
|
|
1327
|
+
else if (t.isTemplateHead(n) || t.isTemplateMiddle(n) || t.isTemplateTail(n)) parts.push(n.text);
|
|
1328
|
+
};
|
|
1329
|
+
take(node);
|
|
1330
|
+
walkAll(t, node, take);
|
|
1331
|
+
return parts.join(" ").toLowerCase();
|
|
1332
|
+
}
|
|
1333
|
+
function mentionsSeal(t, node) {
|
|
1334
|
+
let found = false;
|
|
1335
|
+
const visit = (n) => {
|
|
1336
|
+
if (found) return;
|
|
1337
|
+
if (t.isIdentifier(n)) {
|
|
1338
|
+
const lower = n.text.toLowerCase();
|
|
1339
|
+
if (SEAL_MARKERS.some((m) => lower.includes(m))) found = true;
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
visit(node);
|
|
1343
|
+
walkAll(t, node, visit);
|
|
1344
|
+
return found;
|
|
1345
|
+
}
|
|
1346
|
+
function moduleLevelMutableBindings(t, sf) {
|
|
1347
|
+
const names = /* @__PURE__ */ new Set();
|
|
1348
|
+
for (const st of sf.statements) {
|
|
1349
|
+
if (!t.isVariableStatement(st)) continue;
|
|
1350
|
+
if (st.declarationList.flags & t.NodeFlags.Const) continue;
|
|
1351
|
+
for (const d of st.declarationList.declarations) {
|
|
1352
|
+
if (t.isIdentifier(d.name)) names.add(d.name.text);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
return names;
|
|
1356
|
+
}
|
|
1357
|
+
function indexFunctionBodies(t, sf) {
|
|
1358
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1359
|
+
walkAll(t, sf, (node) => {
|
|
1360
|
+
if (t.isFunctionDeclaration(node) && node.name && node.body) byName.set(node.name.text, node.body);
|
|
1361
|
+
else if (t.isMethodDeclaration(node) && t.isIdentifier(node.name) && node.body) byName.set(node.name.text, node.body);
|
|
1362
|
+
else if (t.isVariableDeclaration(node) && t.isIdentifier(node.name) && node.initializer && (t.isArrowFunction(node.initializer) || t.isFunctionExpression(node.initializer)) && node.initializer.body) {
|
|
1363
|
+
byName.set(node.name.text, node.initializer.body);
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
return byName;
|
|
1367
|
+
}
|
|
1368
|
+
function collectLifecycleUnits(t, sf) {
|
|
1369
|
+
const units = [];
|
|
1370
|
+
const readClass = (node) => {
|
|
1371
|
+
const phases = [];
|
|
1372
|
+
let hasLifecycle = false;
|
|
1373
|
+
for (const member of node.members) {
|
|
1374
|
+
if (t.isMethodDeclaration(member) && t.isIdentifier(member.name)) {
|
|
1375
|
+
if (member.name.text === "init" || member.name.text === "start") hasLifecycle = true;
|
|
1376
|
+
} else if (t.isPropertyDeclaration(member) && t.isIdentifier(member.name) && (member.name.text === "init" || member.name.text === "start") && member.initializer && (t.isArrowFunction(member.initializer) || t.isFunctionExpression(member.initializer))) {
|
|
1377
|
+
hasLifecycle = true;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
if (!hasLifecycle) return;
|
|
1381
|
+
for (const member of node.members) {
|
|
1382
|
+
if (t.isConstructorDeclaration(member) && member.body) {
|
|
1383
|
+
phases.push({ phase: "constructor", body: member.body });
|
|
1384
|
+
} else if (t.isMethodDeclaration(member) && t.isIdentifier(member.name) && member.body) {
|
|
1385
|
+
if (PRE_SEAL_PHASES.has(member.name.text)) phases.push({ phase: member.name.text, body: member.body });
|
|
1386
|
+
} else if (t.isPropertyDeclaration(member) && t.isIdentifier(member.name) && PRE_SEAL_PHASES.has(member.name.text) && member.initializer && (t.isArrowFunction(member.initializer) || t.isFunctionExpression(member.initializer)) && member.initializer.body) {
|
|
1387
|
+
phases.push({ phase: member.name.text, body: member.initializer.body });
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (phases.length) units.push({ label: node.name?.text ?? "<anonymous class>", phases });
|
|
1391
|
+
};
|
|
1392
|
+
const readObjectLiteral = (node) => {
|
|
1393
|
+
const phases = [];
|
|
1394
|
+
let name;
|
|
1395
|
+
let hasLifecycle = false;
|
|
1396
|
+
for (const prop of node.properties) {
|
|
1397
|
+
if (!t.isPropertyAssignment(prop) || !t.isIdentifier(prop.name)) continue;
|
|
1398
|
+
const key = prop.name.text;
|
|
1399
|
+
if (key === "name" && t.isStringLiteralLike(prop.initializer)) name = prop.initializer.text;
|
|
1400
|
+
if ((key === "init" || key === "start") && isFunctionLike(t, prop.initializer)) hasLifecycle = true;
|
|
1401
|
+
if (PRE_SEAL_PHASES.has(key) && (t.isArrowFunction(prop.initializer) || t.isFunctionExpression(prop.initializer)) && prop.initializer.body) {
|
|
1402
|
+
phases.push({ phase: key, body: prop.initializer.body });
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (!name || !hasLifecycle || !phases.length) return;
|
|
1406
|
+
units.push({ label: `${name} (object plugin)`, phases });
|
|
1407
|
+
};
|
|
1408
|
+
walkAll(t, sf, (node) => {
|
|
1409
|
+
if (t.isClassDeclaration(node) || t.isClassExpression(node)) readClass(node);
|
|
1410
|
+
else if (t.isObjectLiteralExpression(node)) readObjectLiteral(node);
|
|
1411
|
+
});
|
|
1412
|
+
return units;
|
|
1413
|
+
}
|
|
1414
|
+
function findStartupRegistryVerdicts(source, options = {}) {
|
|
1415
|
+
if (!source || !source.trim()) return [];
|
|
1416
|
+
let mentionsAny = false;
|
|
1417
|
+
for (const probe of OPEN_VOCABULARY_PROBES.keys()) {
|
|
1418
|
+
if (source.includes(probe)) {
|
|
1419
|
+
mentionsAny = true;
|
|
1420
|
+
break;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (!mentionsAny) return [];
|
|
1424
|
+
const t = loadTypeScript();
|
|
1425
|
+
const fileLabel = options.file ?? "source";
|
|
1426
|
+
let sf;
|
|
1427
|
+
try {
|
|
1428
|
+
sf = t.createSourceFile(fileLabel, source, t.ScriptTarget.Latest, true, t.ScriptKind.TS);
|
|
1429
|
+
} catch {
|
|
1430
|
+
return [];
|
|
1431
|
+
}
|
|
1432
|
+
const findings = [];
|
|
1433
|
+
const moduleBindings = moduleLevelMutableBindings(t, sf);
|
|
1434
|
+
const functionBodies = indexFunctionBodies(t, sf);
|
|
1435
|
+
const lineOf = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
1436
|
+
for (const unit of collectLifecycleUnits(t, sf)) {
|
|
1437
|
+
for (const { phase, body } of unit.phases) {
|
|
1438
|
+
const scopes = [];
|
|
1439
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
1440
|
+
const collect = (b, depth) => {
|
|
1441
|
+
scopes.push(b);
|
|
1442
|
+
if (depth >= 2) return;
|
|
1443
|
+
walkSameTick(t, b, (child) => {
|
|
1444
|
+
const name = calleeName(t, child);
|
|
1445
|
+
if (!name || seenNames.has(name)) return;
|
|
1446
|
+
const helper = functionBodies.get(name);
|
|
1447
|
+
if (!helper) return;
|
|
1448
|
+
seenNames.add(name);
|
|
1449
|
+
collect(helper, depth + 1);
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
collect(body, 0);
|
|
1453
|
+
const reads = [];
|
|
1454
|
+
for (const scope of scopes) {
|
|
1455
|
+
walkSameTick(t, scope, (node) => {
|
|
1456
|
+
const name = calleeName(t, node);
|
|
1457
|
+
if (name && OPEN_VOCABULARY_PROBES.has(name)) reads.push({ probe: name, node });
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
if (!reads.length) continue;
|
|
1461
|
+
if (scopes.some((scope) => mentionsSeal(t, scope))) continue;
|
|
1462
|
+
const records = [];
|
|
1463
|
+
for (const scope of scopes) {
|
|
1464
|
+
walkSameTick(t, scope, (node) => {
|
|
1465
|
+
const level = loggerLevel(t, node);
|
|
1466
|
+
if (level && VERDICT_LEVELS.has(level)) {
|
|
1467
|
+
records.push({ kind: "announced", detail: `${level} log`, line: lineOf(node), node });
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const cn = calleeName(t, node);
|
|
1471
|
+
if (cn && PERSISTENCE_CALLEES.has(cn)) {
|
|
1472
|
+
records.push({ kind: "persisted", detail: `${cn}()`, line: lineOf(node), node });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (t.isBinaryExpression(node) && node.operatorToken.kind === t.SyntaxKind.EqualsToken) {
|
|
1476
|
+
const lhs = node.left;
|
|
1477
|
+
let target;
|
|
1478
|
+
if (t.isPropertyAccessExpression(lhs) && t.isIdentifier(lhs.name)) {
|
|
1479
|
+
target = lhs.expression.kind === t.SyntaxKind.ThisKeyword ? `this.${lhs.name.text}` : `<obj>.${lhs.name.text}`;
|
|
1480
|
+
} else if (t.isIdentifier(lhs) && moduleBindings.has(lhs.text)) {
|
|
1481
|
+
target = `module-level \`${lhs.text}\``;
|
|
1482
|
+
}
|
|
1483
|
+
if (target) records.push({ kind: "cached", detail: target, line: lineOf(node), node });
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
if (!records.length) continue;
|
|
1488
|
+
const read = reads[0];
|
|
1489
|
+
const record = records[0];
|
|
1490
|
+
const where = `${unit.label}.${phase}${phase === "constructor" ? "" : "()"}`;
|
|
1491
|
+
const note = OPEN_VOCABULARY_PROBES.get(read.probe);
|
|
1492
|
+
const phaseNote = PRE_SEAL_PHASES.get(phase);
|
|
1493
|
+
findings.push({
|
|
1494
|
+
severity: "warning",
|
|
1495
|
+
rule: STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
1496
|
+
where,
|
|
1497
|
+
path: `${fileLabel}:${lineOf(read.node)}`,
|
|
1498
|
+
message: `\`${read.probe}()\` reads a vocabulary that is still filling, and the conclusion is recorded (${record.kind}: ${record.detail}, line ${record.line}). ${note}; ${phaseNote}. "absent" here has two meanings the recorded verdict cannot tell apart \u2014 no provider in this deployment, or a provider that registers later in this same boot \u2014 and nothing retracts the record when the second one turns out to be the case (#4771 / #4772).`,
|
|
1499
|
+
hint: STARTUP_VERDICT_HINT
|
|
1500
|
+
});
|
|
1501
|
+
for (const rec of records) {
|
|
1502
|
+
if (rec.kind !== "announced") continue;
|
|
1503
|
+
const text = literalText(t, rec.node);
|
|
1504
|
+
if (!text) continue;
|
|
1505
|
+
if (HEDGE_PHRASES.some((h) => text.includes(h))) continue;
|
|
1506
|
+
const hit = ASSERTIVE_PHRASES.find((p) => text.includes(p));
|
|
1507
|
+
if (!hit) continue;
|
|
1508
|
+
findings.push({
|
|
1509
|
+
severity: "warning",
|
|
1510
|
+
rule: STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
1511
|
+
where,
|
|
1512
|
+
path: `${fileLabel}:${rec.line}`,
|
|
1513
|
+
message: `the diagnostic says "${hit}" about a vocabulary that can still grow during this boot. #4771 printed "will fail at execution time" for eight approval flows 0.8s before the executor that runs them was registered, and a deployment that genuinely lacked the plugin printed the identical eight \u2014 so the line could not tell an operator which of the two they had. #4772's remedy ("you need Redis") sent operators to fix a problem they did not have, and connecting Redis did not change the message.`,
|
|
1514
|
+
hint: STARTUP_VERDICT_HINT
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
return findings;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1109
1522
|
// src/validate-list-view-mode.ts
|
|
1110
1523
|
var LIST_VIEW_FILTERS_IN_VIEWS_MODE = "list-view-filters-in-views-mode";
|
|
1111
1524
|
function asArray3(v) {
|
|
@@ -1245,6 +1658,7 @@ var FLOW_DRAFT_STATUS_AMBIGUOUS = "flow-draft-status-ambiguous";
|
|
|
1245
1658
|
var FLOW_TRIGGER_UNKNOWN_EVENT = "flow-trigger-unknown-event";
|
|
1246
1659
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_INVALID = "flow-time-relative-descriptor-invalid";
|
|
1247
1660
|
var FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE = "flow-time-relative-descriptor-unroutable";
|
|
1661
|
+
var FLOW_TRIGGER_UNROUTABLE = "flow-trigger-unroutable";
|
|
1248
1662
|
var VALID_RECORD_TRIGGER = /^record-(?:before|after)-(?:create|insert|update|delete|write)$/;
|
|
1249
1663
|
function asArray4(v) {
|
|
1250
1664
|
if (Array.isArray(v)) return v;
|
|
@@ -1262,6 +1676,11 @@ function renderNonObject(v) {
|
|
|
1262
1676
|
if (t === "bigint") return `${String(v)}n (a bigint)`;
|
|
1263
1677
|
return `a ${t}`;
|
|
1264
1678
|
}
|
|
1679
|
+
function renderTriggerToken(v) {
|
|
1680
|
+
if (typeof v === "string") return `'${v}'`;
|
|
1681
|
+
const json = JSON.stringify(v);
|
|
1682
|
+
return json === void 0 ? `a ${typeof v}` : json;
|
|
1683
|
+
}
|
|
1265
1684
|
function startNodeOf(flow) {
|
|
1266
1685
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
1267
1686
|
const index = nodes.findIndex((n) => n?.type === "start");
|
|
@@ -1381,6 +1800,25 @@ function validateFlowTriggerReadiness(stack) {
|
|
|
1381
1800
|
hint: `config.timeRelative describes WHICH records to sweep \u2014 an object: { object, dateField, and exactly one of withinDays | offsetDays } (plus optional filter / maxRecords). A cadence like 'daily' is not a descriptor: HOW OFTEN the sweep runs is the sibling key config.schedule on the same start node (it defaults to daily, so it is usually omitted). See TimeRelativeTriggerSchema and content/docs/references/automation/time-relative-trigger.mdx.`
|
|
1382
1801
|
});
|
|
1383
1802
|
}
|
|
1803
|
+
const routesToSomeTrigger = isRecordTriggered2 || isArrayRecordTriggered || isTimeRelative || config.schedule != null || flow.type === "schedule" || flow.type === "api" || triggerType === "api";
|
|
1804
|
+
if (start && flow.type === "record_change" && !routesToSomeTrigger) {
|
|
1805
|
+
const hasTriggerType = config.triggerType != null;
|
|
1806
|
+
findings.push({
|
|
1807
|
+
// `error` (#5762's criterion, applied to a fourth id). The verdict is
|
|
1808
|
+
// the engine's own routing chain — literal `startsWith`/`typeof` tests
|
|
1809
|
+
// with no registry lookup in them — so no installed package can make
|
|
1810
|
+
// this token resolve. `registerTrigger` is keyed by the RESOLVED type,
|
|
1811
|
+
// which is the near-miss worth stating: a plugin can supply the
|
|
1812
|
+
// record-change trigger itself, and it still would not help, because
|
|
1813
|
+
// the flow never reaches the point of asking for one.
|
|
1814
|
+
severity: "error",
|
|
1815
|
+
rule: FLOW_TRIGGER_UNROUTABLE,
|
|
1816
|
+
where: `flow "${flowName}" \u203A start node`,
|
|
1817
|
+
path: `flows[${flowIndex}].nodes[${start.index}].config.triggerType`,
|
|
1818
|
+
message: `declares type: 'record_change' but ` + (hasTriggerType ? `its start node's triggerType is ${renderTriggerToken(config.triggerType)}, which the engine routes to NO trigger` : `its start node has no triggerType at all, so there is nothing for the engine to route`) + ` \u2014 it binds a record-change flow only for a token starting with 'record-', so this flow is demoted to a manual one and never fires. Nothing NAMES it: the unbound-flow audit resolves the same binding and skips the flow as "manual \u2014 nothing to bind", so neither the boot warning nor the startup summary lists it; the only trace is the banner's flow count being one higher than its bound count.`,
|
|
1819
|
+
hint: `Use record-{before,after}-{create,update,delete,write} ('write' is create OR update in one flow, #3427; create/insert are synonyms). If the flow really is launched by hand or from a screen, declare type: 'autolaunched' or 'screen' instead of 'record_change' \u2014 those types have no trigger to be missing.`
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1384
1822
|
if (isAutoTriggered && (flow.status == null || flow.status === "draft")) {
|
|
1385
1823
|
findings.push({
|
|
1386
1824
|
severity: "warning",
|
|
@@ -2150,14 +2588,14 @@ function validateJsxPages(stack, opts = {}) {
|
|
|
2150
2588
|
}
|
|
2151
2589
|
|
|
2152
2590
|
// src/validate-react-pages.ts
|
|
2153
|
-
var
|
|
2154
|
-
var
|
|
2591
|
+
var import_node_module2 = require("module");
|
|
2592
|
+
var import_meta2 = {};
|
|
2155
2593
|
var cachedTransform = null;
|
|
2156
2594
|
function loadSucraseTransform() {
|
|
2157
2595
|
if (cachedTransform) return cachedTransform;
|
|
2158
|
-
const anchor = typeof
|
|
2596
|
+
const anchor = typeof import_meta2 !== "undefined" && import_meta2.url ? import_meta2.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
2159
2597
|
try {
|
|
2160
|
-
cachedTransform = (0,
|
|
2598
|
+
cachedTransform = (0, import_node_module2.createRequire)(anchor)("sucrase").transform;
|
|
2161
2599
|
} catch (err) {
|
|
2162
2600
|
throw new Error(
|
|
2163
2601
|
`@objectstack/lint: validating a kind:'react' page requires the "sucrase" 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 "sucrase" in the image; it is only loaded when a react-source page is validated.`
|
|
@@ -2204,7 +2642,7 @@ function validateReactPages(stack) {
|
|
|
2204
2642
|
}
|
|
2205
2643
|
|
|
2206
2644
|
// src/validate-react-page-props.ts
|
|
2207
|
-
var
|
|
2645
|
+
var import_node_module3 = require("module");
|
|
2208
2646
|
var import_ui2 = require("@objectstack/spec/ui");
|
|
2209
2647
|
var import_data4 = require("@objectstack/spec/data");
|
|
2210
2648
|
|
|
@@ -2323,7 +2761,19 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2323
2761
|
where,
|
|
2324
2762
|
path: `${path}[${i}]`,
|
|
2325
2763
|
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)),
|
|
2326
|
-
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
|
|
2764
|
+
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 stored text 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(", ")}.` : "")
|
|
2765
|
+
});
|
|
2766
|
+
continue;
|
|
2767
|
+
}
|
|
2768
|
+
if ((0, import_data3.isVirtualSearchField)(target.fields[name])) {
|
|
2769
|
+
const vtype = target.fields[name]?.type;
|
|
2770
|
+
findings.push({
|
|
2771
|
+
severity: "error",
|
|
2772
|
+
rule: SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
2773
|
+
where,
|
|
2774
|
+
path: `${path}[${i}]`,
|
|
2775
|
+
message: `${subject} entry "${name}" on object "${objectName}" is a virtual '${vtype}' field: its value is computed on read and never stored, so no driver materializes a column for 'search' to scan and the entry can never match. It reads as search coverage and delivers none \u2014 the runtime used to admit it verbatim because the declaration named it (#6674).`,
|
|
2776
|
+
hint: `Mirror the computed value onto a stored text field on "${objectName}" and declare that instead, or drop "${name}". At runtime the ingress gate now refuses this entry with 400 INVALID_FIELD, the same answer a stale entry gets (#4254).`
|
|
2327
2777
|
});
|
|
2328
2778
|
continue;
|
|
2329
2779
|
}
|
|
@@ -2358,7 +2808,7 @@ function checkSearchableFieldList(declared, objectName, fieldsByObject, where, p
|
|
|
2358
2808
|
where,
|
|
2359
2809
|
path: `${path}[${i}]`,
|
|
2360
2810
|
message: `${subject} entry "${name}" on object "${objectName}" is ${why}. With no 'searchableFields' declared on the object, 'search' scans its text-like columns (${[...import_data3.SEARCHABLE_TEXTUAL_TYPES, ...import_data3.SEARCHABLE_ENUM_TYPES].join(" / ")}). Clients echo this declaration verbatim as the '$searchFields' override, and the runtime refuses it: every toolbar search on this list returns 400 INVALID_FIELD (#4254).`,
|
|
2361
|
-
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a text
|
|
2811
|
+
hint: (isReference ? `A ${meta?.type} column stores only the referenced record's id, so it cannot be a keyword target \u2014 drop "${name}" from this view and, to search by the related record's title, mirror it onto a stored text field here and declare that instead. ` : `Drop "${name}" from this view, or target a text-like field instead. `) + `Declaring 'searchableFields' on object "${objectName}" chooses the searchable set explicitly.`
|
|
2362
2812
|
});
|
|
2363
2813
|
}
|
|
2364
2814
|
return findings;
|
|
@@ -2558,8 +3008,21 @@ var COMPONENT_FIELD_SPECS = {
|
|
|
2558
3008
|
"element:number": { props: ["field"] },
|
|
2559
3009
|
"element:filter": { props: ["fields"] },
|
|
2560
3010
|
"element:form": { props: ["fields"] },
|
|
2561
|
-
//
|
|
2562
|
-
|
|
3011
|
+
// `labelField` is the one field-bearing prop this element declares. Its former
|
|
3012
|
+
// companions `displayField` (renamed to `labelField`, ADR-0087 D2) and
|
|
3013
|
+
// `searchFields` (deleted, ADR-0049) were retired in #5775 and are
|
|
3014
|
+
// `retiredKey()` tombstones on `ElementRecordPickerPropsSchema` — so no
|
|
3015
|
+
// spec-conformant page carries either, and this rule's job (resolve a field
|
|
3016
|
+
// NAME against the object) is not the question a retired key raises (#6629).
|
|
3017
|
+
//
|
|
3018
|
+
// A non-conformant page that writes one anyway is not left unattended: the
|
|
3019
|
+
// #5068 props gate reports the key with its rename/delete prescription. That
|
|
3020
|
+
// gate is advisory and CLI-only and lives in a different registry
|
|
3021
|
+
// (`authoring-rules`) from this suite, so it neither precedes nor suppresses
|
|
3022
|
+
// this rule — what these two entries actually added was a SECOND finding,
|
|
3023
|
+
// saying a field named by a key that no longer exists does not exist either.
|
|
3024
|
+
// The prescription is the useful half; this half was noise on top of it.
|
|
3025
|
+
"element:record_picker": { props: ["labelField"] }
|
|
2563
3026
|
};
|
|
2564
3027
|
var RELATED_LIST_TYPE = "record:related_list";
|
|
2565
3028
|
function componentFieldRefs(type, props, basePath, sep = ".") {
|
|
@@ -2719,19 +3182,19 @@ function describeIssue(issue, root, depth = 0) {
|
|
|
2719
3182
|
}
|
|
2720
3183
|
|
|
2721
3184
|
// src/validate-react-page-props.ts
|
|
2722
|
-
var
|
|
2723
|
-
var
|
|
2724
|
-
function
|
|
2725
|
-
if (
|
|
2726
|
-
const anchor = typeof
|
|
3185
|
+
var import_meta3 = {};
|
|
3186
|
+
var cachedTs2 = null;
|
|
3187
|
+
function loadTypeScript2() {
|
|
3188
|
+
if (cachedTs2) return cachedTs2;
|
|
3189
|
+
const anchor = typeof import_meta3 !== "undefined" && import_meta3.url ? import_meta3.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
2727
3190
|
try {
|
|
2728
|
-
|
|
3191
|
+
cachedTs2 = (0, import_node_module3.createRequire)(anchor)("typescript");
|
|
2729
3192
|
} catch (err) {
|
|
2730
3193
|
throw new Error(
|
|
2731
3194
|
`@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.`
|
|
2732
3195
|
);
|
|
2733
3196
|
}
|
|
2734
|
-
return
|
|
3197
|
+
return cachedTs2;
|
|
2735
3198
|
}
|
|
2736
3199
|
var asArray12 = (v) => Array.isArray(v) ? v : [];
|
|
2737
3200
|
var BLOCKS = new Map(
|
|
@@ -2864,7 +3327,7 @@ function checkChartAggregate(raw, push2) {
|
|
|
2864
3327
|
"warning",
|
|
2865
3328
|
REACT_CHART_AGGREGATE_INVALID,
|
|
2866
3329
|
"aggregate.groupBy is not set, so the aggregate returns ONE ungrouped row and the chart plots a single point.",
|
|
2867
|
-
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis.
|
|
3330
|
+
"Add aggregate.groupBy (a field name, or { field, dateGranularity } to bucket dates) to give the chart a category axis. objectstack#5583 ruled that an ungrouped single-value chart is NOT a supported <ObjectChart> shape \u2014 groupBy stays required, and a single number belongs in an object-metric block instead. This stays a warning rather than an error only because promoting it is its own step."
|
|
2868
3331
|
);
|
|
2869
3332
|
}
|
|
2870
3333
|
const parsed = import_ui2.ChartAggregateSchema.safeParse(raw);
|
|
@@ -3124,7 +3587,7 @@ function validateReactPageProps(stack) {
|
|
|
3124
3587
|
const source = page.source;
|
|
3125
3588
|
if (typeof source !== "string" || source.trim() === "") continue;
|
|
3126
3589
|
const name = String(page.name ?? `#${p}`);
|
|
3127
|
-
const tsc =
|
|
3590
|
+
const tsc = loadTypeScript2();
|
|
3128
3591
|
let sf;
|
|
3129
3592
|
try {
|
|
3130
3593
|
sf = tsc.createSourceFile("page.tsx", source, tsc.ScriptTarget.Latest, true, tsc.ScriptKind.TSX);
|
|
@@ -3382,7 +3845,7 @@ function validateSemanticRoles(stack) {
|
|
|
3382
3845
|
(h) => typeof h === "string" && h.length > 0
|
|
3383
3846
|
);
|
|
3384
3847
|
if (declaredStrings.length > 0 && declaredGroups.size > 0) {
|
|
3385
|
-
const declaredTitle = [obj.nameField, obj.
|
|
3848
|
+
const declaredTitle = [obj.nameField, obj.displayNameField].find((v) => typeof v === "string" && v.length > 0 && fieldNames.has(v));
|
|
3386
3849
|
const titleField = declaredTitle ?? ["name", "full_name", "title", "subject", "display_name"].find((c) => fieldNames.has(c));
|
|
3387
3850
|
const stripSet = new Set(
|
|
3388
3851
|
declaredStrings.filter((h) => h !== titleField).slice(0, 4)
|
|
@@ -3407,6 +3870,60 @@ function validateSemanticRoles(stack) {
|
|
|
3407
3870
|
return findings;
|
|
3408
3871
|
}
|
|
3409
3872
|
|
|
3873
|
+
// src/collection-entries.ts
|
|
3874
|
+
function isRec8(v) {
|
|
3875
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3876
|
+
}
|
|
3877
|
+
function collectionEntries(v, base) {
|
|
3878
|
+
if (Array.isArray(v)) {
|
|
3879
|
+
const out = [];
|
|
3880
|
+
for (let i = 0; i < v.length; i++) {
|
|
3881
|
+
if (isRec8(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
3882
|
+
}
|
|
3883
|
+
return out;
|
|
3884
|
+
}
|
|
3885
|
+
if (isRec8(v)) {
|
|
3886
|
+
return Object.entries(v).filter(([, def]) => isRec8(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
3887
|
+
}
|
|
3888
|
+
return [];
|
|
3889
|
+
}
|
|
3890
|
+
|
|
3891
|
+
// src/view-walk.ts
|
|
3892
|
+
function isRec9(v) {
|
|
3893
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3894
|
+
}
|
|
3895
|
+
function strName5(v) {
|
|
3896
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3897
|
+
}
|
|
3898
|
+
function viewObjectName(view) {
|
|
3899
|
+
return strName5(view.objectName) ?? strName5(view.object) ?? (isRec9(view.data) ? strName5(view.data.object) : void 0);
|
|
3900
|
+
}
|
|
3901
|
+
function viewContainerSites(view, basePath) {
|
|
3902
|
+
if (!isRec9(view)) return [];
|
|
3903
|
+
const sites = [{ view, path: basePath, surface: "", kind: "self" }];
|
|
3904
|
+
if (isRec9(view.form)) {
|
|
3905
|
+
sites.push({ view: view.form, path: `${basePath}.form`, surface: "form", kind: "form" });
|
|
3906
|
+
}
|
|
3907
|
+
for (const key of ["listViews", "formViews"]) {
|
|
3908
|
+
const container = view[key];
|
|
3909
|
+
if (!isRec9(container)) continue;
|
|
3910
|
+
const kind = key === "listViews" ? "listView" : "formView";
|
|
3911
|
+
for (const [subKey, sub] of Object.entries(container)) {
|
|
3912
|
+
if (!isRec9(sub)) continue;
|
|
3913
|
+
sites.push({
|
|
3914
|
+
view: sub,
|
|
3915
|
+
path: `${basePath}.${key}.${subKey}`,
|
|
3916
|
+
surface: `${key}.${subKey}`,
|
|
3917
|
+
kind
|
|
3918
|
+
});
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
return sites;
|
|
3922
|
+
}
|
|
3923
|
+
function formViewSites(view, basePath) {
|
|
3924
|
+
return viewContainerSites(view, basePath).filter((site) => site.kind !== "listView");
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3410
3927
|
// src/validate-form-layout.ts
|
|
3411
3928
|
var FORM_FIELD_UNKNOWN = "form-field-unknown";
|
|
3412
3929
|
var FORM_COLSPAN_ABSOLUTE = "absolute-colspan-discouraged";
|
|
@@ -3417,6 +3934,12 @@ function asArray16(v) {
|
|
|
3417
3934
|
}
|
|
3418
3935
|
return [];
|
|
3419
3936
|
}
|
|
3937
|
+
function isRec10(v) {
|
|
3938
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
3939
|
+
}
|
|
3940
|
+
function strName6(v) {
|
|
3941
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
3942
|
+
}
|
|
3420
3943
|
function fieldNameOf(entry) {
|
|
3421
3944
|
if (typeof entry === "string") return entry.length > 0 ? entry : null;
|
|
3422
3945
|
if (entry && typeof entry === "object" && !Array.isArray(entry)) {
|
|
@@ -3425,13 +3948,6 @@ function fieldNameOf(entry) {
|
|
|
3425
3948
|
}
|
|
3426
3949
|
return null;
|
|
3427
3950
|
}
|
|
3428
|
-
function boundObject(view) {
|
|
3429
|
-
const data = view.data;
|
|
3430
|
-
if (data && typeof data === "object" && typeof data.object === "string") {
|
|
3431
|
-
return data.object;
|
|
3432
|
-
}
|
|
3433
|
-
return typeof view.objectName === "string" ? view.objectName : void 0;
|
|
3434
|
-
}
|
|
3435
3951
|
function validateFormLayout(stack) {
|
|
3436
3952
|
const findings = [];
|
|
3437
3953
|
const objectFields = /* @__PURE__ */ new Map();
|
|
@@ -3441,44 +3957,44 @@ function validateFormLayout(stack) {
|
|
|
3441
3957
|
const fields = obj.fields && typeof obj.fields === "object" && !Array.isArray(obj.fields) ? Object.keys(obj.fields) : [];
|
|
3442
3958
|
objectFields.set(name, new Set(fields));
|
|
3443
3959
|
}
|
|
3444
|
-
const
|
|
3445
|
-
|
|
3446
|
-
const
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
}
|
|
3960
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
3961
|
+
const viewName = strName6(view.name) ?? strName6(view.object) ?? viewPath;
|
|
3962
|
+
const containerObject = viewObjectName(view);
|
|
3963
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
3964
|
+
const objName = viewObjectName(site.view) ?? containerObject;
|
|
3965
|
+
const known = objName ? objectFields.get(objName) : void 0;
|
|
3966
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
3967
|
+
for (const bucket of ["sections", "groups"]) {
|
|
3968
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
3969
|
+
for (let s = 0; s < sections.length; s++) {
|
|
3970
|
+
const sec = sections[s];
|
|
3971
|
+
const secFields = isRec10(sec) && Array.isArray(sec.fields) ? sec.fields : [];
|
|
3972
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
3973
|
+
const entry = secFields[f];
|
|
3974
|
+
const fname = fieldNameOf(entry);
|
|
3975
|
+
const fpath = `${site.path}.${bucket}[${s}].fields[${f}]`;
|
|
3976
|
+
if (fname && known && !known.has(fname)) {
|
|
3977
|
+
findings.push({
|
|
3978
|
+
severity: "warning",
|
|
3979
|
+
rule: FORM_FIELD_UNKNOWN,
|
|
3980
|
+
where,
|
|
3981
|
+
path: fpath,
|
|
3982
|
+
message: `${viewName}: field "${fname}" is not a field on object "${objName}" \u2014 it is silently skipped and never renders on the form`,
|
|
3983
|
+
hint: `Fix the field name, or add "${fname}" to ${objName}. Section field references must match the object's field names exactly.`
|
|
3984
|
+
});
|
|
3985
|
+
}
|
|
3986
|
+
const colSpan = isRec10(entry) ? entry.colSpan : void 0;
|
|
3987
|
+
if (colSpan != null) {
|
|
3988
|
+
findings.push({
|
|
3989
|
+
severity: "warning",
|
|
3990
|
+
rule: FORM_COLSPAN_ABSOLUTE,
|
|
3991
|
+
where,
|
|
3992
|
+
path: `${fpath}.colSpan`,
|
|
3993
|
+
message: `${viewName}: field "${fname ?? "?"}" sets absolute colSpan ${String(colSpan)} \u2014 the form's column count is derived per surface (mobile 1 / modal 2 / page 3-4), so a fixed span only aligns at one width`,
|
|
3994
|
+
hint: `Prefer span: 'full' (whole row at any column count), or omit for auto width. The renderer clamps colSpan to the current column count.`
|
|
3995
|
+
});
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3482
3998
|
}
|
|
3483
3999
|
}
|
|
3484
4000
|
}
|
|
@@ -3487,17 +4003,12 @@ function validateFormLayout(stack) {
|
|
|
3487
4003
|
}
|
|
3488
4004
|
|
|
3489
4005
|
// src/validate-visibility-predicates.ts
|
|
3490
|
-
var
|
|
4006
|
+
var import_formula3 = require("@objectstack/formula");
|
|
3491
4007
|
var VISIBILITY_ROOT_MISLAYERED = "visibility-root-mislayered";
|
|
4008
|
+
var VISIBILITY_BARE_IDENTIFIER = "visibility-bare-identifier";
|
|
4009
|
+
var VISIBILITY_PREDICATE_SYNTAX = "visibility-predicate-syntax";
|
|
4010
|
+
var VISIBILITY_PREDICATE_OVER_BUDGET = "visibility-predicate-over-budget";
|
|
3492
4011
|
var CANONICAL = "visibleWhen";
|
|
3493
|
-
var ALIASES = ["visibleOn", "visibility"];
|
|
3494
|
-
function asArray17(v) {
|
|
3495
|
-
if (Array.isArray(v)) return v;
|
|
3496
|
-
if (v && typeof v === "object") {
|
|
3497
|
-
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
3498
|
-
}
|
|
3499
|
-
return [];
|
|
3500
|
-
}
|
|
3501
4012
|
function predicateSource(v) {
|
|
3502
4013
|
if (typeof v === "string") return v;
|
|
3503
4014
|
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
@@ -3508,6 +4019,67 @@ function predicateSource(v) {
|
|
|
3508
4019
|
function usesRoot(source, root) {
|
|
3509
4020
|
return new RegExp(`(^|[^.\\w$])${root}\\.\\w`).test(source);
|
|
3510
4021
|
}
|
|
4022
|
+
function withoutStringLiterals(source) {
|
|
4023
|
+
return source.replace(/'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/g, (lit) => " ".repeat(lit.length));
|
|
4024
|
+
}
|
|
4025
|
+
var NON_CEL_SPELLINGS = [
|
|
4026
|
+
{ wrote: "===", cel: "==", example: "record.country == 'USA'", re: /===/ },
|
|
4027
|
+
{ wrote: "!==", cel: "!=", example: "record.country != 'USA'", re: /!==/ },
|
|
4028
|
+
{ wrote: "<>", cel: "!=", example: "record.country != 'USA'", re: /<>/ },
|
|
4029
|
+
{ wrote: "and", cel: "&&", example: "record.a == 1 && record.b == 2", re: /(?<![.\w$])and(?![\w$])/i },
|
|
4030
|
+
{ wrote: "or", cel: "||", example: "record.a == 1 || record.b == 2", re: /(?<![.\w$])or(?![\w$])/i },
|
|
4031
|
+
{ wrote: "not", cel: "!", example: "!record.archived", re: /(?<![.\w$])not(?![\w$])/i },
|
|
4032
|
+
// Assignment where a comparison was meant. Last, and fenced off from every
|
|
4033
|
+
// operator that legitimately contains `=` (`==`, `!=`, `<=`, `>=`).
|
|
4034
|
+
{ wrote: "=", cel: "==", example: "record.status == 'open'", re: /(?<![=!<>])=(?!=)/ }
|
|
4035
|
+
];
|
|
4036
|
+
function quoteSource(source) {
|
|
4037
|
+
const flat = source.replace(/\s+/g, " ").trim();
|
|
4038
|
+
return flat.length > 120 ? `${flat.slice(0, 117)}...` : flat;
|
|
4039
|
+
}
|
|
4040
|
+
function celRefusal(source) {
|
|
4041
|
+
if (!source.trim()) return null;
|
|
4042
|
+
const parsed = (0, import_formula3.parseCelToAstWithReason)(source);
|
|
4043
|
+
if (parsed.ok || parsed.kind === "empty") return null;
|
|
4044
|
+
if (parsed.kind === "bounds") return { kind: "bounds", overrun: parsed.overrun };
|
|
4045
|
+
const identifiers = (0, import_formula3.collectCelRootIdentifiers)(source);
|
|
4046
|
+
const detail = identifiers.ok ? "the expression could not be parsed" : identifiers.error.split("\n")[0].trim();
|
|
4047
|
+
const scannable = withoutStringLiterals(source);
|
|
4048
|
+
return { kind: "syntax", detail, token: NON_CEL_SPELLINGS.find((s) => s.re.test(scannable)) ?? null };
|
|
4049
|
+
}
|
|
4050
|
+
function boundName(overrun) {
|
|
4051
|
+
return overrun.limit && overrun.limitValue !== null ? `the \`${overrun.limit}\` budget (platform limit ${overrun.limitValue})` : "one of the platform's parse budgets";
|
|
4052
|
+
}
|
|
4053
|
+
var VIEW_PAGE_EXTRA_ROOTS = ["current_user", "page"];
|
|
4054
|
+
function isNode2(v) {
|
|
4055
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4056
|
+
}
|
|
4057
|
+
function namespaceRoots(node, out) {
|
|
4058
|
+
if (Array.isArray(node)) {
|
|
4059
|
+
for (const child of node) namespaceRoots(child, out);
|
|
4060
|
+
return;
|
|
4061
|
+
}
|
|
4062
|
+
if (!isNode2(node)) return;
|
|
4063
|
+
const args = node.args;
|
|
4064
|
+
if (Array.isArray(args)) {
|
|
4065
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
4066
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode2(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
4067
|
+
out.add(receiver.args);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
namespaceRoots(args, out);
|
|
4071
|
+
}
|
|
4072
|
+
function firstBareIdentifier(source) {
|
|
4073
|
+
const ast = (0, import_formula3.parseCelToAst)(source);
|
|
4074
|
+
if (!ast) return null;
|
|
4075
|
+
const rooted = /* @__PURE__ */ new Set();
|
|
4076
|
+
namespaceRoots(ast, rooted);
|
|
4077
|
+
return (0, import_formula3.firstUndeclaredReference)(source, [...VIEW_PAGE_EXTRA_ROOTS, ...rooted]);
|
|
4078
|
+
}
|
|
4079
|
+
var CANONICAL_ROOT_BY_LAYER = {
|
|
4080
|
+
runtime: "record",
|
|
4081
|
+
metadata: "data"
|
|
4082
|
+
};
|
|
3511
4083
|
var MISLAYER_BY_LAYER = {
|
|
3512
4084
|
runtime: {
|
|
3513
4085
|
forbiddenRoot: "data",
|
|
@@ -3521,18 +4093,6 @@ var MISLAYER_BY_LAYER = {
|
|
|
3521
4093
|
}
|
|
3522
4094
|
};
|
|
3523
4095
|
function checkElement(el, where, path, layer, findings) {
|
|
3524
|
-
for (const alias of ALIASES) {
|
|
3525
|
-
if (el[alias] !== void 0) {
|
|
3526
|
-
findings.push({
|
|
3527
|
-
severity: "warning",
|
|
3528
|
-
rule: VISIBILITY_ALIAS_DEPRECATED,
|
|
3529
|
-
where,
|
|
3530
|
-
path: `${path}.${alias}`,
|
|
3531
|
-
message: `\`${alias}\` is the deprecated spelling of the conditional-visibility predicate (ADR-0089). It still works \u2014 it is normalized to \`visibleWhen\` at parse \u2014 but the canonical key is \`visibleWhen\`.`,
|
|
3532
|
-
hint: `Rename the key \`${alias}\` \u2192 \`visibleWhen\` (same CEL value).`
|
|
3533
|
-
});
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
4096
|
const raw = el[CANONICAL] ?? el.visibleOn ?? el.visibility;
|
|
3537
4097
|
const source = predicateSource(raw);
|
|
3538
4098
|
const rule = MISLAYER_BY_LAYER[layer];
|
|
@@ -3546,6 +4106,43 @@ function checkElement(el, where, path, layer, findings) {
|
|
|
3546
4106
|
hint: rule.hint
|
|
3547
4107
|
});
|
|
3548
4108
|
}
|
|
4109
|
+
const refusal = source ? celRefusal(source) : null;
|
|
4110
|
+
if (source && refusal?.kind === "bounds") {
|
|
4111
|
+
const bound = boundName(refusal.overrun);
|
|
4112
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
4113
|
+
findings.push({
|
|
4114
|
+
severity: "error",
|
|
4115
|
+
rule: VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
4116
|
+
where,
|
|
4117
|
+
path,
|
|
4118
|
+
message: `visibility predicate is syntactically valid CEL but overruns ${bound} (${refusal.overrun.summary}) (predicate: \`${quoteSource(source)}\`). The canonical front end refuses it, so it can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
4119
|
+
hint: `There is no syntax or dialect error to correct here \u2014 this is a SIZE fault, not a dialect mistake, so re-spelling the predicate will not fix it. Make it smaller, or move the work off the predicate: (1) collapse a long \`${root}.f == 'a' || ${root}.f == 'b' || \u2026\` chain into a single \`${root}.f in ['a', 'b', \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2); (2) precompute the heavy part into a formula/rollup field on the object and test that one field instead. Logic genuinely this large is not element visibility \u2014 compute it once on the record rather than re-deriving it in every predicate that needs it.`
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
if (source && refusal?.kind === "syntax") {
|
|
4123
|
+
findings.push({
|
|
4124
|
+
severity: "error",
|
|
4125
|
+
rule: VISIBILITY_PREDICATE_SYNTAX,
|
|
4126
|
+
where,
|
|
4127
|
+
path,
|
|
4128
|
+
message: `visibility predicate is not valid CEL \u2014 ${refusal.detail} (predicate: \`${quoteSource(source)}\`). A predicate that does not parse can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
4129
|
+
hint: refusal.token ? `\`${refusal.token.wrote}\` is not a CEL operator \u2014 CEL spells it \`${refusal.token.cel}\`. Replace \`${refusal.token.wrote}\` with \`${refusal.token.cel}\`, e.g. \`${refusal.token.example}\`.` : `Visibility predicates are bare CEL, e.g. \`record.status == 'open'\`. Spellings from other languages do not parse: write \`==\` (not \`===\`), \`!=\` (not \`!==\` or \`<>\`), \`&&\` (not \`and\`), \`||\` (not \`or\`), \`!\` (not \`not\`).`
|
|
4130
|
+
});
|
|
4131
|
+
}
|
|
4132
|
+
if (source && !refusal) {
|
|
4133
|
+
const bare = firstBareIdentifier(source);
|
|
4134
|
+
if (bare) {
|
|
4135
|
+
const root = CANONICAL_ROOT_BY_LAYER[layer];
|
|
4136
|
+
findings.push({
|
|
4137
|
+
severity: "error",
|
|
4138
|
+
rule: VISIBILITY_BARE_IDENTIFIER,
|
|
4139
|
+
where,
|
|
4140
|
+
path,
|
|
4141
|
+
message: `visibility predicate references \`${bare}\` as a bare identifier. Values are bound under a namespace on this surface \u2014 they are never flattened to top level \u2014 so \`${bare}\` resolves to nothing, the predicate can never evaluate, and the console falls OPEN: the element renders unconditionally and looks exactly like one with no predicate at all (#5149).`,
|
|
4142
|
+
hint: `Write \`${root}.${bare}\` instead of \`${bare}\`` + (layer === "runtime" ? " (runtime view/page surfaces bind `record` + `current_user`; a page component also exposes page state as `page.<var>`)." : " (a `*.form.ts` metadata-editing form binds the row under edit as `data`).")
|
|
4143
|
+
});
|
|
4144
|
+
}
|
|
4145
|
+
}
|
|
3549
4146
|
}
|
|
3550
4147
|
function isFieldObject(entry) {
|
|
3551
4148
|
return !!entry && typeof entry === "object" && !Array.isArray(entry);
|
|
@@ -3553,43 +4150,304 @@ function isFieldObject(entry) {
|
|
|
3553
4150
|
function validateVisibilityPredicates(stack, opts = {}) {
|
|
3554
4151
|
const layer = opts.layer ?? "runtime";
|
|
3555
4152
|
const findings = [];
|
|
3556
|
-
const
|
|
3557
|
-
|
|
3558
|
-
const view
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
4153
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
4154
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
4155
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
4156
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface}` : `view "${viewName}"`;
|
|
4157
|
+
for (const bucket of ["sections", "groups"]) {
|
|
4158
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
4159
|
+
for (let s = 0; s < sections.length; s++) {
|
|
4160
|
+
const sec = sections[s];
|
|
4161
|
+
if (!sec || typeof sec !== "object") continue;
|
|
4162
|
+
const secPath = `${site.path}.${bucket}[${s}]`;
|
|
4163
|
+
checkElement(sec, where, secPath, layer, findings);
|
|
4164
|
+
const secFields = Array.isArray(sec.fields) ? sec.fields : [];
|
|
4165
|
+
for (let f = 0; f < secFields.length; f++) {
|
|
4166
|
+
const entry = secFields[f];
|
|
4167
|
+
if (isFieldObject(entry)) {
|
|
4168
|
+
checkElement(entry, where, `${secPath}.fields[${f}]`, layer, findings);
|
|
4169
|
+
}
|
|
3574
4170
|
}
|
|
3575
4171
|
}
|
|
3576
4172
|
}
|
|
3577
4173
|
}
|
|
3578
4174
|
}
|
|
3579
|
-
const
|
|
3580
|
-
|
|
3581
|
-
const
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
4175
|
+
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
4176
|
+
const pageName = typeof page.name === "string" ? page.name : void 0;
|
|
4177
|
+
const where = `page "${pageName ?? pagePath}"`;
|
|
4178
|
+
for (const walked of walkPageComponents(page, pagePath)) {
|
|
4179
|
+
checkElement(walked.component, where, walked.path, layer, findings);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
return findings;
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
// src/validate-predicate-path-refs.ts
|
|
4186
|
+
var import_formula4 = require("@objectstack/formula");
|
|
4187
|
+
var import_kernel2 = require("@objectstack/spec/kernel");
|
|
4188
|
+
var import_spec = require("@objectstack/spec");
|
|
4189
|
+
var PREDICATE_PATH_UNRESOLVED = "predicate-path-unresolved";
|
|
4190
|
+
var PREDICATE_PATH_UNROOTED = "predicate-path-unrooted";
|
|
4191
|
+
var PREDICATE_KEYS = ["visibleWhen", "visibleOn"];
|
|
4192
|
+
var ROOT = "data";
|
|
4193
|
+
var COMPREHENSION_MACROS = /* @__PURE__ */ new Set(["all", "exists", "exists_one", "map", "filter"]);
|
|
4194
|
+
function defOf(schema) {
|
|
4195
|
+
if (!schema || typeof schema !== "object" && typeof schema !== "function") return void 0;
|
|
4196
|
+
const s = schema;
|
|
4197
|
+
return s.def ?? s._def;
|
|
4198
|
+
}
|
|
4199
|
+
function peel(schema, depth = 0) {
|
|
4200
|
+
if (!schema || depth > 25) return schema;
|
|
4201
|
+
const d = defOf(schema);
|
|
4202
|
+
if (!d) return schema;
|
|
4203
|
+
switch (d.type) {
|
|
4204
|
+
case "optional":
|
|
4205
|
+
case "nullable":
|
|
4206
|
+
case "default":
|
|
4207
|
+
case "prefault":
|
|
4208
|
+
case "readonly":
|
|
4209
|
+
case "catch":
|
|
4210
|
+
case "nonoptional":
|
|
4211
|
+
return peel(d.innerType, depth + 1);
|
|
4212
|
+
case "lazy":
|
|
4213
|
+
return peel(d.getter(), depth + 1);
|
|
4214
|
+
case "pipe": {
|
|
4215
|
+
const inner = peel(d.in, depth + 1);
|
|
4216
|
+
return defOf(inner)?.type === "transform" ? peel(d.out, depth + 1) : inner;
|
|
4217
|
+
}
|
|
4218
|
+
default:
|
|
4219
|
+
return schema;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
function optionsOf(d) {
|
|
4223
|
+
return Array.isArray(d?.options) ? d.options : [];
|
|
4224
|
+
}
|
|
4225
|
+
function keysOf(schema, depth = 0) {
|
|
4226
|
+
if (depth > 25) return null;
|
|
4227
|
+
const u = peel(schema);
|
|
4228
|
+
const d = defOf(u);
|
|
4229
|
+
if (d?.type === "object") return Object.keys(d.shape ?? u.shape ?? {});
|
|
4230
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
4231
|
+
const all = /* @__PURE__ */ new Set();
|
|
4232
|
+
let keyBearing = false;
|
|
4233
|
+
for (const option of optionsOf(d)) {
|
|
4234
|
+
const k = keysOf(option, depth + 1);
|
|
4235
|
+
if (!k) continue;
|
|
4236
|
+
keyBearing = true;
|
|
4237
|
+
for (const key of k) all.add(key);
|
|
4238
|
+
}
|
|
4239
|
+
return keyBearing ? [...all] : null;
|
|
4240
|
+
}
|
|
4241
|
+
if (d?.type === "intersection") {
|
|
4242
|
+
const left = keysOf(d.left, depth + 1);
|
|
4243
|
+
const right = keysOf(d.right, depth + 1);
|
|
4244
|
+
if (!left && !right) return null;
|
|
4245
|
+
return [.../* @__PURE__ */ new Set([...left ?? [], ...right ?? []])];
|
|
4246
|
+
}
|
|
4247
|
+
return null;
|
|
4248
|
+
}
|
|
4249
|
+
function propertyOf(schema, key, depth = 0) {
|
|
4250
|
+
if (depth > 25) return void 0;
|
|
4251
|
+
const u = peel(schema);
|
|
4252
|
+
const d = defOf(u);
|
|
4253
|
+
if (d?.type === "object") return (d.shape ?? u.shape ?? {})[key];
|
|
4254
|
+
if (d?.type === "union" || d?.type === "discriminated_union") {
|
|
4255
|
+
for (const option of optionsOf(d)) {
|
|
4256
|
+
const found = propertyOf(option, key, depth + 1);
|
|
4257
|
+
if (found !== void 0) return found;
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
if (d?.type === "intersection") {
|
|
4261
|
+
return propertyOf(d.left, key, depth + 1) ?? propertyOf(d.right, key, depth + 1);
|
|
4262
|
+
}
|
|
4263
|
+
return void 0;
|
|
4264
|
+
}
|
|
4265
|
+
function rowScopeOf(scope, key) {
|
|
4266
|
+
const prop = propertyOf(scope, key);
|
|
4267
|
+
if (prop === void 0) return void 0;
|
|
4268
|
+
let node = peel(prop);
|
|
4269
|
+
for (let i = 0; i < 25; i++) {
|
|
4270
|
+
const d = defOf(node);
|
|
4271
|
+
if (d?.type === "array") node = peel(d.element);
|
|
4272
|
+
else if (d?.type === "record") node = peel(d.valueType);
|
|
4273
|
+
else return node;
|
|
4274
|
+
}
|
|
4275
|
+
return node;
|
|
4276
|
+
}
|
|
4277
|
+
function stepInto(scope, segment) {
|
|
4278
|
+
const u = peel(scope);
|
|
4279
|
+
const d = defOf(u);
|
|
4280
|
+
if (d?.type === "record") return { kind: "declared", next: d.valueType };
|
|
4281
|
+
const declared = keysOf(u);
|
|
4282
|
+
if (declared === null) return { kind: "opaque" };
|
|
4283
|
+
if (!declared.includes(segment)) return { kind: "undeclared", declared };
|
|
4284
|
+
return { kind: "declared", next: propertyOf(u, segment) };
|
|
4285
|
+
}
|
|
4286
|
+
function isNode3(v) {
|
|
4287
|
+
return !!v && typeof v === "object" && typeof v.op === "string";
|
|
4288
|
+
}
|
|
4289
|
+
function memberChain(node) {
|
|
4290
|
+
if (!isNode3(node)) return null;
|
|
4291
|
+
if (node.op === "id" && typeof node.args === "string") return [node.args];
|
|
4292
|
+
if (node.op === "." && Array.isArray(node.args) && typeof node.args[1] === "string") {
|
|
4293
|
+
const head = memberChain(node.args[0]);
|
|
4294
|
+
return head ? [...head, node.args[1]] : null;
|
|
4295
|
+
}
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
function rootedPaths(node, out) {
|
|
4299
|
+
if (Array.isArray(node)) {
|
|
4300
|
+
for (const child of node) rootedPaths(child, out);
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
4303
|
+
if (!isNode3(node)) return;
|
|
4304
|
+
if (node.op === ".") {
|
|
4305
|
+
const chain = memberChain(node);
|
|
4306
|
+
if (chain && chain[0] === ROOT && chain.length > 1) {
|
|
4307
|
+
out.push(chain.slice(1));
|
|
4308
|
+
return;
|
|
4309
|
+
}
|
|
4310
|
+
}
|
|
4311
|
+
rootedPaths(node.args, out);
|
|
4312
|
+
}
|
|
4313
|
+
function classifyIdentifiers(node, values, excluded) {
|
|
4314
|
+
if (Array.isArray(node)) {
|
|
4315
|
+
for (const child of node) classifyIdentifiers(child, values, excluded);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (!isNode3(node)) return;
|
|
4319
|
+
const args = node.args;
|
|
4320
|
+
if (Array.isArray(args)) {
|
|
4321
|
+
const receiver = node.op === "rcall" ? args[1] : args[0];
|
|
4322
|
+
if ((node.op === "." || node.op === ".?" || node.op === "[]" || node.op === "rcall") && isNode3(receiver) && receiver.op === "id" && typeof receiver.args === "string") {
|
|
4323
|
+
excluded.add(receiver.args);
|
|
4324
|
+
}
|
|
4325
|
+
if (node.op === "rcall" && typeof args[0] === "string" && COMPREHENSION_MACROS.has(args[0])) {
|
|
4326
|
+
const macroArgs = args[2];
|
|
4327
|
+
if (Array.isArray(macroArgs) && macroArgs.length >= 2) {
|
|
4328
|
+
const bound = macroArgs[0];
|
|
4329
|
+
if (isNode3(bound) && bound.op === "id" && typeof bound.args === "string") {
|
|
4330
|
+
excluded.add(bound.args);
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (node.op === "id" && typeof node.args === "string") {
|
|
4336
|
+
values.add(node.args);
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
classifyIdentifiers(args, values, excluded);
|
|
4340
|
+
}
|
|
4341
|
+
function predicateSource2(v) {
|
|
4342
|
+
if (typeof v === "string") return v;
|
|
4343
|
+
if (v && typeof v === "object" && typeof v.source === "string") {
|
|
4344
|
+
return v.source;
|
|
4345
|
+
}
|
|
4346
|
+
return void 0;
|
|
4347
|
+
}
|
|
4348
|
+
function isRec11(v) {
|
|
4349
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4350
|
+
}
|
|
4351
|
+
function schemaIdOf(view) {
|
|
4352
|
+
const data = view.data;
|
|
4353
|
+
if (!isRec11(data)) return void 0;
|
|
4354
|
+
if (data.provider !== "schema") return void 0;
|
|
4355
|
+
return typeof data.schemaId === "string" ? data.schemaId : void 0;
|
|
4356
|
+
}
|
|
4357
|
+
function checkPredicate(source, scope, where, path, findings) {
|
|
4358
|
+
const ast = (0, import_formula4.parseCelToAst)(source);
|
|
4359
|
+
if (!ast) return;
|
|
4360
|
+
const paths = [];
|
|
4361
|
+
rootedPaths(ast, paths);
|
|
4362
|
+
for (const segments of paths) {
|
|
4363
|
+
let cursor = scope;
|
|
4364
|
+
const walked = [];
|
|
4365
|
+
for (const segment of segments) {
|
|
4366
|
+
const step = stepInto(cursor, segment);
|
|
4367
|
+
if (step.kind === "opaque") break;
|
|
4368
|
+
if (step.kind === "undeclared") {
|
|
4369
|
+
const full = [ROOT, ...walked, segment].join(".");
|
|
4370
|
+
const container = walked.length ? `${ROOT}.${walked.join(".")}` : ROOT;
|
|
4371
|
+
findings.push({
|
|
4372
|
+
severity: "error",
|
|
4373
|
+
rule: PREDICATE_PATH_UNRESOLVED,
|
|
4374
|
+
where,
|
|
4375
|
+
path,
|
|
4376
|
+
message: `predicate references \`${full}\`, which the target schema does not declare \u2014 \`${segment}\` is not a key of \`${container}\`. The reference resolves to nothing, so the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149).`,
|
|
4377
|
+
hint: `${(0, import_spec.formatSuggestion)((0, import_spec.findClosestMatches)(segment, step.declared)) || `\`${container}\` declares: ${step.declared.slice(0, 12).sort().join(", ")}`} Every reference must resolve against the schema the form edits.`
|
|
4378
|
+
});
|
|
4379
|
+
break;
|
|
4380
|
+
}
|
|
4381
|
+
walked.push(segment);
|
|
4382
|
+
cursor = step.next;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
const declaredHere = keysOf(scope);
|
|
4386
|
+
if (!declaredHere) return;
|
|
4387
|
+
const values = /* @__PURE__ */ new Set();
|
|
4388
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
4389
|
+
classifyIdentifiers(ast, values, excluded);
|
|
4390
|
+
for (const id of values) {
|
|
4391
|
+
if (excluded.has(id) || !declaredHere.includes(id)) continue;
|
|
4392
|
+
findings.push({
|
|
4393
|
+
severity: "error",
|
|
4394
|
+
rule: PREDICATE_PATH_UNROOTED,
|
|
4395
|
+
where,
|
|
4396
|
+
path,
|
|
4397
|
+
message: `predicate references \`${id}\` as a bare identifier, but \`${id}\` is a key of the schema this form edits \u2014 the binding root was dropped. Values are bound under \`${ROOT}\` and are never flattened to top level, so \`${id}\` resolves to nothing, the predicate can never evaluate and the console falls OPEN: the element renders unconditionally and looks exactly like one carrying no predicate at all (#5149, #6254).`,
|
|
4398
|
+
hint: `Write \`${ROOT}.${id}\` instead of \`${id}\`. A metadata-editing form binds the row under edit as \`${ROOT}\` at every depth \u2014 inside a repeater \`${ROOT}\` is the ROW, but it is still spelled \`${ROOT}\` (there is no implicit row scope).`
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
function walkFields(entries, scope, where, base, findings, depth) {
|
|
4403
|
+
if (!Array.isArray(entries) || depth > 12) return;
|
|
4404
|
+
for (let i = 0; i < entries.length; i++) {
|
|
4405
|
+
const entry = entries[i];
|
|
4406
|
+
if (!isRec11(entry)) continue;
|
|
4407
|
+
const path = `${base}[${i}]`;
|
|
4408
|
+
for (const key of PREDICATE_KEYS) {
|
|
4409
|
+
const source = predicateSource2(entry[key]);
|
|
4410
|
+
if (source !== void 0 && source.trim()) {
|
|
4411
|
+
checkPredicate(source, scope, where, `${path}.${key}`, findings);
|
|
4412
|
+
break;
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
if (Array.isArray(entry.fields) && entry.fields.length > 0 && typeof entry.field === "string") {
|
|
4416
|
+
const row = scope === void 0 ? void 0 : rowScopeOf(scope, entry.field);
|
|
4417
|
+
walkFields(entry.fields, row, where, `${path}.fields`, findings, depth + 1);
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
}
|
|
4421
|
+
function validatePredicatePathRefs(stack, opts = {}) {
|
|
4422
|
+
const resolveSchema = opts.resolveSchema ?? ((schemaId) => (0, import_kernel2.getMetadataTypeSchema)(schemaId));
|
|
4423
|
+
const findings = [];
|
|
4424
|
+
for (const { rec: view, path: viewPath } of collectionEntries(stack.views, "views")) {
|
|
4425
|
+
const viewName = typeof view.name === "string" ? view.name : typeof view.object === "string" ? view.object : viewPath;
|
|
4426
|
+
for (const site of formViewSites(view, viewPath)) {
|
|
4427
|
+
const schemaId = schemaIdOf(site.view);
|
|
4428
|
+
if (!schemaId) continue;
|
|
4429
|
+
let root;
|
|
4430
|
+
try {
|
|
4431
|
+
root = resolveSchema(schemaId);
|
|
4432
|
+
} catch {
|
|
4433
|
+
continue;
|
|
4434
|
+
}
|
|
4435
|
+
if (!root) continue;
|
|
4436
|
+
const where = site.surface ? `view "${viewName}" \xB7 ${site.surface} (schema "${schemaId}")` : `view "${viewName}" (schema "${schemaId}")`;
|
|
4437
|
+
for (const bucket of ["sections", "groups"]) {
|
|
4438
|
+
const sections = Array.isArray(site.view[bucket]) ? site.view[bucket] : [];
|
|
4439
|
+
for (let s = 0; s < sections.length; s++) {
|
|
4440
|
+
const section = sections[s];
|
|
4441
|
+
if (!isRec11(section)) continue;
|
|
4442
|
+
const sectionPath = `${site.path}.${bucket}[${s}]`;
|
|
4443
|
+
for (const key of PREDICATE_KEYS) {
|
|
4444
|
+
const source = predicateSource2(section[key]);
|
|
4445
|
+
if (source !== void 0 && source.trim()) {
|
|
4446
|
+
checkPredicate(source, root, where, `${sectionPath}.${key}`, findings);
|
|
4447
|
+
break;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
walkFields(section.fields, root, where, `${sectionPath}.fields`, findings, 0);
|
|
3593
4451
|
}
|
|
3594
4452
|
}
|
|
3595
4453
|
}
|
|
@@ -3600,7 +4458,7 @@ function validateVisibilityPredicates(stack, opts = {}) {
|
|
|
3600
4458
|
// src/validate-capability-references.ts
|
|
3601
4459
|
var import_security = require("@objectstack/spec/security");
|
|
3602
4460
|
var CAPABILITY_REFERENCE_UNKNOWN = "capability-reference-unknown";
|
|
3603
|
-
function
|
|
4461
|
+
function asArray17(v) {
|
|
3604
4462
|
if (Array.isArray(v)) return v;
|
|
3605
4463
|
if (v && typeof v === "object") {
|
|
3606
4464
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3625,13 +4483,13 @@ function validateCapabilityReferences(stack) {
|
|
|
3625
4483
|
const findings = [];
|
|
3626
4484
|
if (!stack || typeof stack !== "object") return findings;
|
|
3627
4485
|
const known = new Set(import_security.PLATFORM_CAPABILITY_NAMES);
|
|
3628
|
-
for (const cap of
|
|
4486
|
+
for (const cap of asArray17(stack.capabilities)) {
|
|
3629
4487
|
if (typeof cap.name === "string" && cap.name.length > 0) known.add(cap.name);
|
|
3630
4488
|
}
|
|
3631
|
-
for (const ps of
|
|
4489
|
+
for (const ps of asArray17(stack.permissions)) {
|
|
3632
4490
|
for (const cap of asCapArray(ps.systemPermissions)) known.add(cap);
|
|
3633
4491
|
}
|
|
3634
|
-
for (const seed of
|
|
4492
|
+
for (const seed of asArray17(stack.data)) {
|
|
3635
4493
|
if (seed.object !== "sys_capability") continue;
|
|
3636
4494
|
for (const rec of Array.isArray(seed.records) ? seed.records : []) {
|
|
3637
4495
|
const name = rec?.name;
|
|
@@ -3650,7 +4508,7 @@ function validateCapabilityReferences(stack) {
|
|
|
3650
4508
|
hint
|
|
3651
4509
|
});
|
|
3652
4510
|
};
|
|
3653
|
-
const objects =
|
|
4511
|
+
const objects = asArray17(stack.objects);
|
|
3654
4512
|
for (let i = 0; i < objects.length; i++) {
|
|
3655
4513
|
const obj = objects[i];
|
|
3656
4514
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -3659,27 +4517,27 @@ function validateCapabilityReferences(stack) {
|
|
|
3659
4517
|
for (const { cap, key } of flattenObjectRequired(obj.requiredPermissions)) {
|
|
3660
4518
|
flag(cap, `object "${objName}"`, `${objPath}.requiredPermissions${key ? `.${key}` : ""}`);
|
|
3661
4519
|
}
|
|
3662
|
-
const fields =
|
|
4520
|
+
const fields = asArray17(obj.fields);
|
|
3663
4521
|
for (const f of fields) {
|
|
3664
4522
|
const fname = typeof f.name === "string" ? f.name : "(field)";
|
|
3665
4523
|
for (const cap of asCapArray(f.requiredPermissions)) {
|
|
3666
4524
|
flag(cap, `field "${objName}.${fname}"`, `${objPath}.fields.${fname}.requiredPermissions`);
|
|
3667
4525
|
}
|
|
3668
4526
|
}
|
|
3669
|
-
for (const [ai, action] of
|
|
4527
|
+
for (const [ai, action] of asArray17(obj.actions).entries()) {
|
|
3670
4528
|
const aName = typeof action.name === "string" ? action.name : `(action ${ai})`;
|
|
3671
4529
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
3672
4530
|
flag(cap, `action "${objName}.${aName}"`, `${objPath}.actions[${ai}].requiredPermissions`);
|
|
3673
4531
|
}
|
|
3674
4532
|
}
|
|
3675
4533
|
}
|
|
3676
|
-
for (const [i, action] of
|
|
4534
|
+
for (const [i, action] of asArray17(stack.actions).entries()) {
|
|
3677
4535
|
const aName = typeof action.name === "string" ? action.name : `(action ${i})`;
|
|
3678
4536
|
for (const cap of asCapArray(action.requiredPermissions)) {
|
|
3679
4537
|
flag(cap, `action "${aName}"`, `actions[${i}].requiredPermissions`);
|
|
3680
4538
|
}
|
|
3681
4539
|
}
|
|
3682
|
-
const apps =
|
|
4540
|
+
const apps = asArray17(stack.apps);
|
|
3683
4541
|
for (let i = 0; i < apps.length; i++) {
|
|
3684
4542
|
const app = apps[i];
|
|
3685
4543
|
if (!app || typeof app !== "object") continue;
|
|
@@ -3707,8 +4565,8 @@ function validateCapabilityReferences(stack) {
|
|
|
3707
4565
|
|
|
3708
4566
|
// src/validate-approval-approvers.ts
|
|
3709
4567
|
var import_automation4 = require("@objectstack/spec/automation");
|
|
3710
|
-
var
|
|
3711
|
-
var
|
|
4568
|
+
var import_spec2 = require("@objectstack/spec");
|
|
4569
|
+
var import_formula5 = require("@objectstack/formula");
|
|
3712
4570
|
var APPROVAL_APPROVER_NOT_MEMBERSHIP_TIER = "approval-approver-not-membership-tier";
|
|
3713
4571
|
var APPROVAL_APPROVER_TYPE_DEPRECATED = "approval-approver-type-deprecated";
|
|
3714
4572
|
var APPROVAL_APPROVER_TYPE_UNKNOWN = "approval-approver-type-unknown";
|
|
@@ -3722,13 +4580,13 @@ var APPROVAL_APPROVER_CROSS_ORG_UNSUPPORTED = "approval-approver-cross-org-unsup
|
|
|
3722
4580
|
var EXPRESSION_ROOTS = /* @__PURE__ */ new Set(["current", "trigger", "vars"]);
|
|
3723
4581
|
var RESERVED_OUTPUT_KEYS = /* @__PURE__ */ new Set(["decision", "requestId"]);
|
|
3724
4582
|
var GROUP_ROUTED_TYPES = /* @__PURE__ */ new Set(["position", "team", "department"]);
|
|
3725
|
-
var MEMBERSHIP_TIERS = new Set(
|
|
3726
|
-
var MEMBERSHIP_TIER_LIST =
|
|
4583
|
+
var MEMBERSHIP_TIERS = new Set(import_spec2.BUILTIN_MEMBERSHIP_ROLES);
|
|
4584
|
+
var MEMBERSHIP_TIER_LIST = import_spec2.BUILTIN_MEMBERSHIP_ROLES.join("/");
|
|
3727
4585
|
var TYPE_FIX = {
|
|
3728
4586
|
business_unit: "department",
|
|
3729
4587
|
bu: "department"
|
|
3730
4588
|
};
|
|
3731
|
-
function
|
|
4589
|
+
function asArray18(v) {
|
|
3732
4590
|
if (Array.isArray(v)) return v;
|
|
3733
4591
|
if (v && typeof v === "object") {
|
|
3734
4592
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -3738,7 +4596,7 @@ function asArray19(v) {
|
|
|
3738
4596
|
function validateApprovalApprovers(stack) {
|
|
3739
4597
|
const findings = [];
|
|
3740
4598
|
if (!stack || typeof stack !== "object") return findings;
|
|
3741
|
-
const flows =
|
|
4599
|
+
const flows = asArray18(stack.flows);
|
|
3742
4600
|
const validTypes = new Set(import_automation4.ApproverType.options);
|
|
3743
4601
|
for (let fi = 0; fi < flows.length; fi++) {
|
|
3744
4602
|
const flow = flows[fi];
|
|
@@ -3783,7 +4641,7 @@ function validateApprovalApprovers(stack) {
|
|
|
3783
4641
|
hint: `Write a CEL expression over current.* (the record's live state at node entry), trigger.* (the submit-time snapshot) or vars.* (flow variables), e.g. current.approvers_dynamic or vars.approval_lead.picked_departments.`
|
|
3784
4642
|
});
|
|
3785
4643
|
} else {
|
|
3786
|
-
const parsed = (0,
|
|
4644
|
+
const parsed = (0, import_formula5.collectCelRootIdentifiers)(source);
|
|
3787
4645
|
if (!parsed.ok) {
|
|
3788
4646
|
findings.push({
|
|
3789
4647
|
severity: "error",
|
|
@@ -4033,7 +4891,7 @@ var OWD_WIDTH = {
|
|
|
4033
4891
|
public_read: 1,
|
|
4034
4892
|
public_read_write: 2
|
|
4035
4893
|
};
|
|
4036
|
-
function
|
|
4894
|
+
function asArray19(v) {
|
|
4037
4895
|
if (Array.isArray(v)) return v;
|
|
4038
4896
|
if (v && typeof v === "object") {
|
|
4039
4897
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4059,7 +4917,7 @@ function refOf(def) {
|
|
|
4059
4917
|
return typeof r === "string" && r ? r : void 0;
|
|
4060
4918
|
}
|
|
4061
4919
|
function firstMasterDetailField(obj) {
|
|
4062
|
-
for (const f of
|
|
4920
|
+
for (const f of asArray19(obj.fields)) {
|
|
4063
4921
|
if (f.type === "master_detail") {
|
|
4064
4922
|
return { name: String(f.name ?? "?"), parent: refOf(f) };
|
|
4065
4923
|
}
|
|
@@ -4072,8 +4930,8 @@ function grantsObjectAccess(p) {
|
|
|
4072
4930
|
function validateSecurityPosture(stack, opts) {
|
|
4073
4931
|
const findings = [];
|
|
4074
4932
|
if (!stack || typeof stack !== "object") return findings;
|
|
4075
|
-
const objects =
|
|
4076
|
-
const permissionSets =
|
|
4933
|
+
const objects = asArray19(stack.objects);
|
|
4934
|
+
const permissionSets = asArray19(stack.permissions);
|
|
4077
4935
|
for (let i = 0; i < objects.length; i++) {
|
|
4078
4936
|
const obj = objects[i];
|
|
4079
4937
|
if (!obj || typeof obj !== "object") continue;
|
|
@@ -4202,10 +5060,10 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4202
5060
|
if (!obj || typeof obj !== "object" || isSystemObject(obj)) continue;
|
|
4203
5061
|
const objName = typeof obj.name === "string" ? obj.name : `(object ${i})`;
|
|
4204
5062
|
flagRole("object", obj.name, obj.label, `object "${objName}"`, `objects[${i}].name`);
|
|
4205
|
-
for (const f of
|
|
5063
|
+
for (const f of asArray19(obj.fields)) {
|
|
4206
5064
|
flagRole("field", f.name, f.label, `field "${objName}.${String(f.name ?? "?")}"`, `objects[${i}].fields.${String(f.name ?? "?")}.name`);
|
|
4207
5065
|
}
|
|
4208
|
-
for (const [ai, action] of
|
|
5066
|
+
for (const [ai, action] of asArray19(obj.actions).entries()) {
|
|
4209
5067
|
flagRole("action", action.name, action.label, `action "${objName}.${String(action.name ?? "?")}"`, `objects[${i}].actions[${ai}].name`);
|
|
4210
5068
|
}
|
|
4211
5069
|
}
|
|
@@ -4214,19 +5072,19 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4214
5072
|
if (!ps || typeof ps !== "object") continue;
|
|
4215
5073
|
flagRole("permission set", ps.name, ps.label, `permission set "${String(ps.name ?? i)}"`, `permissions[${i}].name`);
|
|
4216
5074
|
}
|
|
4217
|
-
for (const [i, pos] of
|
|
5075
|
+
for (const [i, pos] of asArray19(stack.positions).entries()) {
|
|
4218
5076
|
flagRole("position", pos.name, pos.label, `position "${String(pos.name ?? i)}"`, `positions[${i}].name`);
|
|
4219
5077
|
}
|
|
4220
|
-
for (const [i, app] of
|
|
5078
|
+
for (const [i, app] of asArray19(stack.apps).entries()) {
|
|
4221
5079
|
flagRole("app", app.name, app.label, `app "${String(app.name ?? i)}"`, `apps[${i}].name`);
|
|
4222
5080
|
}
|
|
4223
|
-
for (const [i, book] of
|
|
5081
|
+
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
4224
5082
|
flagRole("book", book.name, book.label, `book "${String(book.name ?? i)}"`, `books[${i}].name`);
|
|
4225
5083
|
}
|
|
4226
5084
|
const stackSetNames = new Set(
|
|
4227
5085
|
permissionSets.map((ps) => typeof ps.name === "string" ? ps.name : void 0).filter((n) => !!n)
|
|
4228
5086
|
);
|
|
4229
|
-
for (const [i, book] of
|
|
5087
|
+
for (const [i, book] of asArray19(stack.books).entries()) {
|
|
4230
5088
|
const audience = book.audience;
|
|
4231
5089
|
if (!audience || typeof audience !== "object") continue;
|
|
4232
5090
|
const setName = audience.permissionSet;
|
|
@@ -4304,7 +5162,7 @@ function validateSecurityPosture(stack, opts) {
|
|
|
4304
5162
|
}
|
|
4305
5163
|
const GRANT_SEED_OBJECTS = /* @__PURE__ */ new Set(["sys_user_position", "sys_user_permission_set"]);
|
|
4306
5164
|
const nowMs = opts?.nowMs ?? Date.now();
|
|
4307
|
-
for (const [i, seed] of
|
|
5165
|
+
for (const [i, seed] of asArray19(stack.data).entries()) {
|
|
4308
5166
|
const seedObject = typeof seed.object === "string" ? seed.object : "";
|
|
4309
5167
|
if (!GRANT_SEED_OBJECTS.has(seedObject)) continue;
|
|
4310
5168
|
const records = Array.isArray(seed.records) ? seed.records : [];
|
|
@@ -4349,7 +5207,7 @@ var ORG_AXIS_PERMISSION_INHERITANCE = "org-axis-permission-inheritance";
|
|
|
4349
5207
|
var ORG_AXIS_CROSS_ORG_BU_GRANT = "org-axis-cross-org-bu-grant";
|
|
4350
5208
|
var ORG_PARENT_FIELD = "parent_organization_id";
|
|
4351
5209
|
var BU_TREE_RECIPIENT_TYPES = /* @__PURE__ */ new Set(["business_unit", "unit_and_subordinates"]);
|
|
4352
|
-
function
|
|
5210
|
+
function asArray20(v) {
|
|
4353
5211
|
if (Array.isArray(v)) return v;
|
|
4354
5212
|
if (v && typeof v === "object") {
|
|
4355
5213
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4379,9 +5237,9 @@ var INHERITANCE_HINT = `Remove the ${ORG_PARENT_FIELD} reference. Cross-organiza
|
|
|
4379
5237
|
function validateOrgAxisRedLines(stack) {
|
|
4380
5238
|
const findings = [];
|
|
4381
5239
|
const cfg = stack ?? {};
|
|
4382
|
-
const permissionSets =
|
|
5240
|
+
const permissionSets = asArray20(cfg.permissions);
|
|
4383
5241
|
permissionSets.forEach((ps, psIndex) => {
|
|
4384
|
-
|
|
5242
|
+
asArray20(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4385
5243
|
for (const clause of ["using", "check"]) {
|
|
4386
5244
|
if (!str(policy[clause]).includes(ORG_PARENT_FIELD)) continue;
|
|
4387
5245
|
findings.push({
|
|
@@ -4395,7 +5253,7 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4395
5253
|
}
|
|
4396
5254
|
});
|
|
4397
5255
|
});
|
|
4398
|
-
|
|
5256
|
+
asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4399
5257
|
const slots = [
|
|
4400
5258
|
{ key: "condition", text: expressionText(rule.condition) },
|
|
4401
5259
|
{ key: "sharedWith", text: JSON.stringify(rule.sharedWith ?? "") ?? "" }
|
|
@@ -4413,9 +5271,9 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4413
5271
|
}
|
|
4414
5272
|
});
|
|
4415
5273
|
const tenancyDisabledObjects = new Set(
|
|
4416
|
-
|
|
5274
|
+
asArray20(cfg.objects).filter((o) => isTenancyDisabled(o)).map((o) => str(o.name)).filter(Boolean)
|
|
4417
5275
|
);
|
|
4418
|
-
|
|
5276
|
+
asArray20(cfg.sharingRules).forEach((rule, rIndex) => {
|
|
4419
5277
|
const target = str(rule.object);
|
|
4420
5278
|
if (!target || !tenancyDisabledObjects.has(target)) return;
|
|
4421
5279
|
const sharedWith = rule.sharedWith;
|
|
@@ -4435,10 +5293,10 @@ function validateOrgAxisRedLines(stack) {
|
|
|
4435
5293
|
}
|
|
4436
5294
|
|
|
4437
5295
|
// src/validate-sharing-rule-enforceability.ts
|
|
4438
|
-
var
|
|
5296
|
+
var import_formula6 = require("@objectstack/formula");
|
|
4439
5297
|
var SHARING_RULE_UNLOWERABLE_CONDITION = "sharing-rule-unlowerable-condition";
|
|
4440
5298
|
var SHARING_RULE_RUNTIME_VARIABLE_CONDITION = "sharing-rule-runtime-variable-condition";
|
|
4441
|
-
function
|
|
5299
|
+
function asArray21(v) {
|
|
4442
5300
|
if (Array.isArray(v)) return v;
|
|
4443
5301
|
if (v && typeof v === "object") {
|
|
4444
5302
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4465,10 +5323,10 @@ var PUSHDOWN_SUBSET = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in
|
|
|
4465
5323
|
function validateSharingRuleEnforceability(stack) {
|
|
4466
5324
|
const findings = [];
|
|
4467
5325
|
const cfg = stack ?? {};
|
|
4468
|
-
|
|
5326
|
+
asArray21(cfg.sharingRules).forEach((rule, index) => {
|
|
4469
5327
|
const input = toCompilerInput(rule.condition);
|
|
4470
5328
|
if (input === null) return;
|
|
4471
|
-
const result = (0,
|
|
5329
|
+
const result = (0, import_formula6.compileCelToFilter)(input, { variables: {} });
|
|
4472
5330
|
if (result.ok) return;
|
|
4473
5331
|
if (result.reason === "parse-error") return;
|
|
4474
5332
|
const name = str2(rule.name) || String(index);
|
|
@@ -4501,10 +5359,11 @@ function validateSharingRuleEnforceability(stack) {
|
|
|
4501
5359
|
}
|
|
4502
5360
|
|
|
4503
5361
|
// src/validate-rls-predicate-enforceability.ts
|
|
4504
|
-
var
|
|
5362
|
+
var import_formula7 = require("@objectstack/formula");
|
|
4505
5363
|
var RLS_PREDICATE_UNENFORCEABLE = "rls-predicate-unenforceable";
|
|
4506
5364
|
var RLS_PREDICATE_UNPARSEABLE = "rls-predicate-unparseable";
|
|
4507
|
-
|
|
5365
|
+
var RLS_PREDICATE_OVER_BUDGET = "rls-predicate-over-budget";
|
|
5366
|
+
function asArray22(v) {
|
|
4508
5367
|
if (Array.isArray(v)) return v;
|
|
4509
5368
|
if (v && typeof v === "object") {
|
|
4510
5369
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4515,6 +5374,13 @@ function str3(v) {
|
|
|
4515
5374
|
return typeof v === "string" ? v : "";
|
|
4516
5375
|
}
|
|
4517
5376
|
var PUSHDOWN_SUBSET2 = "The lowerable subset is: `==` `!=` `>` `<` `>=` `<=`, `in`, `&&` `||` `!`, `== null` / `!= null`, and the string methods `startsWith` / `endsWith` / `contains` \u2014 over SINGLE-column field paths (ADR-0058 D2), compared against a literal or a `current_user.*` value.";
|
|
5377
|
+
function boundsOverrunOf(bridged) {
|
|
5378
|
+
const parsed = (0, import_formula7.parseCelToAstWithReason)(bridged);
|
|
5379
|
+
return !parsed.ok && parsed.kind === "bounds" ? parsed.overrun : null;
|
|
5380
|
+
}
|
|
5381
|
+
function quote(source) {
|
|
5382
|
+
return source.length > 200 ? `${source.slice(0, 197)}...` : source;
|
|
5383
|
+
}
|
|
4518
5384
|
function consequence(clause) {
|
|
4519
5385
|
const dropped = 'so `RLSCompiler` DROPS the policy at request time (one WARN line \u2014 "has an uncompilable predicate \u2026 and was DROPPED (no enforcement)" \u2014 is the only signal, and nothing reports it at authoring time). ';
|
|
4520
5386
|
return clause === "using" ? dropped + "When it is the only applicable policy for that object and operation, `compileFilter` returns the `RLS_DENY_FILTER` sentinel instead, which is AND-ed onto the where clause: every select / update / delete on the object matches ZERO rows. When other policies also apply, this one just vanishes from the OR and grants none of the access it appears to." : dropped + "On the ADR-0058 D4 write path that leaves the post-image `check` as the `RLS_DENY_FILTER` sentinel, which no record can satisfy: every insert / update the policy governs fails with `PermissionDeniedError`. The policy reads as a write rule and behaves as a blanket refusal.";
|
|
@@ -4522,20 +5388,36 @@ function consequence(clause) {
|
|
|
4522
5388
|
function validateRlsPredicateEnforceability(stack) {
|
|
4523
5389
|
const findings = [];
|
|
4524
5390
|
const cfg = stack ?? {};
|
|
4525
|
-
|
|
4526
|
-
|
|
5391
|
+
asArray22(cfg.permissions).forEach((ps, psIndex) => {
|
|
5392
|
+
asArray22(ps.rowLevelSecurity).forEach((policy, pIndex) => {
|
|
4527
5393
|
for (const clause of ["using", "check"]) {
|
|
4528
5394
|
const source = str3(policy[clause]);
|
|
4529
5395
|
if (!source.trim()) continue;
|
|
4530
|
-
if ((0,
|
|
4531
|
-
const
|
|
5396
|
+
if ((0, import_formula7.isSupportedRlsExpression)(source)) continue;
|
|
5397
|
+
const bridged = (0, import_formula7.sqlPredicateToCel)(source);
|
|
5398
|
+
const why = (0, import_formula7.isPushdownableCel)(bridged);
|
|
4532
5399
|
const detail = why.ok ? "" : why.detail;
|
|
4533
5400
|
const parseError = !why.ok && why.reason === "parse-error";
|
|
5401
|
+
const overrun = parseError ? boundsOverrunOf(bridged) : null;
|
|
4534
5402
|
const psName = str3(ps.name) || String(psIndex);
|
|
4535
5403
|
const policyName = str3(policy.name) || String(pIndex);
|
|
4536
5404
|
const object = str3(policy.object);
|
|
4537
5405
|
const where = `permission set "${psName}" policy "${policyName}"` + (object ? ` on object "${object}"` : "");
|
|
4538
5406
|
const path = `permissions[${psIndex}].rowLevelSecurity[${pIndex}].${clause}`;
|
|
5407
|
+
if (overrun) {
|
|
5408
|
+
const bound = overrun.limit ?? "an unnamed platform CEL bound";
|
|
5409
|
+
const budget = overrun.limitValue !== null ? ` (platform limit ${overrun.limitValue})` : "";
|
|
5410
|
+
const measured = overrun.measured !== null ? `, this predicate measures ${overrun.measured}` : "";
|
|
5411
|
+
findings.push({
|
|
5412
|
+
severity: "error",
|
|
5413
|
+
rule: RLS_PREDICATE_OVER_BUDGET,
|
|
5414
|
+
where,
|
|
5415
|
+
path,
|
|
5416
|
+
message: `RLS ${clause} \`${quote(source)}\` is syntactically valid, lowerable CEL but overruns the platform parse bound ${bound}${budget}${measured} (${overrun.summary}), ` + consequence(clause),
|
|
5417
|
+
hint: `There is no syntax or dialect error to correct here \u2014 the predicate is well-formed CEL and is simply too large for ${bound}${budget}, so the fix is to make it smaller or to move the work off the predicate. (1) Collapse a long \`field == a || field == b || \u2026\` chain into a single \`field in [a, b, \u2026]\`, which is far fewer AST nodes (\`maxListElements\` is 64, so a very large set needs option 2). (2) Pre-resolve the set into a membership key the runtime exposes and test \`field in current_user.<key>\` (ADR-0105 D11) \u2014 one comparison whatever the set size. (3) Denormalise a repeated sub-expression onto this object as a formula/rollup field and test that single column. (4) Split a TOP-LEVEL \`||\` across several \`rowLevelSecurity\` policies: applicable policies are OR-ed, so that is equivalent \u2014 but never split a top-level \`&&\` this way, which would WIDEN access rather than preserve it. Logic genuinely this large is not a row filter: move it to a hook or action body (\`ScriptBody { language: 'js' }\`, the L2 sandboxed surface).`
|
|
5418
|
+
});
|
|
5419
|
+
continue;
|
|
5420
|
+
}
|
|
4539
5421
|
if (parseError) {
|
|
4540
5422
|
findings.push({
|
|
4541
5423
|
severity: "error",
|
|
@@ -4564,14 +5446,14 @@ function validateRlsPredicateEnforceability(stack) {
|
|
|
4564
5446
|
// src/validate-dashboard-action-refs.ts
|
|
4565
5447
|
var DASHBOARD_ACTION_TARGET_UNDEFINED = "dashboard-action-target-undefined";
|
|
4566
5448
|
var DASHBOARD_ACTION_ROUTE_UNRESOLVED = "dashboard-action-route-unresolved";
|
|
4567
|
-
function
|
|
5449
|
+
function asArray23(v) {
|
|
4568
5450
|
if (Array.isArray(v)) return v;
|
|
4569
5451
|
if (v && typeof v === "object") {
|
|
4570
5452
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4571
5453
|
}
|
|
4572
5454
|
return [];
|
|
4573
5455
|
}
|
|
4574
|
-
function
|
|
5456
|
+
function strName7(v) {
|
|
4575
5457
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4576
5458
|
}
|
|
4577
5459
|
var MODAL_VERB_RE = /^(?:create|new|add|edit|update)_(.+)$/;
|
|
@@ -4588,7 +5470,7 @@ var URL_COLLECTION_TO_STACK_KEY = {
|
|
|
4588
5470
|
views: "views"
|
|
4589
5471
|
};
|
|
4590
5472
|
function viewContainerName(item) {
|
|
4591
|
-
return
|
|
5473
|
+
return strName7(item.name) ?? strName7(item.id) ?? strName7(item.object) ?? strName7(item.list?.data && item.list.data.object) ?? strName7(item.form?.data && item.form.data.object);
|
|
4592
5474
|
}
|
|
4593
5475
|
function collectKnownTargets(stack) {
|
|
4594
5476
|
const actions = /* @__PURE__ */ new Set();
|
|
@@ -4598,22 +5480,22 @@ function collectKnownTargets(stack) {
|
|
|
4598
5480
|
const pages = /* @__PURE__ */ new Set();
|
|
4599
5481
|
const views = /* @__PURE__ */ new Set();
|
|
4600
5482
|
const collectNames = (v, into, name) => {
|
|
4601
|
-
for (const item of
|
|
5483
|
+
for (const item of asArray23(v)) {
|
|
4602
5484
|
if (!item || typeof item !== "object") continue;
|
|
4603
5485
|
const n = name(item);
|
|
4604
5486
|
if (n) into.add(n);
|
|
4605
5487
|
}
|
|
4606
5488
|
};
|
|
4607
|
-
collectNames(stack.actions, actions, (a) =>
|
|
4608
|
-
for (const obj of
|
|
5489
|
+
collectNames(stack.actions, actions, (a) => strName7(a.name));
|
|
5490
|
+
for (const obj of asArray23(stack.objects)) {
|
|
4609
5491
|
if (!obj || typeof obj !== "object") continue;
|
|
4610
|
-
const n =
|
|
5492
|
+
const n = strName7(obj.name);
|
|
4611
5493
|
if (n) objects.add(n);
|
|
4612
|
-
collectNames(obj.actions, actions, (a) =>
|
|
5494
|
+
collectNames(obj.actions, actions, (a) => strName7(a.name));
|
|
4613
5495
|
}
|
|
4614
|
-
collectNames(stack.reports, reports, (r) =>
|
|
4615
|
-
collectNames(stack.dashboards, dashboards, (d) =>
|
|
4616
|
-
collectNames(stack.pages, pages, (p) =>
|
|
5496
|
+
collectNames(stack.reports, reports, (r) => strName7(r.name));
|
|
5497
|
+
collectNames(stack.dashboards, dashboards, (d) => strName7(d.name));
|
|
5498
|
+
collectNames(stack.pages, pages, (p) => strName7(p.name));
|
|
4617
5499
|
collectNames(stack.views, views, viewContainerName);
|
|
4618
5500
|
for (const o of objects) views.add(o);
|
|
4619
5501
|
return { actions, objects, reports, dashboards, pages, views };
|
|
@@ -4645,14 +5527,14 @@ function resolveUrlRoute(target, known) {
|
|
|
4645
5527
|
function validateDashboardActionRefs(stack) {
|
|
4646
5528
|
const findings = [];
|
|
4647
5529
|
if (!stack || typeof stack !== "object") return findings;
|
|
4648
|
-
const dashboards =
|
|
5530
|
+
const dashboards = asArray23(stack.dashboards);
|
|
4649
5531
|
if (dashboards.length === 0) return findings;
|
|
4650
5532
|
const known = collectKnownTargets(stack);
|
|
4651
5533
|
const checkOne = (action, where, path) => {
|
|
4652
|
-
const target =
|
|
5534
|
+
const target = strName7(action.actionUrl);
|
|
4653
5535
|
if (!target) return;
|
|
4654
5536
|
if (target.includes("${")) return;
|
|
4655
|
-
const actionType =
|
|
5537
|
+
const actionType = strName7(action.actionType) ?? "url";
|
|
4656
5538
|
if (actionType === "script" || actionType === "modal") {
|
|
4657
5539
|
if (resolveActionTarget(actionType, target, known)) return;
|
|
4658
5540
|
const kindWord = actionType === "script" ? "script" : "modal";
|
|
@@ -4683,13 +5565,13 @@ function validateDashboardActionRefs(stack) {
|
|
|
4683
5565
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4684
5566
|
const dash = dashboards[di];
|
|
4685
5567
|
if (!dash || typeof dash !== "object") continue;
|
|
4686
|
-
const dashName =
|
|
5568
|
+
const dashName = strName7(dash.name) ?? `(dashboard ${di})`;
|
|
4687
5569
|
const dashPath = `dashboards[${di}]`;
|
|
4688
|
-
const headerActions =
|
|
5570
|
+
const headerActions = asArray23(dash.header?.actions);
|
|
4689
5571
|
for (let ai = 0; ai < headerActions.length; ai++) {
|
|
4690
5572
|
const action = headerActions[ai];
|
|
4691
5573
|
if (!action || typeof action !== "object") continue;
|
|
4692
|
-
const label2 =
|
|
5574
|
+
const label2 = strName7(action.label) ?? strName7(action.actionUrl) ?? `#${ai}`;
|
|
4693
5575
|
checkOne(
|
|
4694
5576
|
action,
|
|
4695
5577
|
`dashboard "${dashName}" \xB7 header action "${label2}"`,
|
|
@@ -4702,9 +5584,10 @@ function validateDashboardActionRefs(stack) {
|
|
|
4702
5584
|
|
|
4703
5585
|
// src/validate-filter-tokens.ts
|
|
4704
5586
|
var import_data6 = require("@objectstack/spec/data");
|
|
4705
|
-
|
|
5587
|
+
|
|
5588
|
+
// src/filter-walk.ts
|
|
4706
5589
|
var FILTER_KEYS = /* @__PURE__ */ new Set(["filter", "filters", "runtimeFilter"]);
|
|
4707
|
-
function
|
|
5590
|
+
function asArray24(v) {
|
|
4708
5591
|
if (Array.isArray(v)) return v;
|
|
4709
5592
|
if (v && typeof v === "object") {
|
|
4710
5593
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -4714,7 +5597,62 @@ function asArray25(v) {
|
|
|
4714
5597
|
function label(v, fallback) {
|
|
4715
5598
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
4716
5599
|
}
|
|
5600
|
+
function scanForFilters(node, path, where, visit, seen = /* @__PURE__ */ new Set()) {
|
|
5601
|
+
if (!node || typeof node !== "object") return;
|
|
5602
|
+
if (seen.has(node)) return;
|
|
5603
|
+
seen.add(node);
|
|
5604
|
+
if (Array.isArray(node)) {
|
|
5605
|
+
node.forEach((v, i) => scanForFilters(v, `${path}[${i}]`, where, visit, seen));
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
for (const [k, v] of Object.entries(node)) {
|
|
5609
|
+
const childPath = `${path}.${k}`;
|
|
5610
|
+
if (FILTER_KEYS.has(k)) {
|
|
5611
|
+
visit({ value: v, path: childPath, where });
|
|
5612
|
+
continue;
|
|
5613
|
+
}
|
|
5614
|
+
scanForFilters(v, childPath, where, visit, seen);
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5617
|
+
function walkAuthoredFilters(stack, surfaces, visit) {
|
|
5618
|
+
if (!stack || typeof stack !== "object") return;
|
|
5619
|
+
for (const { key, kind } of surfaces) {
|
|
5620
|
+
const items = asArray24(stack[key]);
|
|
5621
|
+
items.forEach((item, i) => {
|
|
5622
|
+
const name = label(item.name ?? item.id, `#${i}`);
|
|
5623
|
+
if (kind === "dashboard") {
|
|
5624
|
+
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
5625
|
+
widgets.forEach((w, wi) => {
|
|
5626
|
+
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
5627
|
+
scanForFilters(
|
|
5628
|
+
w,
|
|
5629
|
+
`${key}[${i}].widgets[${wi}]`,
|
|
5630
|
+
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
5631
|
+
visit,
|
|
5632
|
+
/* @__PURE__ */ new Set()
|
|
5633
|
+
);
|
|
5634
|
+
});
|
|
5635
|
+
const { widgets: _skip, ...rest } = item;
|
|
5636
|
+
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
5637
|
+
return;
|
|
5638
|
+
}
|
|
5639
|
+
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, visit, /* @__PURE__ */ new Set());
|
|
5640
|
+
});
|
|
5641
|
+
}
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
// src/validate-filter-tokens.ts
|
|
5645
|
+
var FILTER_TOKEN_UNKNOWN = "filter-token-unknown";
|
|
4717
5646
|
var KNOWN_LIST = import_data6.CONTEXT_TOKENS.join("}, {");
|
|
5647
|
+
var TOKEN_FILTER_SURFACES = [
|
|
5648
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
5649
|
+
{ key: "objects", kind: "object" },
|
|
5650
|
+
{ key: "views", kind: "view" },
|
|
5651
|
+
{ key: "reports", kind: "report" },
|
|
5652
|
+
{ key: "datasets", kind: "dataset" },
|
|
5653
|
+
{ key: "pages", kind: "page" },
|
|
5654
|
+
{ key: "apps", kind: "app" }
|
|
5655
|
+
];
|
|
4718
5656
|
function walkFilterValues(node, path, where, out, seen) {
|
|
4719
5657
|
if (node === null || node === void 0) return;
|
|
4720
5658
|
if (typeof node === "string") {
|
|
@@ -4743,58 +5681,132 @@ function walkFilterValues(node, path, where, out, seen) {
|
|
|
4743
5681
|
walkFilterValues(v, `${path}.${k}`, where, out, seen);
|
|
4744
5682
|
}
|
|
4745
5683
|
}
|
|
4746
|
-
function
|
|
4747
|
-
if (!
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
5684
|
+
function validateFilterTokens(stack) {
|
|
5685
|
+
if (!stack || typeof stack !== "object") return [];
|
|
5686
|
+
const out = [];
|
|
5687
|
+
walkAuthoredFilters(stack, TOKEN_FILTER_SURFACES, ({ value, path, where }) => {
|
|
5688
|
+
walkFilterValues(value, path, where, out, /* @__PURE__ */ new Set());
|
|
5689
|
+
});
|
|
5690
|
+
return out;
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
// src/validate-empty-combinators.ts
|
|
5694
|
+
var import_data7 = require("@objectstack/spec/data");
|
|
5695
|
+
var FILTER_EMPTY_COMBINATOR = "filter-empty-combinator";
|
|
5696
|
+
var FILTER_EMPTY_NODE = "filter-empty-node";
|
|
5697
|
+
var EMPTY_COMBINATOR_SURFACES = [
|
|
5698
|
+
{ key: "dashboards", kind: "dashboard" },
|
|
5699
|
+
{ key: "objects", kind: "object" },
|
|
5700
|
+
{ key: "views", kind: "view" },
|
|
5701
|
+
{ key: "reports", kind: "report" },
|
|
5702
|
+
{ key: "datasets", kind: "dataset" },
|
|
5703
|
+
{ key: "pages", kind: "page" },
|
|
5704
|
+
{ key: "apps", kind: "app" },
|
|
5705
|
+
{ key: "flows", kind: "flow" }
|
|
5706
|
+
];
|
|
5707
|
+
function isFilterNode(value) {
|
|
5708
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
5709
|
+
const proto = Object.getPrototypeOf(value);
|
|
5710
|
+
return proto === Object.prototype || proto === null;
|
|
5711
|
+
}
|
|
5712
|
+
var VERDICT_OF = {
|
|
5713
|
+
$and: (0, import_data7.reduceFilterVerdict)({ $and: [] }),
|
|
5714
|
+
$or: (0, import_data7.reduceFilterVerdict)({ $or: [] }),
|
|
5715
|
+
$not: (0, import_data7.reduceFilterVerdict)({ $not: {} }),
|
|
5716
|
+
node: (0, import_data7.reduceFilterVerdict)({}),
|
|
5717
|
+
/** One TRUE disjunct absorbs its `$or`: the sibling branches stop mattering. */
|
|
5718
|
+
orWithEmptyBranch: (0, import_data7.reduceFilterVerdict)({ $or: [{ status: "open" }, {}] })
|
|
5719
|
+
};
|
|
5720
|
+
function rows(verdict) {
|
|
5721
|
+
if (verdict === "true") return "matches EVERY row";
|
|
5722
|
+
if (verdict === "false") return "matches NO row";
|
|
5723
|
+
return "carries a real predicate";
|
|
5724
|
+
}
|
|
5725
|
+
var MATCH_NONE_SPELLING = "If you really do want a predicate that selects nothing, `{ <field>: { $in: [] } }` is the declared spelling for it (an empty `$in` list matches nothing, on every backend) \u2014 it says so where an empty combinator only implies it.";
|
|
5726
|
+
var OMIT_THE_KEY = 'To express "no filter", DELETE the key \u2014 an absent `filter` and a filter that reduces to TRUE run identically, and only the absent key says so to the next reader (and to the next AI author that copies this metadata).';
|
|
5727
|
+
function emitEmptyCombinator(key, path, ctx) {
|
|
5728
|
+
const spelling = key === "$not" ? "`$not: {}`" : `\`${key}: []\``;
|
|
5729
|
+
const message = key === "$and" ? `\`$and: []\` is a conjunction of ZERO conditions. Under the #5322 identity ruling it ${rows(VERDICT_OF.$and)} \u2014 the key is authored, and it constrains nothing, so this surface reads as filtered and is not.` : key === "$or" ? `\`$or: []\` is a disjunction of ZERO branches. Under the #5322 identity ruling it ${rows(VERDICT_OF.$or)}: this surface renders permanently empty, and on a read scope it hides every row (fail-closed by design \u2014 #5134).` : `\`$not: {}\` negates an EMPTY node. An empty node is TRUE and NOT TRUE is FALSE, so it ${rows(VERDICT_OF.$not)} \u2014 the opposite of the "no filter" an empty operand looks like.`;
|
|
5730
|
+
const hint = key === "$and" ? `${OMIT_THE_KEY} To express a constraint, put the conditions in the array. ${MATCH_NONE_SPELLING}` : key === "$or" ? `If you meant "no filter", this is its OPPOSITE: emptying the array does not relax the filter, it closes it. ${OMIT_THE_KEY} If you meant to offer alternatives, put the branches in the array. ${MATCH_NONE_SPELLING}` : `Put the condition you are negating inside \`$not\` (\`{ $not: { status: 'closed' } }\`). ${OMIT_THE_KEY} ${MATCH_NONE_SPELLING}`;
|
|
5731
|
+
ctx.out.push({
|
|
5732
|
+
severity: "error",
|
|
5733
|
+
rule: FILTER_EMPTY_COMBINATOR,
|
|
5734
|
+
where: ctx.where,
|
|
5735
|
+
path,
|
|
5736
|
+
message: `${message} A literal ${spelling} is not an authoring surface (#5330).`,
|
|
5737
|
+
hint: `${hint} A PROGRAMMATIC producer that loops to zero operands keeps the runtime identity unchanged \u2014 this rule judges only what is written in the metadata.`
|
|
5738
|
+
});
|
|
5739
|
+
}
|
|
5740
|
+
function emitEmptyNode(position, path, ctx) {
|
|
5741
|
+
if (position === "root") {
|
|
5742
|
+
ctx.out.push({
|
|
5743
|
+
severity: "error",
|
|
5744
|
+
rule: FILTER_EMPTY_NODE,
|
|
5745
|
+
where: ctx.where,
|
|
5746
|
+
path,
|
|
5747
|
+
message: `An EMPTY filter node (\`{}\`) is authored here. Under the #5322 identity ruling an empty node is TRUE \u2014 it ${rows(VERDICT_OF.node)}, exactly as if the key were absent \u2014 so a filter is declared and enforces nothing.`,
|
|
5748
|
+
hint: `${OMIT_THE_KEY} If you meant to constrain something, write the condition into the node. ${MATCH_NONE_SPELLING}`
|
|
5749
|
+
});
|
|
4752
5750
|
return;
|
|
4753
5751
|
}
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
5752
|
+
if (position === "or-branch") {
|
|
5753
|
+
ctx.out.push({
|
|
5754
|
+
severity: "error",
|
|
5755
|
+
rule: FILTER_EMPTY_NODE,
|
|
5756
|
+
where: ctx.where,
|
|
5757
|
+
path,
|
|
5758
|
+
message: `An EMPTY branch (\`{}\`) of a \`$or\`. An empty node is TRUE, and one TRUE disjunct ABSORBS the whole disjunction (\`{ $or: [{ status: 'open' }, {}] }\` ${rows(VERDICT_OF.orWithEmptyBranch)}), so every branch you wrote beside it is dead.`,
|
|
5759
|
+
hint: "Delete the empty branch \u2014 the `$or` then means what it looks like. If it was meant to carry a condition, write it. (A compiler that DROPPED the empty branch instead would silently NARROW the scope to the surviving branches, which is why the runtime absorbs rather than filters \u2014 #5297.)"
|
|
5760
|
+
});
|
|
5761
|
+
return;
|
|
5762
|
+
}
|
|
5763
|
+
ctx.out.push({
|
|
5764
|
+
severity: "error",
|
|
5765
|
+
rule: FILTER_EMPTY_NODE,
|
|
5766
|
+
where: ctx.where,
|
|
5767
|
+
path,
|
|
5768
|
+
message: "An EMPTY branch (`{}`) of a `$and`. An empty node is TRUE \u2014 the AND identity \u2014 so the branch contributes no condition and the conjunction means whatever its other branches mean.",
|
|
5769
|
+
hint: "Delete the empty branch, or write the condition it was meant to carry. A branch that constrains nothing is indistinguishable from one whose condition was lost in an edit."
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
function scanNodeKeys(node, path, ctx) {
|
|
5773
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5774
|
+
if (key === "$and" || key === "$or") {
|
|
5775
|
+
if (!Array.isArray(value)) continue;
|
|
5776
|
+
if (value.length === 0) {
|
|
5777
|
+
emitEmptyCombinator(key, `${path}.${key}`, ctx);
|
|
5778
|
+
continue;
|
|
5779
|
+
}
|
|
5780
|
+
value.forEach((element, index) => {
|
|
5781
|
+
scanBranch(element, `${path}.${key}[${index}]`, key === "$and" ? "and-branch" : "or-branch", ctx);
|
|
5782
|
+
});
|
|
5783
|
+
continue;
|
|
5784
|
+
}
|
|
5785
|
+
if (key === "$not") {
|
|
5786
|
+
if (!isFilterNode(value)) continue;
|
|
5787
|
+
if (Object.keys(value).length === 0) {
|
|
5788
|
+
emitEmptyCombinator("$not", `${path}.$not`, ctx);
|
|
5789
|
+
continue;
|
|
5790
|
+
}
|
|
5791
|
+
scanNodeKeys(value, `${path}.$not`, ctx);
|
|
4758
5792
|
continue;
|
|
4759
5793
|
}
|
|
4760
|
-
scanForFilters(v, childPath, where, out, seen);
|
|
4761
5794
|
}
|
|
4762
5795
|
}
|
|
4763
|
-
function
|
|
5796
|
+
function scanBranch(value, path, position, ctx) {
|
|
5797
|
+
if (!isFilterNode(value)) return;
|
|
5798
|
+
if (Object.keys(value).length === 0) {
|
|
5799
|
+
emitEmptyNode(position, path, ctx);
|
|
5800
|
+
return;
|
|
5801
|
+
}
|
|
5802
|
+
scanNodeKeys(value, path, ctx);
|
|
5803
|
+
}
|
|
5804
|
+
function validateEmptyCombinators(stack) {
|
|
4764
5805
|
if (!stack || typeof stack !== "object") return [];
|
|
4765
5806
|
const out = [];
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
["views", "view"],
|
|
4770
|
-
["reports", "report"],
|
|
4771
|
-
["datasets", "dataset"],
|
|
4772
|
-
["pages", "page"],
|
|
4773
|
-
["apps", "app"]
|
|
4774
|
-
];
|
|
4775
|
-
for (const [key, kind] of surfaces) {
|
|
4776
|
-
const items = asArray25(stack[key]);
|
|
4777
|
-
items.forEach((item, i) => {
|
|
4778
|
-
const name = label(item.name ?? item.id, `#${i}`);
|
|
4779
|
-
if (kind === "dashboard") {
|
|
4780
|
-
const widgets = Array.isArray(item.widgets) ? item.widgets : [];
|
|
4781
|
-
widgets.forEach((w, wi) => {
|
|
4782
|
-
const wName = label(w.id ?? w.title, `#${wi}`);
|
|
4783
|
-
scanForFilters(
|
|
4784
|
-
w,
|
|
4785
|
-
`${key}[${i}].widgets[${wi}]`,
|
|
4786
|
-
`dashboard "${name}" \xB7 widget "${wName}"`,
|
|
4787
|
-
out,
|
|
4788
|
-
/* @__PURE__ */ new Set()
|
|
4789
|
-
);
|
|
4790
|
-
});
|
|
4791
|
-
const { widgets: _skip, ...rest } = item;
|
|
4792
|
-
scanForFilters(rest, `${key}[${i}]`, `dashboard "${name}"`, out, /* @__PURE__ */ new Set());
|
|
4793
|
-
return;
|
|
4794
|
-
}
|
|
4795
|
-
scanForFilters(item, `${key}[${i}]`, `${kind} "${name}"`, out, /* @__PURE__ */ new Set());
|
|
4796
|
-
});
|
|
4797
|
-
}
|
|
5807
|
+
walkAuthoredFilters(stack, EMPTY_COMBINATOR_SURFACES, ({ value, path, where }) => {
|
|
5808
|
+
scanBranch(value, path, "root", { where, out });
|
|
5809
|
+
});
|
|
4798
5810
|
return out;
|
|
4799
5811
|
}
|
|
4800
5812
|
|
|
@@ -4803,14 +5815,14 @@ var import_system2 = require("@objectstack/spec/system");
|
|
|
4803
5815
|
var PLATFORM_NAMES = [...import_system2.PLATFORM_PROVIDED_OBJECT_NAMES];
|
|
4804
5816
|
var OBJECT_REFERENCE_UNKNOWN = "object-reference-unknown";
|
|
4805
5817
|
var OBJECT_REFERENCE_UNREGISTERED_PLATFORM = "object-reference-unregistered-platform";
|
|
4806
|
-
function
|
|
5818
|
+
function asArray25(v) {
|
|
4807
5819
|
if (Array.isArray(v)) return v;
|
|
4808
5820
|
if (v && typeof v === "object") {
|
|
4809
5821
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
4810
5822
|
}
|
|
4811
5823
|
return [];
|
|
4812
5824
|
}
|
|
4813
|
-
function
|
|
5825
|
+
function strName8(v) {
|
|
4814
5826
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
4815
5827
|
}
|
|
4816
5828
|
function isInterpolated(target) {
|
|
@@ -4850,14 +5862,14 @@ function distance2(a, b) {
|
|
|
4850
5862
|
function validateObjectReferences(stack) {
|
|
4851
5863
|
const findings = [];
|
|
4852
5864
|
if (!stack || typeof stack !== "object") return findings;
|
|
4853
|
-
const objects =
|
|
5865
|
+
const objects = asArray25(stack.objects);
|
|
4854
5866
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
4855
5867
|
for (const obj of objects) {
|
|
4856
|
-
const n =
|
|
5868
|
+
const n = strName8(obj.name);
|
|
4857
5869
|
if (n) ownObjects.add(n);
|
|
4858
5870
|
}
|
|
4859
5871
|
const check = (target, where, path, subject, fix) => {
|
|
4860
|
-
const name =
|
|
5872
|
+
const name = strName8(target);
|
|
4861
5873
|
if (!name) return;
|
|
4862
5874
|
if (isInterpolated(name)) return;
|
|
4863
5875
|
if (ownObjects.has(name)) return;
|
|
@@ -4883,21 +5895,21 @@ function validateObjectReferences(stack) {
|
|
|
4883
5895
|
});
|
|
4884
5896
|
};
|
|
4885
5897
|
const checkActionParams2 = (action, actionPath, actionLabel) => {
|
|
4886
|
-
const params =
|
|
5898
|
+
const params = asArray25(action.params);
|
|
4887
5899
|
for (let pi = 0; pi < params.length; pi++) {
|
|
4888
5900
|
const param = params[pi];
|
|
4889
5901
|
if (!param || typeof param !== "object") continue;
|
|
4890
|
-
const paramLabel =
|
|
5902
|
+
const paramLabel = strName8(param.name) ?? strName8(param.field) ?? `#${pi}`;
|
|
4891
5903
|
const where = `${actionLabel} \xB7 param "${paramLabel}"`;
|
|
4892
5904
|
check(
|
|
4893
|
-
|
|
5905
|
+
strName8(param.reference),
|
|
4894
5906
|
where,
|
|
4895
5907
|
`${actionPath}.params[${pi}].reference`,
|
|
4896
5908
|
"record-picker target",
|
|
4897
5909
|
"Without a resolvable target the picker degrades to a raw record-id text input."
|
|
4898
5910
|
);
|
|
4899
5911
|
check(
|
|
4900
|
-
|
|
5912
|
+
strName8(param.objectOverride),
|
|
4901
5913
|
where,
|
|
4902
5914
|
`${actionPath}.params[${pi}].objectOverride`,
|
|
4903
5915
|
"field-backed param object",
|
|
@@ -4905,70 +5917,70 @@ function validateObjectReferences(stack) {
|
|
|
4905
5917
|
);
|
|
4906
5918
|
}
|
|
4907
5919
|
};
|
|
4908
|
-
const globalActions =
|
|
5920
|
+
const globalActions = asArray25(stack.actions);
|
|
4909
5921
|
for (let ai = 0; ai < globalActions.length; ai++) {
|
|
4910
5922
|
const action = globalActions[ai];
|
|
4911
5923
|
if (!action || typeof action !== "object") continue;
|
|
4912
|
-
checkActionParams2(action, `actions[${ai}]`, `action "${
|
|
5924
|
+
checkActionParams2(action, `actions[${ai}]`, `action "${strName8(action.name) ?? `#${ai}`}"`);
|
|
4913
5925
|
}
|
|
4914
5926
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
4915
5927
|
const obj = objects[oi];
|
|
4916
5928
|
if (!obj || typeof obj !== "object") continue;
|
|
4917
|
-
const objName =
|
|
4918
|
-
const objActions =
|
|
5929
|
+
const objName = strName8(obj.name) ?? `#${oi}`;
|
|
5930
|
+
const objActions = asArray25(obj.actions);
|
|
4919
5931
|
for (let ai = 0; ai < objActions.length; ai++) {
|
|
4920
5932
|
const action = objActions[ai];
|
|
4921
5933
|
if (!action || typeof action !== "object") continue;
|
|
4922
5934
|
checkActionParams2(
|
|
4923
5935
|
action,
|
|
4924
5936
|
`objects[${oi}].actions[${ai}]`,
|
|
4925
|
-
`object "${objName}" \xB7 action "${
|
|
5937
|
+
`object "${objName}" \xB7 action "${strName8(action.name) ?? `#${ai}`}"`
|
|
4926
5938
|
);
|
|
4927
5939
|
}
|
|
4928
5940
|
}
|
|
4929
|
-
const dashboards =
|
|
5941
|
+
const dashboards = asArray25(stack.dashboards);
|
|
4930
5942
|
for (let di = 0; di < dashboards.length; di++) {
|
|
4931
5943
|
const dash = dashboards[di];
|
|
4932
5944
|
if (!dash || typeof dash !== "object") continue;
|
|
4933
|
-
const dashName =
|
|
4934
|
-
const filters =
|
|
5945
|
+
const dashName = strName8(dash.name) ?? `#${di}`;
|
|
5946
|
+
const filters = asArray25(dash.globalFilters);
|
|
4935
5947
|
for (let fi = 0; fi < filters.length; fi++) {
|
|
4936
5948
|
const filter = filters[fi];
|
|
4937
5949
|
if (!filter || typeof filter !== "object") continue;
|
|
4938
5950
|
const optionsFrom = filter.optionsFrom;
|
|
4939
5951
|
if (!optionsFrom || typeof optionsFrom !== "object") continue;
|
|
4940
5952
|
check(
|
|
4941
|
-
|
|
4942
|
-
`dashboard "${dashName}" \xB7 filter "${
|
|
5953
|
+
strName8(optionsFrom.object),
|
|
5954
|
+
`dashboard "${dashName}" \xB7 filter "${strName8(filter.name) ?? `#${fi}`}"`,
|
|
4943
5955
|
`dashboards[${di}].globalFilters[${fi}].optionsFrom.object`,
|
|
4944
5956
|
"filter options source",
|
|
4945
5957
|
"The dropdown fetches its options from this object; an unknown one renders an always-empty filter."
|
|
4946
5958
|
);
|
|
4947
5959
|
}
|
|
4948
5960
|
}
|
|
4949
|
-
const apps =
|
|
5961
|
+
const apps = asArray25(stack.apps);
|
|
4950
5962
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
4951
5963
|
const app = apps[ai];
|
|
4952
5964
|
if (!app || typeof app !== "object") continue;
|
|
4953
|
-
const appName =
|
|
5965
|
+
const appName = strName8(app.name) ?? `#${ai}`;
|
|
4954
5966
|
const walkNav = (items, basePath) => {
|
|
4955
|
-
const navItems =
|
|
5967
|
+
const navItems = asArray25(items);
|
|
4956
5968
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
4957
5969
|
const nav = navItems[ni];
|
|
4958
5970
|
if (!nav || typeof nav !== "object") continue;
|
|
4959
|
-
const navId =
|
|
5971
|
+
const navId = strName8(nav.id) ?? `#${ni}`;
|
|
4960
5972
|
const where = `app "${appName}" \xB7 nav "${navId}"`;
|
|
4961
5973
|
const navPath = `${basePath}[${ni}]`;
|
|
4962
5974
|
check(
|
|
4963
|
-
|
|
5975
|
+
strName8(nav.requiresObject),
|
|
4964
5976
|
where,
|
|
4965
5977
|
`${navPath}.requiresObject`,
|
|
4966
5978
|
"capability gate object",
|
|
4967
5979
|
"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."
|
|
4968
5980
|
);
|
|
4969
|
-
if (nav.requiresObject &&
|
|
5981
|
+
if (nav.requiresObject && strName8(nav.objectName)) {
|
|
4970
5982
|
check(
|
|
4971
|
-
|
|
5983
|
+
strName8(nav.objectName),
|
|
4972
5984
|
where,
|
|
4973
5985
|
`${navPath}.objectName`,
|
|
4974
5986
|
"navigation target",
|
|
@@ -4979,7 +5991,7 @@ function validateObjectReferences(stack) {
|
|
|
4979
5991
|
}
|
|
4980
5992
|
};
|
|
4981
5993
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
4982
|
-
const areas =
|
|
5994
|
+
const areas = asArray25(app.areas);
|
|
4983
5995
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
4984
5996
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
4985
5997
|
}
|
|
@@ -4989,13 +6001,13 @@ function validateObjectReferences(stack) {
|
|
|
4989
6001
|
|
|
4990
6002
|
// src/validate-nav-target-refs.ts
|
|
4991
6003
|
var NAV_TARGET_UNRESOLVED = "nav-target-unresolved";
|
|
4992
|
-
var
|
|
4993
|
-
function
|
|
4994
|
-
if (Array.isArray(v)) return v.filter(
|
|
4995
|
-
if (
|
|
6004
|
+
var isRec12 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6005
|
+
function asArray26(v) {
|
|
6006
|
+
if (Array.isArray(v)) return v.filter(isRec12);
|
|
6007
|
+
if (isRec12(v)) return Object.entries(v).map(([name, def]) => isRec12(def) ? { name, ...def } : { name });
|
|
4996
6008
|
return [];
|
|
4997
6009
|
}
|
|
4998
|
-
function
|
|
6010
|
+
function strName9(v) {
|
|
4999
6011
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5000
6012
|
}
|
|
5001
6013
|
var isInterpolated2 = (s) => s.includes("${") || s.includes("{");
|
|
@@ -5006,32 +6018,32 @@ var NAV_TARGETS = [
|
|
|
5006
6018
|
];
|
|
5007
6019
|
function namesOf(collection) {
|
|
5008
6020
|
const out = /* @__PURE__ */ new Set();
|
|
5009
|
-
for (const entry of
|
|
5010
|
-
const n =
|
|
6021
|
+
for (const entry of asArray26(collection)) {
|
|
6022
|
+
const n = strName9(entry.name);
|
|
5011
6023
|
if (n) out.add(n);
|
|
5012
6024
|
}
|
|
5013
6025
|
return out;
|
|
5014
6026
|
}
|
|
5015
6027
|
function validateNavTargetRefs(stack) {
|
|
5016
6028
|
const findings = [];
|
|
5017
|
-
if (!
|
|
5018
|
-
const apps =
|
|
6029
|
+
if (!isRec12(stack)) return findings;
|
|
6030
|
+
const apps = asArray26(stack.apps);
|
|
5019
6031
|
if (apps.length === 0) return findings;
|
|
5020
6032
|
const declared = /* @__PURE__ */ new Map();
|
|
5021
6033
|
for (const [, , collection] of NAV_TARGETS) {
|
|
5022
6034
|
declared.set(collection, namesOf(stack[collection]));
|
|
5023
6035
|
}
|
|
5024
6036
|
for (const [ai, app] of apps.entries()) {
|
|
5025
|
-
const appName =
|
|
6037
|
+
const appName = strName9(app.name) ?? `#${ai}`;
|
|
5026
6038
|
const walk = (items, basePath) => {
|
|
5027
6039
|
if (!Array.isArray(items)) return;
|
|
5028
6040
|
for (const [ni, raw] of items.entries()) {
|
|
5029
|
-
if (!
|
|
6041
|
+
if (!isRec12(raw)) continue;
|
|
5030
6042
|
const nav = raw;
|
|
5031
6043
|
const navPath = `${basePath}[${ni}]`;
|
|
5032
6044
|
for (const [type, prop, collection, noun] of NAV_TARGETS) {
|
|
5033
6045
|
if (nav.type !== type) continue;
|
|
5034
|
-
const target =
|
|
6046
|
+
const target = strName9(nav[prop]);
|
|
5035
6047
|
if (!target || isInterpolated2(target)) continue;
|
|
5036
6048
|
const known = declared.get(collection);
|
|
5037
6049
|
if (known.has(target)) continue;
|
|
@@ -5039,7 +6051,7 @@ function validateNavTargetRefs(stack) {
|
|
|
5039
6051
|
findings.push({
|
|
5040
6052
|
severity: "warning",
|
|
5041
6053
|
rule: NAV_TARGET_UNRESOLVED,
|
|
5042
|
-
where: `app "${appName}" \xB7 nav "${
|
|
6054
|
+
where: `app "${appName}" \xB7 nav "${strName9(nav.id) ?? strName9(nav.label) ?? `#${ni}`}"`,
|
|
5043
6055
|
path: `${navPath}.${prop}`,
|
|
5044
6056
|
message: `Navigation targets ${noun} '${target}', which this stack does not declare in \`${collection}\`. ` + (emptyCollection ? `The stack declares NO ${collection} at all, so \`defineStack\`'s own cross-reference check skipped this entry entirely (it is gated on \`${collection === "pages" ? "pageNames" : collection === "reports" ? "reportNames" : "dashboardNames"}.size > 0\`) \u2014 nothing else will report it. ` : "") + `The entry renders in the sidebar and resolves to nothing when clicked. If another package provides this ${noun}, this is expected and advisory only.`,
|
|
5045
6057
|
hint: `Declare the ${noun} in \`${collection}\`, correct the name, or remove the nav entry if the ${noun} is gone.`
|
|
@@ -5049,7 +6061,7 @@ function validateNavTargetRefs(stack) {
|
|
|
5049
6061
|
}
|
|
5050
6062
|
};
|
|
5051
6063
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
5052
|
-
for (const [ari, area] of
|
|
6064
|
+
for (const [ari, area] of asArray26(app.areas).entries()) {
|
|
5053
6065
|
walk(area.items, `apps[${ai}].areas[${ari}].items`);
|
|
5054
6066
|
walk(area.navigation, `apps[${ai}].areas[${ari}].navigation`);
|
|
5055
6067
|
}
|
|
@@ -5059,14 +6071,14 @@ function validateNavTargetRefs(stack) {
|
|
|
5059
6071
|
|
|
5060
6072
|
// src/validate-action-name-refs.ts
|
|
5061
6073
|
var ACTION_NAME_UNDEFINED = "action-name-undefined";
|
|
5062
|
-
function
|
|
6074
|
+
function asArray27(v) {
|
|
5063
6075
|
if (Array.isArray(v)) return v;
|
|
5064
6076
|
if (v && typeof v === "object") {
|
|
5065
6077
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5066
6078
|
}
|
|
5067
6079
|
return [];
|
|
5068
6080
|
}
|
|
5069
|
-
function
|
|
6081
|
+
function strName10(v) {
|
|
5070
6082
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5071
6083
|
}
|
|
5072
6084
|
function strList(v) {
|
|
@@ -5103,14 +6115,14 @@ function suggest4(target, known) {
|
|
|
5103
6115
|
}
|
|
5104
6116
|
function collectActionNames(stack) {
|
|
5105
6117
|
const names = /* @__PURE__ */ new Set();
|
|
5106
|
-
for (const action of
|
|
5107
|
-
const n =
|
|
6118
|
+
for (const action of asArray27(stack.actions)) {
|
|
6119
|
+
const n = strName10(action?.name);
|
|
5108
6120
|
if (n) names.add(n);
|
|
5109
6121
|
}
|
|
5110
|
-
for (const obj of
|
|
6122
|
+
for (const obj of asArray27(stack.objects)) {
|
|
5111
6123
|
if (!obj || typeof obj !== "object") continue;
|
|
5112
|
-
for (const action of
|
|
5113
|
-
const n =
|
|
6124
|
+
for (const action of asArray27(obj.actions)) {
|
|
6125
|
+
const n = strName10(action?.name);
|
|
5114
6126
|
if (n) names.add(n);
|
|
5115
6127
|
}
|
|
5116
6128
|
}
|
|
@@ -5153,7 +6165,7 @@ function validateActionNameRefs(stack) {
|
|
|
5153
6165
|
if (!def || typeof def !== "object") continue;
|
|
5154
6166
|
if (def.execution !== "aggregate") continue;
|
|
5155
6167
|
if (def.actionDef !== void 0) continue;
|
|
5156
|
-
const name =
|
|
6168
|
+
const name = strName10(def.name);
|
|
5157
6169
|
if (!name) continue;
|
|
5158
6170
|
check(
|
|
5159
6171
|
name,
|
|
@@ -5164,11 +6176,11 @@ function validateActionNameRefs(stack) {
|
|
|
5164
6176
|
);
|
|
5165
6177
|
}
|
|
5166
6178
|
};
|
|
5167
|
-
const views =
|
|
6179
|
+
const views = asArray27(stack.views);
|
|
5168
6180
|
for (let vi = 0; vi < views.length; vi++) {
|
|
5169
6181
|
const view = views[vi];
|
|
5170
6182
|
if (!view || typeof view !== "object") continue;
|
|
5171
|
-
const viewName =
|
|
6183
|
+
const viewName = strName10(view.name) ?? strName10(view.object) ?? `#${vi}`;
|
|
5172
6184
|
const owner = `view "${viewName}"`;
|
|
5173
6185
|
checkListContainer(view.list, owner, "list", `views[${vi}].list`);
|
|
5174
6186
|
const listViews = view.listViews;
|
|
@@ -5178,22 +6190,22 @@ function validateActionNameRefs(stack) {
|
|
|
5178
6190
|
}
|
|
5179
6191
|
}
|
|
5180
6192
|
}
|
|
5181
|
-
const objects =
|
|
6193
|
+
const objects = asArray27(stack.objects);
|
|
5182
6194
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5183
6195
|
const obj = objects[oi];
|
|
5184
6196
|
if (!obj || typeof obj !== "object") continue;
|
|
5185
6197
|
const objListViews = obj.listViews;
|
|
5186
6198
|
if (!objListViews || typeof objListViews !== "object" || Array.isArray(objListViews)) continue;
|
|
5187
|
-
const owner = `object "${
|
|
6199
|
+
const owner = `object "${strName10(obj.name) ?? `#${oi}`}"`;
|
|
5188
6200
|
for (const [key, lv] of Object.entries(objListViews)) {
|
|
5189
6201
|
checkListContainer(lv, owner, `listViews.${key}`, `objects[${oi}].listViews.${key}`);
|
|
5190
6202
|
}
|
|
5191
6203
|
}
|
|
5192
|
-
const pages =
|
|
6204
|
+
const pages = asArray27(stack.pages);
|
|
5193
6205
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5194
6206
|
const page = pages[pi];
|
|
5195
6207
|
if (!page || typeof page !== "object") continue;
|
|
5196
|
-
const pageName =
|
|
6208
|
+
const pageName = strName10(page.name) ?? `#${pi}`;
|
|
5197
6209
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5198
6210
|
const props = component.properties;
|
|
5199
6211
|
if (!props || typeof props !== "object") continue;
|
|
@@ -5201,39 +6213,48 @@ function validateActionNameRefs(stack) {
|
|
|
5201
6213
|
for (let ai = 0; ai < names.length; ai++) {
|
|
5202
6214
|
check(
|
|
5203
6215
|
names[ai],
|
|
5204
|
-
`page "${pageName}" \xB7 component "${
|
|
6216
|
+
`page "${pageName}" \xB7 component "${strName10(component.type) ?? "?"}"`,
|
|
5205
6217
|
`${path}.properties.actionNames[${ai}]`,
|
|
5206
6218
|
"Quick-actions bar"
|
|
5207
6219
|
);
|
|
5208
6220
|
}
|
|
5209
6221
|
}
|
|
5210
6222
|
}
|
|
5211
|
-
const apps =
|
|
6223
|
+
const apps = asArray27(stack.apps);
|
|
5212
6224
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
5213
6225
|
const app = apps[ai];
|
|
5214
6226
|
if (!app || typeof app !== "object") continue;
|
|
5215
|
-
const appName =
|
|
6227
|
+
const appName = strName10(app.name) ?? `#${ai}`;
|
|
5216
6228
|
const walkNav = (items, basePath) => {
|
|
5217
|
-
const navItems =
|
|
6229
|
+
const navItems = asArray27(items);
|
|
5218
6230
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
5219
6231
|
const nav = navItems[ni];
|
|
5220
6232
|
if (!nav || typeof nav !== "object") continue;
|
|
5221
6233
|
const navPath = `${basePath}[${ni}]`;
|
|
5222
6234
|
const actionDef = nav.actionDef;
|
|
5223
|
-
const actionName =
|
|
6235
|
+
const actionName = strName10(actionDef?.actionName);
|
|
5224
6236
|
if (nav.type === "action" && actionName) {
|
|
5225
6237
|
check(
|
|
5226
6238
|
actionName,
|
|
5227
|
-
`app "${appName}" \xB7 nav "${
|
|
6239
|
+
`app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
|
|
5228
6240
|
`${navPath}.actionDef.actionName`,
|
|
5229
6241
|
"Navigation action item"
|
|
5230
6242
|
);
|
|
5231
6243
|
}
|
|
6244
|
+
const runAction = strName10(nav.runAction);
|
|
6245
|
+
if (nav.type === "object" && runAction) {
|
|
6246
|
+
check(
|
|
6247
|
+
runAction,
|
|
6248
|
+
`app "${appName}" \xB7 nav "${strName10(nav.id) ?? `#${ni}`}"`,
|
|
6249
|
+
`${navPath}.runAction`,
|
|
6250
|
+
"Navigation deep-link auto-run"
|
|
6251
|
+
);
|
|
6252
|
+
}
|
|
5232
6253
|
if (Array.isArray(nav.children)) walkNav(nav.children, `${navPath}.children`);
|
|
5233
6254
|
}
|
|
5234
6255
|
};
|
|
5235
6256
|
walkNav(app.navigation, `apps[${ai}].navigation`);
|
|
5236
|
-
const areas =
|
|
6257
|
+
const areas = asArray27(app.areas);
|
|
5237
6258
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
5238
6259
|
walkNav(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
5239
6260
|
}
|
|
@@ -5243,14 +6264,14 @@ function validateActionNameRefs(stack) {
|
|
|
5243
6264
|
|
|
5244
6265
|
// src/validate-action-locations.ts
|
|
5245
6266
|
var ACTION_NO_PLACEMENT = "action-no-placement";
|
|
5246
|
-
function
|
|
6267
|
+
function asArray28(v) {
|
|
5247
6268
|
if (Array.isArray(v)) return v;
|
|
5248
6269
|
if (v && typeof v === "object") {
|
|
5249
6270
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5250
6271
|
}
|
|
5251
6272
|
return [];
|
|
5252
6273
|
}
|
|
5253
|
-
function
|
|
6274
|
+
function strName11(v) {
|
|
5254
6275
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5255
6276
|
}
|
|
5256
6277
|
function strList2(v) {
|
|
@@ -5264,8 +6285,8 @@ function collectNamePlacedActions(stack) {
|
|
|
5264
6285
|
for (const key of ["rowActions", "bulkActions"]) {
|
|
5265
6286
|
for (const n of strList2(list3[key])) placed.add(n);
|
|
5266
6287
|
}
|
|
5267
|
-
for (const def of
|
|
5268
|
-
const n =
|
|
6288
|
+
for (const def of asArray28(list3.bulkActionDefs)) {
|
|
6289
|
+
const n = strName11(def?.name);
|
|
5269
6290
|
if (n) placed.add(n);
|
|
5270
6291
|
}
|
|
5271
6292
|
};
|
|
@@ -5273,12 +6294,12 @@ function collectNamePlacedActions(stack) {
|
|
|
5273
6294
|
if (!listViews || typeof listViews !== "object" || Array.isArray(listViews)) return;
|
|
5274
6295
|
for (const lv of Object.values(listViews)) harvest(lv);
|
|
5275
6296
|
};
|
|
5276
|
-
for (const view of
|
|
6297
|
+
for (const view of asArray28(stack.views)) {
|
|
5277
6298
|
if (!view || typeof view !== "object") continue;
|
|
5278
6299
|
harvest(view.list);
|
|
5279
6300
|
harvestListViews(view.listViews);
|
|
5280
6301
|
}
|
|
5281
|
-
for (const obj of
|
|
6302
|
+
for (const obj of asArray28(stack.objects)) {
|
|
5282
6303
|
if (!obj || typeof obj !== "object") continue;
|
|
5283
6304
|
harvestListViews(obj.listViews);
|
|
5284
6305
|
}
|
|
@@ -5291,7 +6312,7 @@ function validateActionLocations(stack) {
|
|
|
5291
6312
|
const check = (action, path) => {
|
|
5292
6313
|
if (!action || typeof action !== "object") return;
|
|
5293
6314
|
if ("locations" in action) return;
|
|
5294
|
-
const name =
|
|
6315
|
+
const name = strName11(action.name);
|
|
5295
6316
|
if (!name) return;
|
|
5296
6317
|
if (namePlaced.has(name)) return;
|
|
5297
6318
|
findings.push({
|
|
@@ -5300,16 +6321,16 @@ function validateActionLocations(stack) {
|
|
|
5300
6321
|
where: `action "${name}"`,
|
|
5301
6322
|
path,
|
|
5302
6323
|
message: `Action "${name}" declares no \`locations\` and no view places it by name, so it renders on no surface \u2014 the button exists in metadata and nowhere in the UI.`,
|
|
5303
|
-
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related
|
|
6324
|
+
hint: "Add the surface it belongs on, e.g. `locations: ['record_header']` (or `list_item`, `list_toolbar`, `record_more`, `record_section`, `record_related`); or place it from a list view's `bulkActions` / `bulkActionDefs` if it acts on a selection. If it is meant to be callable over REST / MCP / AI with no UI surface, say so explicitly with `locations: []` \u2014 an empty array is the documented headless shape and is never flagged."
|
|
5304
6325
|
});
|
|
5305
6326
|
};
|
|
5306
|
-
const actions =
|
|
6327
|
+
const actions = asArray28(stack.actions);
|
|
5307
6328
|
for (let i = 0; i < actions.length; i++) check(actions[i], `actions[${i}]`);
|
|
5308
|
-
const objects =
|
|
6329
|
+
const objects = asArray28(stack.objects);
|
|
5309
6330
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5310
6331
|
const obj = objects[oi];
|
|
5311
6332
|
if (!obj || typeof obj !== "object") continue;
|
|
5312
|
-
const own =
|
|
6333
|
+
const own = asArray28(obj.actions);
|
|
5313
6334
|
for (let ai = 0; ai < own.length; ai++) check(own[ai], `objects[${oi}].actions[${ai}]`);
|
|
5314
6335
|
}
|
|
5315
6336
|
return findings;
|
|
@@ -5317,16 +6338,16 @@ function validateActionLocations(stack) {
|
|
|
5317
6338
|
|
|
5318
6339
|
// src/validate-component-props.ts
|
|
5319
6340
|
var import_ui3 = require("@objectstack/spec/ui");
|
|
5320
|
-
var
|
|
6341
|
+
var import_spec3 = require("@objectstack/spec");
|
|
5321
6342
|
var COMPONENT_PROPS_UNKNOWN_KEY = "component-props-unknown-key";
|
|
5322
6343
|
var COMPONENT_PROPS_INVALID = "component-props-invalid";
|
|
5323
|
-
function
|
|
6344
|
+
function isRec13(v) {
|
|
5324
6345
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5325
6346
|
}
|
|
5326
|
-
function
|
|
6347
|
+
function strName12(v) {
|
|
5327
6348
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5328
6349
|
}
|
|
5329
|
-
function
|
|
6350
|
+
function asArray29(v) {
|
|
5330
6351
|
if (Array.isArray(v)) return v;
|
|
5331
6352
|
if (v && typeof v === "object") {
|
|
5332
6353
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5337,27 +6358,27 @@ var PROPS_SCHEMAS = import_ui3.ComponentPropsMap;
|
|
|
5337
6358
|
var DATASOURCE_SUPPLIED_PROP = "object";
|
|
5338
6359
|
function suppliedByDataSource(issue, component) {
|
|
5339
6360
|
if (issue.path.length !== 1 || issue.path[0] !== DATASOURCE_SUPPLIED_PROP) return false;
|
|
5340
|
-
const dataSource =
|
|
5341
|
-
return
|
|
6361
|
+
const dataSource = isRec13(component.dataSource) ? component.dataSource : void 0;
|
|
6362
|
+
return strName12(dataSource?.object) !== void 0;
|
|
5342
6363
|
}
|
|
5343
6364
|
function validateComponentProps(stack) {
|
|
5344
6365
|
const findings = [];
|
|
5345
|
-
if (!
|
|
5346
|
-
const pages =
|
|
6366
|
+
if (!isRec13(stack)) return findings;
|
|
6367
|
+
const pages = asArray29(stack.pages);
|
|
5347
6368
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5348
6369
|
const page = pages[pi];
|
|
5349
|
-
if (!
|
|
5350
|
-
const pageName =
|
|
6370
|
+
if (!isRec13(page)) continue;
|
|
6371
|
+
const pageName = strName12(page.name) ?? `#${pi}`;
|
|
5351
6372
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5352
|
-
const type =
|
|
6373
|
+
const type = strName12(component.type);
|
|
5353
6374
|
if (!type) continue;
|
|
5354
6375
|
const schema = PROPS_SCHEMAS[type];
|
|
5355
6376
|
if (!schema) continue;
|
|
5356
|
-
const props =
|
|
6377
|
+
const props = isRec13(component.properties) ? component.properties : void 0;
|
|
5357
6378
|
if (!props) continue;
|
|
5358
6379
|
const where = `page "${pageName}" \xB7 ${type}`;
|
|
5359
6380
|
const base = `${path}.properties`;
|
|
5360
|
-
for (const f of (0,
|
|
6381
|
+
for (const f of (0, import_spec3.lintUnknownKeysAgainstSchema)(schema, props, type, base)) {
|
|
5361
6382
|
findings.push({
|
|
5362
6383
|
severity: "warning",
|
|
5363
6384
|
rule: COMPONENT_PROPS_UNKNOWN_KEY,
|
|
@@ -5404,20 +6425,20 @@ var CHART_DIMENSION_UNKNOWN = "chart-dimension-unknown";
|
|
|
5404
6425
|
var CHART_MEASURE_UNKNOWN = "chart-measure-unknown";
|
|
5405
6426
|
var CHART_DATASET_UNKNOWN = "chart-dataset-unknown";
|
|
5406
6427
|
var CHART_AXIS_NOT_SELECTED = "chart-axis-not-selected";
|
|
5407
|
-
function
|
|
6428
|
+
function asArray30(v) {
|
|
5408
6429
|
if (Array.isArray(v)) return v;
|
|
5409
6430
|
if (v && typeof v === "object") {
|
|
5410
6431
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5411
6432
|
}
|
|
5412
6433
|
return [];
|
|
5413
6434
|
}
|
|
5414
|
-
function
|
|
6435
|
+
function strName13(v) {
|
|
5415
6436
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
5416
6437
|
}
|
|
5417
6438
|
function strList3(v) {
|
|
5418
6439
|
return Array.isArray(v) ? v.filter((x) => typeof x === "string" && x.length > 0) : [];
|
|
5419
6440
|
}
|
|
5420
|
-
function
|
|
6441
|
+
function isRec14(v) {
|
|
5421
6442
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5422
6443
|
}
|
|
5423
6444
|
function distance4(a, b) {
|
|
@@ -5455,17 +6476,17 @@ function list2(names) {
|
|
|
5455
6476
|
}
|
|
5456
6477
|
function indexDatasets(stack) {
|
|
5457
6478
|
const out = /* @__PURE__ */ new Map();
|
|
5458
|
-
for (const ds of
|
|
5459
|
-
const name =
|
|
6479
|
+
for (const ds of asArray30(stack.datasets)) {
|
|
6480
|
+
const name = strName13(ds.name);
|
|
5460
6481
|
if (!name) continue;
|
|
5461
6482
|
const dimensions = /* @__PURE__ */ new Set();
|
|
5462
|
-
for (const d of
|
|
5463
|
-
const n =
|
|
6483
|
+
for (const d of asArray30(ds.dimensions)) {
|
|
6484
|
+
const n = strName13(d.name);
|
|
5464
6485
|
if (n) dimensions.add(n);
|
|
5465
6486
|
}
|
|
5466
6487
|
const measures = /* @__PURE__ */ new Set();
|
|
5467
|
-
for (const m of
|
|
5468
|
-
const n =
|
|
6488
|
+
for (const m of asArray30(ds.measures)) {
|
|
6489
|
+
const n = strName13(m.name);
|
|
5469
6490
|
if (n) measures.add(n);
|
|
5470
6491
|
}
|
|
5471
6492
|
out.set(name, { dimensions, measures });
|
|
@@ -5543,29 +6564,29 @@ function validateChartBindings(stack) {
|
|
|
5543
6564
|
if (binding.yAxis) measureRef(binding.yAxis.name, binding.yAxis.path, selected);
|
|
5544
6565
|
for (const s of binding.series ?? []) measureRef(s.name, s.path, selected);
|
|
5545
6566
|
};
|
|
5546
|
-
const reports =
|
|
6567
|
+
const reports = asArray30(stack.reports);
|
|
5547
6568
|
for (let ri = 0; ri < reports.length; ri++) {
|
|
5548
6569
|
const report = reports[ri];
|
|
5549
|
-
if (!
|
|
5550
|
-
const reportName =
|
|
6570
|
+
if (!isRec14(report)) continue;
|
|
6571
|
+
const reportName = strName13(report.name) ?? `#${ri}`;
|
|
5551
6572
|
const checkReportChart = (chart, dataset, values, where, path) => {
|
|
5552
|
-
if (!
|
|
6573
|
+
if (!isRec14(chart)) return;
|
|
5553
6574
|
check({
|
|
5554
6575
|
dataset,
|
|
5555
6576
|
// `values` is the report's measure SELECTION, not a chart ref; feeding
|
|
5556
6577
|
// it in lets the yAxis "declared but not selected" check work without
|
|
5557
6578
|
// reporting the selection itself twice.
|
|
5558
6579
|
values: { names: values, path: `${path}.values` },
|
|
5559
|
-
xAxis:
|
|
5560
|
-
yAxis:
|
|
5561
|
-
series:
|
|
6580
|
+
xAxis: strName13(chart.xAxis) ? { name: strName13(chart.xAxis), path: `${path}.chart.xAxis` } : void 0,
|
|
6581
|
+
yAxis: strName13(chart.yAxis) ? { name: strName13(chart.yAxis), path: `${path}.chart.yAxis` } : void 0,
|
|
6582
|
+
series: asArray30(chart.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.chart.series[${si}].name` })).filter((s) => !!s.name),
|
|
5562
6583
|
where,
|
|
5563
6584
|
path: `${path}.chart`
|
|
5564
6585
|
});
|
|
5565
6586
|
};
|
|
5566
6587
|
checkReportChart(
|
|
5567
6588
|
report.chart,
|
|
5568
|
-
|
|
6589
|
+
strName13(report.dataset),
|
|
5569
6590
|
strList3(report.values),
|
|
5570
6591
|
`report "${reportName}" \xB7 chart`,
|
|
5571
6592
|
`reports[${ri}]`
|
|
@@ -5573,45 +6594,45 @@ function validateChartBindings(stack) {
|
|
|
5573
6594
|
const blocks = Array.isArray(report.blocks) ? report.blocks : [];
|
|
5574
6595
|
for (let bi = 0; bi < blocks.length; bi++) {
|
|
5575
6596
|
const block = blocks[bi];
|
|
5576
|
-
if (!
|
|
6597
|
+
if (!isRec14(block)) continue;
|
|
5577
6598
|
checkReportChart(
|
|
5578
6599
|
block.chart,
|
|
5579
|
-
|
|
6600
|
+
strName13(block.dataset),
|
|
5580
6601
|
strList3(block.values),
|
|
5581
|
-
`report "${reportName}" \xB7 block "${
|
|
6602
|
+
`report "${reportName}" \xB7 block "${strName13(block.name) ?? `#${bi}`}" chart`,
|
|
5582
6603
|
`reports[${ri}].blocks[${bi}]`
|
|
5583
6604
|
);
|
|
5584
6605
|
}
|
|
5585
6606
|
}
|
|
5586
6607
|
const checkListChart = (container, where, path) => {
|
|
5587
|
-
if (!
|
|
6608
|
+
if (!isRec14(container)) return;
|
|
5588
6609
|
const chart = container.chart;
|
|
5589
|
-
if (!
|
|
6610
|
+
if (!isRec14(chart)) return;
|
|
5590
6611
|
check({
|
|
5591
|
-
dataset:
|
|
6612
|
+
dataset: strName13(chart.dataset),
|
|
5592
6613
|
dimensions: { names: strList3(chart.dimensions), path: `${path}.chart.dimensions` },
|
|
5593
6614
|
values: { names: strList3(chart.values), path: `${path}.chart.values` },
|
|
5594
6615
|
where,
|
|
5595
6616
|
path: `${path}.chart`
|
|
5596
6617
|
});
|
|
5597
6618
|
};
|
|
5598
|
-
const views =
|
|
6619
|
+
const views = asArray30(stack.views);
|
|
5599
6620
|
for (let vi = 0; vi < views.length; vi++) {
|
|
5600
6621
|
const view = views[vi];
|
|
5601
|
-
if (!
|
|
5602
|
-
const viewName =
|
|
6622
|
+
if (!isRec14(view)) continue;
|
|
6623
|
+
const viewName = strName13(view.name) ?? strName13(view.objectName) ?? `#${vi}`;
|
|
5603
6624
|
checkListChart(view.list, `view "${viewName}" \xB7 list chart`, `views[${vi}].list`);
|
|
5604
|
-
if (
|
|
6625
|
+
if (isRec14(view.listViews)) {
|
|
5605
6626
|
for (const [key, lv] of Object.entries(view.listViews)) {
|
|
5606
6627
|
checkListChart(lv, `view "${viewName}" \xB7 listViews.${key} chart`, `views[${vi}].listViews.${key}`);
|
|
5607
6628
|
}
|
|
5608
6629
|
}
|
|
5609
6630
|
}
|
|
5610
|
-
const objects =
|
|
6631
|
+
const objects = asArray30(stack.objects);
|
|
5611
6632
|
for (let oi = 0; oi < objects.length; oi++) {
|
|
5612
6633
|
const obj = objects[oi];
|
|
5613
|
-
if (!
|
|
5614
|
-
const objName =
|
|
6634
|
+
if (!isRec14(obj) || !isRec14(obj.listViews)) continue;
|
|
6635
|
+
const objName = strName13(obj.name) ?? `#${oi}`;
|
|
5615
6636
|
for (const [key, lv] of Object.entries(obj.listViews)) {
|
|
5616
6637
|
checkListChart(
|
|
5617
6638
|
lv,
|
|
@@ -5620,22 +6641,22 @@ function validateChartBindings(stack) {
|
|
|
5620
6641
|
);
|
|
5621
6642
|
}
|
|
5622
6643
|
}
|
|
5623
|
-
const pages =
|
|
6644
|
+
const pages = asArray30(stack.pages);
|
|
5624
6645
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
5625
6646
|
const page = pages[pi];
|
|
5626
|
-
if (!
|
|
5627
|
-
const pageName =
|
|
6647
|
+
if (!isRec14(page)) continue;
|
|
6648
|
+
const pageName = strName13(page.name) ?? `#${pi}`;
|
|
5628
6649
|
for (const { component, path } of walkPageComponents(page, `pages[${pi}]`)) {
|
|
5629
|
-
const props =
|
|
5630
|
-
if (!props || !
|
|
5631
|
-
const axisRefs =
|
|
5632
|
-
const seriesRefs =
|
|
6650
|
+
const props = isRec14(component.properties) ? component.properties : void 0;
|
|
6651
|
+
if (!props || !strName13(props.dataset)) continue;
|
|
6652
|
+
const axisRefs = asArray30(props.yAxis).map((a, ai) => ({ name: strName13(a.field), path: `${path}.properties.yAxis[${ai}].field` })).filter((a) => !!a.name);
|
|
6653
|
+
const seriesRefs = asArray30(props.series).map((s, si) => ({ name: strName13(s.name), path: `${path}.properties.series[${si}].name` })).filter((s) => !!s.name);
|
|
5633
6654
|
check({
|
|
5634
|
-
dataset:
|
|
6655
|
+
dataset: strName13(props.dataset),
|
|
5635
6656
|
dimensions: { names: strList3(props.dimensions), path: `${path}.properties.dimensions` },
|
|
5636
6657
|
values: { names: strList3(props.values), path: `${path}.properties.values` },
|
|
5637
6658
|
series: [...axisRefs, ...seriesRefs],
|
|
5638
|
-
where: `page "${pageName}" \xB7 ${
|
|
6659
|
+
where: `page "${pageName}" \xB7 ${strName13(component.type) ?? "chart"}`,
|
|
5639
6660
|
path: `${path}.properties`
|
|
5640
6661
|
});
|
|
5641
6662
|
}
|
|
@@ -5644,16 +6665,16 @@ function validateChartBindings(stack) {
|
|
|
5644
6665
|
}
|
|
5645
6666
|
|
|
5646
6667
|
// src/validate-rule-compilability.ts
|
|
5647
|
-
var
|
|
5648
|
-
var
|
|
6668
|
+
var import_node_module4 = require("module");
|
|
6669
|
+
var import_meta4 = {};
|
|
5649
6670
|
var VALIDATION_RULE_REGEX_UNCOMPILABLE = "validation-rule-regex-uncompilable";
|
|
5650
6671
|
var VALIDATION_RULE_SCHEMA_UNCOMPILABLE = "validation-rule-json-schema-uncompilable";
|
|
5651
6672
|
var RUNTIME_AJV_OPTIONS = { allErrors: true, strict: false };
|
|
5652
|
-
var
|
|
5653
|
-
function
|
|
5654
|
-
if (Array.isArray(v)) return v.filter(
|
|
5655
|
-
if (
|
|
5656
|
-
return Object.entries(v).filter(([, def]) =>
|
|
6673
|
+
var isRec15 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
6674
|
+
function asArray31(v) {
|
|
6675
|
+
if (Array.isArray(v)) return v.filter(isRec15);
|
|
6676
|
+
if (isRec15(v)) {
|
|
6677
|
+
return Object.entries(v).filter(([, def]) => isRec15(def)).map(([name, def]) => ({ name, ...def }));
|
|
5657
6678
|
}
|
|
5658
6679
|
return [];
|
|
5659
6680
|
}
|
|
@@ -5661,31 +6682,31 @@ var cachedAjv = null;
|
|
|
5661
6682
|
var cachedAddFormats = null;
|
|
5662
6683
|
function loadAjv() {
|
|
5663
6684
|
if (cachedAjv) return cachedAjv;
|
|
5664
|
-
const anchor = typeof
|
|
6685
|
+
const anchor = typeof import_meta4 !== "undefined" && import_meta4.url ? import_meta4.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5665
6686
|
let mod;
|
|
5666
6687
|
try {
|
|
5667
|
-
mod = (0,
|
|
6688
|
+
mod = (0, import_node_module4.createRequire)(anchor)("ajv");
|
|
5668
6689
|
} catch (err) {
|
|
5669
6690
|
throw new Error(
|
|
5670
6691
|
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule.`
|
|
5671
6692
|
);
|
|
5672
6693
|
}
|
|
5673
|
-
const ctor =
|
|
6694
|
+
const ctor = isRec15(mod) && "default" in mod ? mod.default : mod;
|
|
5674
6695
|
cachedAjv = ctor;
|
|
5675
6696
|
return ctor;
|
|
5676
6697
|
}
|
|
5677
6698
|
function loadAddFormats() {
|
|
5678
6699
|
if (cachedAddFormats) return cachedAddFormats;
|
|
5679
|
-
const anchor = typeof
|
|
6700
|
+
const anchor = typeof import_meta4 !== "undefined" && import_meta4.url ? import_meta4.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
5680
6701
|
let mod;
|
|
5681
6702
|
try {
|
|
5682
|
-
mod = (0,
|
|
6703
|
+
mod = (0, import_node_module4.createRequire)(anchor)("ajv-formats");
|
|
5683
6704
|
} catch (err) {
|
|
5684
6705
|
throw new Error(
|
|
5685
6706
|
`@objectstack/lint: checking a \`json_schema\` validation rule requires the "ajv-formats" package, which could not be loaded (${err instanceof Error ? err.message : String(err)}). It is a declared dependency of @objectstack/lint \u2014 if this deployment prunes packages, keep "ajv-formats" in the image; it is only loaded when a stack declares a \`json_schema\` validation rule. The runtime registers it too, and this gate must compile in the SAME environment or it starts disagreeing with the write path.`
|
|
5686
6707
|
);
|
|
5687
6708
|
}
|
|
5688
|
-
const plugin =
|
|
6709
|
+
const plugin = isRec15(mod) && "default" in mod ? mod.default : mod;
|
|
5689
6710
|
cachedAddFormats = plugin;
|
|
5690
6711
|
return plugin;
|
|
5691
6712
|
}
|
|
@@ -5715,17 +6736,17 @@ function flattenRules(rule, labelTrail, pathTrail, depth = 0) {
|
|
|
5715
6736
|
if (depth >= MAX_RULE_NESTING_DEPTH) return out;
|
|
5716
6737
|
for (const branch of ["then", "otherwise"]) {
|
|
5717
6738
|
const nested = rule[branch];
|
|
5718
|
-
if (
|
|
6739
|
+
if (isRec15(nested)) out.push(...flattenRules(nested, label2, `${path}.${branch}`, depth + 1));
|
|
5719
6740
|
}
|
|
5720
6741
|
return out;
|
|
5721
6742
|
}
|
|
5722
6743
|
function walkObjectValidationRules(stack) {
|
|
5723
6744
|
const walked = [];
|
|
5724
|
-
if (!
|
|
5725
|
-
for (const obj of
|
|
6745
|
+
if (!isRec15(stack)) return walked;
|
|
6746
|
+
for (const obj of asArray31(stack.objects)) {
|
|
5726
6747
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
5727
6748
|
const validations = obj.validations;
|
|
5728
|
-
for (const authored of
|
|
6749
|
+
for (const authored of asArray31(validations)) {
|
|
5729
6750
|
for (const { rule, label: label2, path } of flattenRules(authored, "", "")) {
|
|
5730
6751
|
walked.push({
|
|
5731
6752
|
rule,
|
|
@@ -5756,7 +6777,7 @@ function validateRuleCompilability(stack) {
|
|
|
5756
6777
|
});
|
|
5757
6778
|
}
|
|
5758
6779
|
}
|
|
5759
|
-
if (rule.type === "json_schema" &&
|
|
6780
|
+
if (rule.type === "json_schema" && isRec15(rule.schema)) {
|
|
5760
6781
|
try {
|
|
5761
6782
|
createRuntimeAjv().compile(rule.schema);
|
|
5762
6783
|
} catch (err) {
|
|
@@ -5776,7 +6797,7 @@ function validateRuleCompilability(stack) {
|
|
|
5776
6797
|
|
|
5777
6798
|
// src/validate-rule-schema-formats.ts
|
|
5778
6799
|
var VALIDATION_RULE_SCHEMA_UNKNOWN_FORMAT = "validation-rule-json-schema-unknown-format";
|
|
5779
|
-
var
|
|
6800
|
+
var isRec16 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
5780
6801
|
var SUBSCHEMA_KEYS = [
|
|
5781
6802
|
"additionalItems",
|
|
5782
6803
|
"additionalProperties",
|
|
@@ -5800,7 +6821,7 @@ var SUBSCHEMA_MAP_KEYS = [
|
|
|
5800
6821
|
var MAX_SCHEMA_WALK_DEPTH = 32;
|
|
5801
6822
|
var escapePointerSegment = (segment) => segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
5802
6823
|
function collectFormatUses(schema, pointer, out, depth) {
|
|
5803
|
-
if (!
|
|
6824
|
+
if (!isRec16(schema)) return;
|
|
5804
6825
|
if (typeof schema.format === "string") {
|
|
5805
6826
|
out.push({ pointer: `${pointer}/format`, name: schema.format });
|
|
5806
6827
|
}
|
|
@@ -5819,7 +6840,7 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
5819
6840
|
}
|
|
5820
6841
|
for (const key of SUBSCHEMA_MAP_KEYS) {
|
|
5821
6842
|
const value = schema[key];
|
|
5822
|
-
if (!
|
|
6843
|
+
if (!isRec16(value)) continue;
|
|
5823
6844
|
for (const [name, entry] of Object.entries(value)) {
|
|
5824
6845
|
collectFormatUses(entry, `${pointer}/${escapePointerSegment(key)}/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5825
6846
|
}
|
|
@@ -5827,13 +6848,13 @@ function collectFormatUses(schema, pointer, out, depth) {
|
|
|
5827
6848
|
const items = schema.items;
|
|
5828
6849
|
if (Array.isArray(items)) {
|
|
5829
6850
|
items.forEach((entry, index) => collectFormatUses(entry, `${pointer}/items/${index}`, out, depth + 1));
|
|
5830
|
-
} else if (
|
|
6851
|
+
} else if (isRec16(items)) {
|
|
5831
6852
|
collectFormatUses(items, `${pointer}/items`, out, depth + 1);
|
|
5832
6853
|
}
|
|
5833
6854
|
const dependencies = schema.dependencies;
|
|
5834
|
-
if (
|
|
6855
|
+
if (isRec16(dependencies)) {
|
|
5835
6856
|
for (const [name, entry] of Object.entries(dependencies)) {
|
|
5836
|
-
if (!
|
|
6857
|
+
if (!isRec16(entry)) continue;
|
|
5837
6858
|
collectFormatUses(entry, `${pointer}/dependencies/${escapePointerSegment(name)}`, out, depth + 1);
|
|
5838
6859
|
}
|
|
5839
6860
|
}
|
|
@@ -5872,7 +6893,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
5872
6893
|
const findings = [];
|
|
5873
6894
|
const pending = [];
|
|
5874
6895
|
for (const { rule, objectName, label: label2, where, basePath } of walkObjectValidationRules(stack)) {
|
|
5875
|
-
if (rule.type !== "json_schema" || !
|
|
6896
|
+
if (rule.type !== "json_schema" || !isRec16(rule.schema)) continue;
|
|
5876
6897
|
const uses = [];
|
|
5877
6898
|
collectFormatUses(rule.schema, "", uses, 0);
|
|
5878
6899
|
for (const use of uses) pending.push({ use, where, label: label2, objectName, basePath });
|
|
@@ -5900,7 +6921,7 @@ function validateRuleSchemaFormats(stack) {
|
|
|
5900
6921
|
var import_system3 = require("@objectstack/spec/system");
|
|
5901
6922
|
|
|
5902
6923
|
// src/build-access-matrix.ts
|
|
5903
|
-
function
|
|
6924
|
+
function asArray32(v) {
|
|
5904
6925
|
if (Array.isArray(v)) return v;
|
|
5905
6926
|
if (v && typeof v === "object") {
|
|
5906
6927
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -5911,13 +6932,13 @@ function buildAccessMatrix(stack) {
|
|
|
5911
6932
|
const entries = [];
|
|
5912
6933
|
if (!stack || typeof stack !== "object") return { version: 1, entries };
|
|
5913
6934
|
const owdByObject = /* @__PURE__ */ new Map();
|
|
5914
|
-
for (const obj of
|
|
6935
|
+
for (const obj of asArray32(stack.objects)) {
|
|
5915
6936
|
const name = typeof obj.name === "string" ? obj.name : "";
|
|
5916
6937
|
if (!name) continue;
|
|
5917
6938
|
const owd = obj.sharingModel ?? obj.security?.sharingModel;
|
|
5918
6939
|
if (typeof owd === "string") owdByObject.set(name, owd);
|
|
5919
6940
|
}
|
|
5920
|
-
for (const ps of
|
|
6941
|
+
for (const ps of asArray32(stack.permissions)) {
|
|
5921
6942
|
const psName = typeof ps.name === "string" ? ps.name : "";
|
|
5922
6943
|
if (!psName) continue;
|
|
5923
6944
|
const objects = ps.objects && typeof ps.objects === "object" ? ps.objects : {};
|
|
@@ -5990,34 +7011,34 @@ function diffAccessMatrix(before, after) {
|
|
|
5990
7011
|
|
|
5991
7012
|
// src/validate-nav-access.ts
|
|
5992
7013
|
var NAV_OBJECT_UNGRANTED = "nav-object-ungranted";
|
|
5993
|
-
function
|
|
7014
|
+
function asArray33(v) {
|
|
5994
7015
|
if (Array.isArray(v)) return v;
|
|
5995
7016
|
if (v && typeof v === "object") {
|
|
5996
7017
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
5997
7018
|
}
|
|
5998
7019
|
return [];
|
|
5999
7020
|
}
|
|
6000
|
-
function
|
|
7021
|
+
function strName14(v) {
|
|
6001
7022
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6002
7023
|
}
|
|
6003
7024
|
function collectNavExposures(stack) {
|
|
6004
7025
|
const out = [];
|
|
6005
|
-
const apps =
|
|
7026
|
+
const apps = asArray33(stack.apps);
|
|
6006
7027
|
for (let ai = 0; ai < apps.length; ai++) {
|
|
6007
7028
|
const app = apps[ai];
|
|
6008
7029
|
if (!app || typeof app !== "object") continue;
|
|
6009
|
-
const appName =
|
|
7030
|
+
const appName = strName14(app.name) ?? `#${ai}`;
|
|
6010
7031
|
const walk = (items, basePath) => {
|
|
6011
|
-
const navItems =
|
|
7032
|
+
const navItems = asArray33(items);
|
|
6012
7033
|
for (let ni = 0; ni < navItems.length; ni++) {
|
|
6013
7034
|
const nav = navItems[ni];
|
|
6014
7035
|
if (!nav || typeof nav !== "object") continue;
|
|
6015
7036
|
const navPath = `${basePath}[${ni}]`;
|
|
6016
|
-
const objectName =
|
|
7037
|
+
const objectName = strName14(nav.objectName);
|
|
6017
7038
|
if (nav.type === "object" && objectName) {
|
|
6018
7039
|
out.push({
|
|
6019
7040
|
objectName,
|
|
6020
|
-
where: `app "${appName}" \xB7 nav "${
|
|
7041
|
+
where: `app "${appName}" \xB7 nav "${strName14(nav.id) ?? `#${ni}`}"`,
|
|
6021
7042
|
path: `${navPath}.objectName`
|
|
6022
7043
|
});
|
|
6023
7044
|
}
|
|
@@ -6025,7 +7046,7 @@ function collectNavExposures(stack) {
|
|
|
6025
7046
|
}
|
|
6026
7047
|
};
|
|
6027
7048
|
walk(app.navigation, `apps[${ai}].navigation`);
|
|
6028
|
-
const areas =
|
|
7049
|
+
const areas = asArray33(app.areas);
|
|
6029
7050
|
for (let ri = 0; ri < areas.length; ri++) {
|
|
6030
7051
|
walk(areas[ri]?.navigation, `apps[${ai}].areas[${ri}].navigation`);
|
|
6031
7052
|
}
|
|
@@ -6035,13 +7056,13 @@ function collectNavExposures(stack) {
|
|
|
6035
7056
|
function validateNavAccess(stack) {
|
|
6036
7057
|
const findings = [];
|
|
6037
7058
|
if (!stack || typeof stack !== "object") return findings;
|
|
6038
|
-
const permissionSets =
|
|
7059
|
+
const permissionSets = asArray33(stack.permissions);
|
|
6039
7060
|
if (permissionSets.length === 0) return findings;
|
|
6040
7061
|
const exposures = collectNavExposures(stack);
|
|
6041
7062
|
if (exposures.length === 0) return findings;
|
|
6042
7063
|
const ownObjects = /* @__PURE__ */ new Set();
|
|
6043
|
-
for (const obj of
|
|
6044
|
-
const n =
|
|
7064
|
+
for (const obj of asArray33(stack.objects)) {
|
|
7065
|
+
const n = strName14(obj.name);
|
|
6045
7066
|
if (n) ownObjects.add(n);
|
|
6046
7067
|
}
|
|
6047
7068
|
const readable = /* @__PURE__ */ new Set();
|
|
@@ -6070,18 +7091,19 @@ function validateNavAccess(stack) {
|
|
|
6070
7091
|
}
|
|
6071
7092
|
|
|
6072
7093
|
// src/validate-translation-references.ts
|
|
7094
|
+
var import_spec4 = require("@objectstack/spec");
|
|
6073
7095
|
var import_system4 = require("@objectstack/spec/system");
|
|
6074
7096
|
var TRANSLATION_TARGET_UNKNOWN = "translation-target-unknown";
|
|
6075
7097
|
var TRANSLATION_OPTION_KEY_UNKNOWN = "translation-option-key-unknown";
|
|
6076
|
-
function
|
|
7098
|
+
function isRec17(v) {
|
|
6077
7099
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6078
7100
|
}
|
|
6079
|
-
function
|
|
7101
|
+
function asArray34(v) {
|
|
6080
7102
|
if (Array.isArray(v)) return v;
|
|
6081
|
-
if (
|
|
7103
|
+
if (isRec17(v)) return Object.entries(v).map(([name, def]) => ({ name, ...isRec17(def) ? def : {} }));
|
|
6082
7104
|
return [];
|
|
6083
7105
|
}
|
|
6084
|
-
function
|
|
7106
|
+
function strName15(v) {
|
|
6085
7107
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6086
7108
|
}
|
|
6087
7109
|
function distance5(a, b) {
|
|
@@ -6141,30 +7163,52 @@ function collectViewRecord(view, factsFor) {
|
|
|
6141
7163
|
};
|
|
6142
7164
|
const addSections = (container, binding) => {
|
|
6143
7165
|
if (!binding) return;
|
|
6144
|
-
for (const section of
|
|
6145
|
-
const sectionName =
|
|
7166
|
+
for (const section of asArray34(container.sections)) {
|
|
7167
|
+
const sectionName = strName15(section.name);
|
|
6146
7168
|
if (sectionName) factsFor(binding).sections.add(sectionName);
|
|
6147
7169
|
}
|
|
6148
7170
|
};
|
|
6149
|
-
const listBinding =
|
|
6150
|
-
if (
|
|
6151
|
-
addView(recordObject ?? listBinding,
|
|
6152
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
7171
|
+
const listBinding = isRec17(view.list) ? bindingOf(view.list) : void 0;
|
|
7172
|
+
if (isRec17(view.list)) addView(listBinding, defaultListViewKey(listBinding, view));
|
|
7173
|
+
addView(recordObject ?? listBinding, strName15(view.name));
|
|
7174
|
+
const named = namedViewKeys(view);
|
|
7175
|
+
for (const family of ["listViews", "formViews"]) {
|
|
7176
|
+
const container = view[family];
|
|
7177
|
+
if (!isRec17(container)) continue;
|
|
7178
|
+
const registryKeys = family === "listViews" ? named.list : named.form;
|
|
7179
|
+
let at = 0;
|
|
7180
|
+
for (const sub of Object.values(container)) {
|
|
7181
|
+
if (!sub || typeof sub !== "object") continue;
|
|
7182
|
+
const registryKey = registryKeys[at++];
|
|
7183
|
+
if (!isRec17(sub)) continue;
|
|
6157
7184
|
const binding = bindingOf(sub) ?? listBinding;
|
|
6158
|
-
addView(binding,
|
|
6159
|
-
addView(binding, strName13(sub.name));
|
|
7185
|
+
addView(binding, registryKey);
|
|
6160
7186
|
addSections(sub, binding);
|
|
6161
7187
|
}
|
|
6162
7188
|
}
|
|
6163
|
-
if (
|
|
7189
|
+
if (isRec17(view.form)) addSections(view.form, bindingOf(view.form) ?? listBinding);
|
|
6164
7190
|
addSections(view, recordObject ?? listBinding);
|
|
6165
7191
|
}
|
|
6166
|
-
function
|
|
6167
|
-
|
|
7192
|
+
function defaultListViewKey(object, container) {
|
|
7193
|
+
if (!object || !isRec17(container.list)) return void 0;
|
|
7194
|
+
const item = (0, import_spec4.expandViewContainer)(object, container).find(
|
|
7195
|
+
(i) => i.viewKind === "list" && i.isDefault
|
|
7196
|
+
);
|
|
7197
|
+
if (!item) return void 0;
|
|
7198
|
+
const prefix = `${object}.`;
|
|
7199
|
+
return item.name.startsWith(prefix) ? item.name.slice(prefix.length) : item.name;
|
|
7200
|
+
}
|
|
7201
|
+
function namedViewKeys(container) {
|
|
7202
|
+
const object = "probe";
|
|
7203
|
+
const prefix = `${object}.`;
|
|
7204
|
+
const bare = (name) => name.startsWith(prefix) ? name.slice(prefix.length) : name;
|
|
7205
|
+
const countEntries = (v) => isRec17(v) ? Object.values(v).filter((e) => !!e && typeof e === "object").length : 0;
|
|
7206
|
+
const listCount = countEntries(container.listViews);
|
|
7207
|
+
const formCount = countEntries(container.formViews);
|
|
7208
|
+
if (!listCount && !formCount) return { list: [], form: [] };
|
|
7209
|
+
const items = (0, import_spec4.expandViewContainer)(object, container);
|
|
7210
|
+
const keysOf2 = (kind, count) => items.filter((i) => i.viewKind === kind).slice(0, count).map((i) => bare(i.name));
|
|
7211
|
+
return { list: keysOf2("list", listCount), form: keysOf2("form", formCount) };
|
|
6168
7212
|
}
|
|
6169
7213
|
function readOptions(field) {
|
|
6170
7214
|
const raw = field.options;
|
|
@@ -6176,14 +7220,14 @@ function readOptions(field) {
|
|
|
6176
7220
|
values.add(opt);
|
|
6177
7221
|
continue;
|
|
6178
7222
|
}
|
|
6179
|
-
if (!
|
|
6180
|
-
const value =
|
|
7223
|
+
if (!isRec17(opt)) continue;
|
|
7224
|
+
const value = strName15(opt.value);
|
|
6181
7225
|
if (!value) continue;
|
|
6182
7226
|
values.add(value);
|
|
6183
|
-
const label2 =
|
|
7227
|
+
const label2 = strName15(opt.label);
|
|
6184
7228
|
if (label2) byLabel.set(label2.toLowerCase(), value);
|
|
6185
7229
|
}
|
|
6186
|
-
} else if (
|
|
7230
|
+
} else if (isRec17(raw)) {
|
|
6187
7231
|
for (const [value, label2] of Object.entries(raw)) {
|
|
6188
7232
|
values.add(value);
|
|
6189
7233
|
if (typeof label2 === "string" && label2.length > 0) byLabel.set(label2.toLowerCase(), value);
|
|
@@ -6203,48 +7247,48 @@ function buildUniverse(stack) {
|
|
|
6203
7247
|
}
|
|
6204
7248
|
return facts;
|
|
6205
7249
|
};
|
|
6206
|
-
for (const obj of
|
|
6207
|
-
const objectName =
|
|
7250
|
+
for (const obj of asArray34(stack.objects)) {
|
|
7251
|
+
const objectName = strName15(obj.name);
|
|
6208
7252
|
if (!objectName) continue;
|
|
6209
7253
|
const facts = factsFor(objectName);
|
|
6210
|
-
for (const field of
|
|
6211
|
-
const fieldName =
|
|
7254
|
+
for (const field of asArray34(obj.fields)) {
|
|
7255
|
+
const fieldName = strName15(field.name);
|
|
6212
7256
|
if (fieldName) facts.fields.set(fieldName, field);
|
|
6213
7257
|
}
|
|
6214
|
-
for (const action of
|
|
6215
|
-
const actionName =
|
|
7258
|
+
for (const action of asArray34(obj.actions)) {
|
|
7259
|
+
const actionName = strName15(action.name);
|
|
6216
7260
|
if (actionName) facts.actions.set(actionName, action);
|
|
6217
7261
|
}
|
|
6218
|
-
for (const view of
|
|
6219
|
-
collectViewRecord({ ...view, object:
|
|
7262
|
+
for (const view of asArray34(obj.views)) {
|
|
7263
|
+
collectViewRecord({ ...view, object: strName15(view.object) ?? objectName }, factsFor);
|
|
6220
7264
|
}
|
|
6221
7265
|
collectViewRecord({ object: objectName, listViews: obj.listViews }, factsFor);
|
|
6222
|
-
for (const group of
|
|
6223
|
-
const key =
|
|
7266
|
+
for (const group of asArray34(obj.fieldGroups)) {
|
|
7267
|
+
const key = strName15(group.key) ?? strName15(group.name);
|
|
6224
7268
|
if (key) facts.sections.add(key);
|
|
6225
7269
|
}
|
|
6226
7270
|
}
|
|
6227
|
-
for (const view of
|
|
7271
|
+
for (const view of asArray34(stack.views)) {
|
|
6228
7272
|
collectViewRecord(view, factsFor);
|
|
6229
7273
|
}
|
|
6230
|
-
const pages =
|
|
7274
|
+
const pages = asArray34(stack.pages);
|
|
6231
7275
|
for (let pi = 0; pi < pages.length; pi++) {
|
|
6232
7276
|
for (const walked of walkPageComponents(pages[pi], `pages[${pi}]`)) {
|
|
6233
7277
|
if (!walked.objectName) continue;
|
|
6234
|
-
const props =
|
|
7278
|
+
const props = isRec17(walked.component.properties) ? walked.component.properties : void 0;
|
|
6235
7279
|
if (!props) continue;
|
|
6236
|
-
for (const section of
|
|
6237
|
-
const sectionName =
|
|
7280
|
+
for (const section of asArray34(props.sections)) {
|
|
7281
|
+
const sectionName = strName15(section.name);
|
|
6238
7282
|
if (sectionName) factsFor(walked.objectName).sections.add(sectionName);
|
|
6239
7283
|
}
|
|
6240
7284
|
}
|
|
6241
7285
|
}
|
|
6242
7286
|
const globalActions = /* @__PURE__ */ new Map();
|
|
6243
7287
|
const actionOwners = /* @__PURE__ */ new Map();
|
|
6244
|
-
for (const action of
|
|
6245
|
-
const actionName =
|
|
7288
|
+
for (const action of asArray34(stack.actions)) {
|
|
7289
|
+
const actionName = strName15(action.name);
|
|
6246
7290
|
if (!actionName) continue;
|
|
6247
|
-
const owner =
|
|
7291
|
+
const owner = strName15(action.objectName) ?? strName15(action.object);
|
|
6248
7292
|
if (owner) {
|
|
6249
7293
|
factsFor(owner).actions.set(actionName, action);
|
|
6250
7294
|
actionOwners.set(actionName, owner);
|
|
@@ -6258,41 +7302,41 @@ function buildUniverse(stack) {
|
|
|
6258
7302
|
}
|
|
6259
7303
|
}
|
|
6260
7304
|
const apps = /* @__PURE__ */ new Map();
|
|
6261
|
-
for (const app of
|
|
6262
|
-
const appName =
|
|
7305
|
+
for (const app of asArray34(stack.apps)) {
|
|
7306
|
+
const appName = strName15(app.name);
|
|
6263
7307
|
if (!appName) continue;
|
|
6264
7308
|
const navIds = apps.get(appName) ?? /* @__PURE__ */ new Set();
|
|
6265
7309
|
const walkNav = (items) => {
|
|
6266
|
-
for (const item of
|
|
6267
|
-
const id =
|
|
7310
|
+
for (const item of asArray34(items)) {
|
|
7311
|
+
const id = strName15(item.id);
|
|
6268
7312
|
if (id) navIds.add(id);
|
|
6269
7313
|
if (item.children) walkNav(item.children);
|
|
6270
7314
|
}
|
|
6271
7315
|
};
|
|
6272
7316
|
walkNav(app.navigation);
|
|
6273
|
-
for (const area of
|
|
6274
|
-
const areaId =
|
|
7317
|
+
for (const area of asArray34(app.areas)) {
|
|
7318
|
+
const areaId = strName15(area.id);
|
|
6275
7319
|
if (areaId) navIds.add(areaId);
|
|
6276
7320
|
walkNav(area.navigation);
|
|
6277
7321
|
}
|
|
6278
7322
|
apps.set(appName, navIds);
|
|
6279
7323
|
}
|
|
6280
7324
|
const dashboards = /* @__PURE__ */ new Map();
|
|
6281
|
-
for (const dash of
|
|
6282
|
-
const dashName =
|
|
7325
|
+
for (const dash of asArray34(stack.dashboards)) {
|
|
7326
|
+
const dashName = strName15(dash.name);
|
|
6283
7327
|
if (!dashName) continue;
|
|
6284
7328
|
const widgets = /* @__PURE__ */ new Set();
|
|
6285
|
-
for (const widget of
|
|
6286
|
-
const id =
|
|
7329
|
+
for (const widget of asArray34(dash.widgets)) {
|
|
7330
|
+
const id = strName15(widget.id) ?? strName15(widget.name);
|
|
6287
7331
|
if (id) widgets.add(id);
|
|
6288
7332
|
}
|
|
6289
7333
|
const actions = /* @__PURE__ */ new Set();
|
|
6290
7334
|
const headerActions = [
|
|
6291
|
-
...
|
|
6292
|
-
...
|
|
7335
|
+
...asArray34(isRec17(dash.header) ? dash.header.actions : void 0),
|
|
7336
|
+
...asArray34(dash.actions)
|
|
6293
7337
|
];
|
|
6294
7338
|
for (const action of headerActions) {
|
|
6295
|
-
const key =
|
|
7339
|
+
const key = strName15(action.actionUrl) ?? strName15(action.url) ?? strName15(action.name);
|
|
6296
7340
|
if (key) actions.add(key);
|
|
6297
7341
|
}
|
|
6298
7342
|
dashboards.set(dashName, { widgets, actions });
|
|
@@ -6304,7 +7348,7 @@ function localePath(bundleIndex, locale) {
|
|
|
6304
7348
|
}
|
|
6305
7349
|
function validateTranslationReferences(stack) {
|
|
6306
7350
|
const findings = [];
|
|
6307
|
-
if (!
|
|
7351
|
+
if (!isRec17(stack)) return findings;
|
|
6308
7352
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6309
7353
|
if (bundles.length === 0) return findings;
|
|
6310
7354
|
const universe = buildUniverse(stack);
|
|
@@ -6313,13 +7357,13 @@ function validateTranslationReferences(stack) {
|
|
|
6313
7357
|
};
|
|
6314
7358
|
for (let bi = 0; bi < bundles.length; bi++) {
|
|
6315
7359
|
const bundle = bundles[bi];
|
|
6316
|
-
if (!
|
|
7360
|
+
if (!isRec17(bundle)) continue;
|
|
6317
7361
|
for (const [locale, rawData] of Object.entries(bundle)) {
|
|
6318
|
-
if (!
|
|
7362
|
+
if (!isRec17(rawData)) continue;
|
|
6319
7363
|
const base = localePath(bi, locale);
|
|
6320
7364
|
const inLocale = `locale "${locale}"`;
|
|
6321
7365
|
for (const [objectName, rawNode] of Object.entries(asRecord(rawData.objects))) {
|
|
6322
|
-
if (!
|
|
7366
|
+
if (!isRec17(rawNode)) continue;
|
|
6323
7367
|
const objPath = `${base}.objects.${objectName}`;
|
|
6324
7368
|
const facts = universe.objects.get(objectName);
|
|
6325
7369
|
if (!facts) {
|
|
@@ -6345,7 +7389,7 @@ function validateTranslationReferences(stack) {
|
|
|
6345
7389
|
);
|
|
6346
7390
|
continue;
|
|
6347
7391
|
}
|
|
6348
|
-
if (!
|
|
7392
|
+
if (!isRec17(rawField)) continue;
|
|
6349
7393
|
checkOptionKeys(findings, {
|
|
6350
7394
|
optionMap: rawField.options,
|
|
6351
7395
|
field,
|
|
@@ -6427,7 +7471,7 @@ function validateTranslationReferences(stack) {
|
|
|
6427
7471
|
);
|
|
6428
7472
|
continue;
|
|
6429
7473
|
}
|
|
6430
|
-
if (!
|
|
7474
|
+
if (!isRec17(rawApp)) continue;
|
|
6431
7475
|
for (const navId of Object.keys(asRecord(rawApp.navigation))) {
|
|
6432
7476
|
if (navIds.has(navId)) continue;
|
|
6433
7477
|
orphan(
|
|
@@ -6450,7 +7494,7 @@ function validateTranslationReferences(stack) {
|
|
|
6450
7494
|
);
|
|
6451
7495
|
continue;
|
|
6452
7496
|
}
|
|
6453
|
-
if (!
|
|
7497
|
+
if (!isRec17(rawDash)) continue;
|
|
6454
7498
|
for (const widgetId of Object.keys(asRecord(rawDash.widgets))) {
|
|
6455
7499
|
if (dash.widgets.has(widgetId)) continue;
|
|
6456
7500
|
orphan(
|
|
@@ -6475,7 +7519,7 @@ function validateTranslationReferences(stack) {
|
|
|
6475
7519
|
return findings;
|
|
6476
7520
|
}
|
|
6477
7521
|
function asRecord(v) {
|
|
6478
|
-
return
|
|
7522
|
+
return isRec17(v) ? v : {};
|
|
6479
7523
|
}
|
|
6480
7524
|
function checkOptionKeys(findings, ctx) {
|
|
6481
7525
|
const optionKeys = Object.keys(asRecord(ctx.optionMap));
|
|
@@ -6487,7 +7531,7 @@ function checkOptionKeys(findings, ctx) {
|
|
|
6487
7531
|
rule: TRANSLATION_OPTION_KEY_UNKNOWN,
|
|
6488
7532
|
where: ctx.where,
|
|
6489
7533
|
path: ctx.path,
|
|
6490
|
-
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${
|
|
7534
|
+
message: `Option translations are keyed under field "${ctx.fieldName}" of object "${ctx.objectName}", which declares no \`options\` at all (field type "${strName15(ctx.field.type) ?? "unknown"}"). Nothing reads this map.`,
|
|
6491
7535
|
hint: `Declare the options on the field, move the translations to the field that owns them, or drop them.`
|
|
6492
7536
|
});
|
|
6493
7537
|
return;
|
|
@@ -6506,11 +7550,11 @@ function checkOptionKeys(findings, ctx) {
|
|
|
6506
7550
|
}
|
|
6507
7551
|
}
|
|
6508
7552
|
function checkActionParams(findings, ctx) {
|
|
6509
|
-
const rawParams = Object.keys(asRecord(
|
|
7553
|
+
const rawParams = Object.keys(asRecord(isRec17(ctx.rawAction) ? ctx.rawAction.params : void 0));
|
|
6510
7554
|
if (rawParams.length === 0) return;
|
|
6511
7555
|
const declared = /* @__PURE__ */ new Set();
|
|
6512
|
-
for (const param of
|
|
6513
|
-
const name =
|
|
7556
|
+
for (const param of asArray34(ctx.action.params)) {
|
|
7557
|
+
const name = strName15(param.name) ?? strName15(param.field);
|
|
6514
7558
|
if (name) declared.add(name);
|
|
6515
7559
|
}
|
|
6516
7560
|
for (const paramName of rawParams) {
|
|
@@ -6528,76 +7572,40 @@ function checkActionParams(findings, ctx) {
|
|
|
6528
7572
|
|
|
6529
7573
|
// src/validate-translatable-sections.ts
|
|
6530
7574
|
var TRANSLATION_SECTION_NAME_MISSING = "translation-section-name-missing";
|
|
6531
|
-
function
|
|
7575
|
+
function isRec18(v) {
|
|
6532
7576
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
6533
7577
|
}
|
|
6534
|
-
function
|
|
7578
|
+
function strName16(v) {
|
|
6535
7579
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6536
7580
|
}
|
|
6537
|
-
function viewObjectName2(view) {
|
|
6538
|
-
return strName14(view.objectName) ?? strName14(view.object) ?? (isRec14(view.data) ? strName14(view.data.object) : void 0);
|
|
6539
|
-
}
|
|
6540
|
-
function collectionEntries(v, base) {
|
|
6541
|
-
if (Array.isArray(v)) {
|
|
6542
|
-
const out = [];
|
|
6543
|
-
for (let i = 0; i < v.length; i++) {
|
|
6544
|
-
if (isRec14(v[i])) out.push({ rec: v[i], path: `${base}[${i}]` });
|
|
6545
|
-
}
|
|
6546
|
-
return out;
|
|
6547
|
-
}
|
|
6548
|
-
if (isRec14(v)) {
|
|
6549
|
-
return Object.entries(v).filter(([, def]) => isRec14(def)).map(([name, def]) => ({ rec: { name, ...def }, path: `${base}.${name}` }));
|
|
6550
|
-
}
|
|
6551
|
-
return [];
|
|
6552
|
-
}
|
|
6553
7581
|
function viewLabel(view) {
|
|
6554
|
-
const name =
|
|
7582
|
+
const name = strName16(view.name);
|
|
6555
7583
|
return name ? `view "${name}"` : "";
|
|
6556
7584
|
}
|
|
6557
7585
|
function joinWhere(...parts) {
|
|
6558
7586
|
return parts.filter((p) => p.length > 0).join(" \xB7 ");
|
|
6559
7587
|
}
|
|
6560
7588
|
function collectViewSites(view, basePath, label2, sites) {
|
|
6561
|
-
const recordObject =
|
|
6562
|
-
const listBinding =
|
|
6563
|
-
const
|
|
6564
|
-
sites.push({
|
|
6565
|
-
path: `${basePath}.sections`,
|
|
6566
|
-
surface: label2,
|
|
6567
|
-
objectName: recordObject ?? listBinding,
|
|
6568
|
-
sections: view.sections
|
|
6569
|
-
});
|
|
6570
|
-
if (isRec14(view.form)) {
|
|
7589
|
+
const recordObject = viewObjectName(view);
|
|
7590
|
+
const listBinding = isRec18(view.list) ? viewObjectName(view.list) ?? recordObject : void 0;
|
|
7591
|
+
for (const site of viewContainerSites(view, basePath)) {
|
|
6571
7592
|
sites.push({
|
|
6572
|
-
path: `${
|
|
6573
|
-
surface: joinWhere(label2,
|
|
6574
|
-
objectName:
|
|
6575
|
-
sections: view.
|
|
7593
|
+
path: `${site.path}.sections`,
|
|
7594
|
+
surface: joinWhere(label2, site.surface),
|
|
7595
|
+
objectName: viewObjectName(site.view) ?? recordObject ?? listBinding,
|
|
7596
|
+
sections: site.view.sections
|
|
6576
7597
|
});
|
|
6577
7598
|
}
|
|
6578
|
-
for (const key of ["listViews", "formViews"]) {
|
|
6579
|
-
const container = view[key];
|
|
6580
|
-
if (!isRec14(container)) continue;
|
|
6581
|
-
for (const [subKey, sub] of Object.entries(container)) {
|
|
6582
|
-
if (!isRec14(sub)) continue;
|
|
6583
|
-
sites.push({
|
|
6584
|
-
path: `${basePath}.${key}.${subKey}.sections`,
|
|
6585
|
-
surface: joinWhere(label2, `${key}.${subKey}`),
|
|
6586
|
-
objectName: bindingOf(sub) ?? listBinding,
|
|
6587
|
-
sections: sub.sections
|
|
6588
|
-
});
|
|
6589
|
-
}
|
|
6590
|
-
}
|
|
6591
7599
|
}
|
|
6592
7600
|
function translatedObjectNames(stack) {
|
|
6593
7601
|
const out = /* @__PURE__ */ new Set();
|
|
6594
7602
|
const bundles = Array.isArray(stack.translations) ? stack.translations : [];
|
|
6595
7603
|
for (const bundle of bundles) {
|
|
6596
|
-
if (!
|
|
7604
|
+
if (!isRec18(bundle)) continue;
|
|
6597
7605
|
for (const data of Object.values(bundle)) {
|
|
6598
|
-
if (!
|
|
7606
|
+
if (!isRec18(data) || !isRec18(data.objects)) continue;
|
|
6599
7607
|
for (const [objectName, node] of Object.entries(data.objects)) {
|
|
6600
|
-
if (
|
|
7608
|
+
if (isRec18(node)) out.add(objectName);
|
|
6601
7609
|
}
|
|
6602
7610
|
}
|
|
6603
7611
|
}
|
|
@@ -6609,22 +7617,22 @@ function suggestedName(label2) {
|
|
|
6609
7617
|
}
|
|
6610
7618
|
function validateTranslatableSections(stack) {
|
|
6611
7619
|
const findings = [];
|
|
6612
|
-
if (!
|
|
7620
|
+
if (!isRec18(stack)) return findings;
|
|
6613
7621
|
const translated = translatedObjectNames(stack);
|
|
6614
7622
|
if (translated.size === 0) return findings;
|
|
6615
7623
|
const sites = [];
|
|
6616
7624
|
for (const { rec: obj, path: objPath } of collectionEntries(stack.objects, "objects")) {
|
|
6617
|
-
const objectName =
|
|
7625
|
+
const objectName = strName16(obj.name);
|
|
6618
7626
|
if (!objectName) continue;
|
|
6619
7627
|
for (const { rec: view, path } of collectionEntries(obj.views, `${objPath}.views`)) {
|
|
6620
7628
|
collectViewSites(
|
|
6621
|
-
{ ...view, object:
|
|
7629
|
+
{ ...view, object: strName16(view.object) ?? objectName },
|
|
6622
7630
|
path,
|
|
6623
7631
|
viewLabel(view),
|
|
6624
7632
|
sites
|
|
6625
7633
|
);
|
|
6626
7634
|
}
|
|
6627
|
-
if (
|
|
7635
|
+
if (isRec18(obj.listViews)) {
|
|
6628
7636
|
collectViewSites({ object: objectName, listViews: obj.listViews }, objPath, "", sites);
|
|
6629
7637
|
}
|
|
6630
7638
|
}
|
|
@@ -6632,13 +7640,13 @@ function validateTranslatableSections(stack) {
|
|
|
6632
7640
|
collectViewSites(view, path, viewLabel(view), sites);
|
|
6633
7641
|
}
|
|
6634
7642
|
for (const { rec: page, path: pagePath } of collectionEntries(stack.pages, "pages")) {
|
|
6635
|
-
const pageName =
|
|
7643
|
+
const pageName = strName16(page.name);
|
|
6636
7644
|
const pageLabel = pageName ? `page "${pageName}"` : "";
|
|
6637
7645
|
for (const walked of walkPageComponents(page, pagePath)) {
|
|
6638
7646
|
if (!walked.objectName) continue;
|
|
6639
|
-
const props =
|
|
7647
|
+
const props = isRec18(walked.component.properties) ? walked.component.properties : void 0;
|
|
6640
7648
|
if (!props) continue;
|
|
6641
|
-
const type =
|
|
7649
|
+
const type = strName16(walked.component.type) ?? "component";
|
|
6642
7650
|
sites.push({
|
|
6643
7651
|
path: `${walked.path}.properties.sections`,
|
|
6644
7652
|
surface: joinWhere(pageLabel, type),
|
|
@@ -6653,9 +7661,9 @@ function validateTranslatableSections(stack) {
|
|
|
6653
7661
|
if (!Array.isArray(site.sections)) continue;
|
|
6654
7662
|
for (let i = 0; i < site.sections.length; i++) {
|
|
6655
7663
|
const section = site.sections[i];
|
|
6656
|
-
if (!
|
|
6657
|
-
if (
|
|
6658
|
-
const heading =
|
|
7664
|
+
if (!isRec18(section)) continue;
|
|
7665
|
+
if (strName16(section.name)) continue;
|
|
7666
|
+
const heading = strName16(section.label);
|
|
6659
7667
|
if (!heading) continue;
|
|
6660
7668
|
const slug = suggestedName(heading);
|
|
6661
7669
|
findings.push({
|
|
@@ -6673,14 +7681,14 @@ function validateTranslatableSections(stack) {
|
|
|
6673
7681
|
|
|
6674
7682
|
// src/validate-ai-surface-affinity.ts
|
|
6675
7683
|
var AI_SKILL_SURFACE_MISMATCH = "ai-skill-surface-mismatch";
|
|
6676
|
-
function
|
|
7684
|
+
function asArray35(v) {
|
|
6677
7685
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6678
7686
|
if (v && typeof v === "object") {
|
|
6679
7687
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6680
7688
|
}
|
|
6681
7689
|
return [];
|
|
6682
7690
|
}
|
|
6683
|
-
function
|
|
7691
|
+
function strName17(v) {
|
|
6684
7692
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6685
7693
|
}
|
|
6686
7694
|
function surfaceOf(v) {
|
|
@@ -6690,18 +7698,18 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
6690
7698
|
const findings = [];
|
|
6691
7699
|
if (!stack || typeof stack !== "object") return findings;
|
|
6692
7700
|
const skillsByName = /* @__PURE__ */ new Map();
|
|
6693
|
-
for (const skill of
|
|
6694
|
-
const n =
|
|
7701
|
+
for (const skill of asArray35(stack.skills)) {
|
|
7702
|
+
const n = strName17(skill.name);
|
|
6695
7703
|
if (n) skillsByName.set(n, skill);
|
|
6696
7704
|
}
|
|
6697
|
-
const agents =
|
|
7705
|
+
const agents = asArray35(stack.agents);
|
|
6698
7706
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
6699
7707
|
const agent = agents[ai];
|
|
6700
|
-
const agentName =
|
|
7708
|
+
const agentName = strName17(agent.name) ?? `#${ai}`;
|
|
6701
7709
|
const agentSurface = surfaceOf(agent.surface);
|
|
6702
7710
|
const skillRefs = Array.isArray(agent.skills) ? agent.skills : [];
|
|
6703
7711
|
for (let si = 0; si < skillRefs.length; si++) {
|
|
6704
|
-
const ref =
|
|
7712
|
+
const ref = strName17(skillRefs[si]);
|
|
6705
7713
|
if (!ref) continue;
|
|
6706
7714
|
const skill = skillsByName.get(ref);
|
|
6707
7715
|
if (!skill) continue;
|
|
@@ -6723,14 +7731,14 @@ function validateAiSurfaceAffinity(stack) {
|
|
|
6723
7731
|
// src/validate-ai-tool-references.ts
|
|
6724
7732
|
var import_system5 = require("@objectstack/spec/system");
|
|
6725
7733
|
var AI_SKILL_TOOL_UNRESOLVED = "ai-skill-tool-unresolved";
|
|
6726
|
-
function
|
|
7734
|
+
function asArray36(v) {
|
|
6727
7735
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6728
7736
|
if (v && typeof v === "object") {
|
|
6729
7737
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6730
7738
|
}
|
|
6731
7739
|
return [];
|
|
6732
7740
|
}
|
|
6733
|
-
function
|
|
7741
|
+
function strName18(v) {
|
|
6734
7742
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6735
7743
|
}
|
|
6736
7744
|
function distance6(a, b) {
|
|
@@ -6771,26 +7779,26 @@ function materialisesAsTool(action) {
|
|
|
6771
7779
|
if (!ai || typeof ai !== "object") return false;
|
|
6772
7780
|
const aiRec = ai;
|
|
6773
7781
|
if (aiRec.exposed !== true) return false;
|
|
6774
|
-
if (!
|
|
6775
|
-
const type =
|
|
7782
|
+
if (!strName18(aiRec.description)) return false;
|
|
7783
|
+
const type = strName18(action.type);
|
|
6776
7784
|
if (!type || !HEADLESS_ACTION_TYPES.has(type)) return false;
|
|
6777
7785
|
if (type === "script") return Boolean(action.target || action.body);
|
|
6778
7786
|
return Boolean(action.target);
|
|
6779
7787
|
}
|
|
6780
7788
|
function collectToolUniverse(stack) {
|
|
6781
7789
|
const universe = new Set(import_system5.PLATFORM_PROVIDED_TOOL_NAMES);
|
|
6782
|
-
for (const tool of
|
|
6783
|
-
const n =
|
|
7790
|
+
for (const tool of asArray36(stack.tools)) {
|
|
7791
|
+
const n = strName18(tool.name);
|
|
6784
7792
|
if (n) universe.add(n);
|
|
6785
7793
|
}
|
|
6786
7794
|
const addActionFamily = (actions) => {
|
|
6787
|
-
for (const action of
|
|
6788
|
-
const n =
|
|
7795
|
+
for (const action of asArray36(actions)) {
|
|
7796
|
+
const n = strName18(action.name);
|
|
6789
7797
|
if (n && materialisesAsTool(action)) universe.add(`action_${n}`);
|
|
6790
7798
|
}
|
|
6791
7799
|
};
|
|
6792
7800
|
addActionFamily(stack.actions);
|
|
6793
|
-
for (const obj of
|
|
7801
|
+
for (const obj of asArray36(stack.objects)) {
|
|
6794
7802
|
addActionFamily(obj.actions);
|
|
6795
7803
|
}
|
|
6796
7804
|
return universe;
|
|
@@ -6798,13 +7806,13 @@ function collectToolUniverse(stack) {
|
|
|
6798
7806
|
function collectUnexposedActionNames(stack) {
|
|
6799
7807
|
const names = /* @__PURE__ */ new Set();
|
|
6800
7808
|
const scan = (actions) => {
|
|
6801
|
-
for (const action of
|
|
6802
|
-
const n =
|
|
7809
|
+
for (const action of asArray36(actions)) {
|
|
7810
|
+
const n = strName18(action.name);
|
|
6803
7811
|
if (n && !materialisesAsTool(action)) names.add(n);
|
|
6804
7812
|
}
|
|
6805
7813
|
};
|
|
6806
7814
|
scan(stack.actions);
|
|
6807
|
-
for (const obj of
|
|
7815
|
+
for (const obj of asArray36(stack.objects)) scan(obj.actions);
|
|
6808
7816
|
return names;
|
|
6809
7817
|
}
|
|
6810
7818
|
function validateAiToolReferences(stack) {
|
|
@@ -6822,13 +7830,13 @@ function validateAiToolReferences(stack) {
|
|
|
6822
7830
|
}
|
|
6823
7831
|
return universe.has(ref);
|
|
6824
7832
|
};
|
|
6825
|
-
const skills =
|
|
7833
|
+
const skills = asArray36(stack.skills);
|
|
6826
7834
|
for (let si = 0; si < skills.length; si++) {
|
|
6827
7835
|
const skill = skills[si];
|
|
6828
|
-
const skillName =
|
|
7836
|
+
const skillName = strName18(skill.name) ?? `#${si}`;
|
|
6829
7837
|
const refs = Array.isArray(skill.tools) ? skill.tools : [];
|
|
6830
7838
|
for (let ti = 0; ti < refs.length; ti++) {
|
|
6831
|
-
const ref =
|
|
7839
|
+
const ref = strName18(refs[ti]);
|
|
6832
7840
|
if (!ref || resolves(ref)) continue;
|
|
6833
7841
|
const isPattern = ref.endsWith("*");
|
|
6834
7842
|
const unexposed = !isPattern && ref.startsWith("action_") && unexposedActions.has(ref.slice("action_".length)) ? ref.slice("action_".length) : void 0;
|
|
@@ -6847,24 +7855,25 @@ function validateAiToolReferences(stack) {
|
|
|
6847
7855
|
|
|
6848
7856
|
// src/validate-ai-agent-authoring.ts
|
|
6849
7857
|
var AGENT_AUTHORING_WITHDRAWN = "agent-authoring-withdrawn";
|
|
6850
|
-
|
|
7858
|
+
var DEFAULT_AGENT_OUTSIDE_ROSTER = "default-agent-outside-roster";
|
|
7859
|
+
function asArray37(v) {
|
|
6851
7860
|
if (Array.isArray(v)) return v.filter((x) => !!x && typeof x === "object");
|
|
6852
7861
|
if (v && typeof v === "object") {
|
|
6853
7862
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
6854
7863
|
}
|
|
6855
7864
|
return [];
|
|
6856
7865
|
}
|
|
6857
|
-
function
|
|
7866
|
+
function strName19(v) {
|
|
6858
7867
|
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
6859
7868
|
}
|
|
6860
7869
|
var PLATFORM_AGENT_NAMES = /* @__PURE__ */ new Set(["ask", "build", "data_chat", "metadata_assistant"]);
|
|
6861
7870
|
function validateAiAgentAuthoring(stack) {
|
|
6862
7871
|
const findings = [];
|
|
6863
7872
|
if (!stack || typeof stack !== "object") return findings;
|
|
6864
|
-
const agents =
|
|
7873
|
+
const agents = asArray37(stack.agents);
|
|
6865
7874
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
6866
7875
|
const agent = agents[ai];
|
|
6867
|
-
const name =
|
|
7876
|
+
const name = strName19(agent.name) ?? `#${ai}`;
|
|
6868
7877
|
const isPlatformName = PLATFORM_AGENT_NAMES.has(name);
|
|
6869
7878
|
const skillCount = Array.isArray(agent.skills) ? agent.skills.length : 0;
|
|
6870
7879
|
findings.push({
|
|
@@ -6876,25 +7885,41 @@ function validateAiAgentAuthoring(stack) {
|
|
|
6876
7885
|
hint: isPlatformName ? `Remove the declaration; the platform owns "${name}". Extend it with skills instead.` : `Delete the agent and express its capability as skills. Everything an agent carried that a skill does not is persona text: move the useful parts of \`instructions\` into the skills' own instructions.` + (skillCount > 0 ? ` The ${skillCount} skill${skillCount === 1 ? "" : "s"} this agent references already carry the capability \u2014 they attach to the platform agent by \`surface\` affinity, so nothing is lost by dropping the persona.` : ``)
|
|
6877
7886
|
});
|
|
6878
7887
|
}
|
|
7888
|
+
const roster = [...PLATFORM_AGENT_NAMES].join(", ");
|
|
7889
|
+
const apps = asArray37(stack.apps);
|
|
7890
|
+
for (let appIdx = 0; appIdx < apps.length; appIdx++) {
|
|
7891
|
+
const app = apps[appIdx];
|
|
7892
|
+
const defaultAgent = strName19(app.defaultAgent);
|
|
7893
|
+
if (!defaultAgent || PLATFORM_AGENT_NAMES.has(defaultAgent)) continue;
|
|
7894
|
+
const appName = strName19(app.name) ?? `#${appIdx}`;
|
|
7895
|
+
findings.push({
|
|
7896
|
+
severity: "warning",
|
|
7897
|
+
rule: DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
7898
|
+
where: `app "${appName}".defaultAgent`,
|
|
7899
|
+
path: `apps[${appIdx}].defaultAgent`,
|
|
7900
|
+
message: `app "${appName}" pins \`defaultAgent\` to "${defaultAgent}", which is not in the platform agent roster (${roster}). The kernel ships exactly two agents (ADR-0063 \xA72) and resolves this key against them and their legacy aliases only \u2014 an unrecognized name is not rejected, it silently falls back to the platform default at runtime, so the pin has no effect and the value drifts from what actually serves the app.`,
|
|
7901
|
+
hint: `Set \`defaultAgent\` to one of the platform agent names: ${roster}. If the goal is a dedicated persona or capability, express it as skills instead \u2014 they attach to "ask" / "build" by surface affinity, not as a custom \`defaultAgent\` value.`
|
|
7902
|
+
});
|
|
7903
|
+
}
|
|
6879
7904
|
return findings;
|
|
6880
7905
|
}
|
|
6881
7906
|
|
|
6882
7907
|
// src/validate-hook-body-writes.ts
|
|
6883
|
-
var
|
|
7908
|
+
var import_node_module5 = require("module");
|
|
6884
7909
|
var import_shared = require("@objectstack/spec/shared");
|
|
6885
|
-
var
|
|
6886
|
-
var
|
|
6887
|
-
function
|
|
6888
|
-
if (
|
|
6889
|
-
const anchor = typeof
|
|
7910
|
+
var import_meta5 = {};
|
|
7911
|
+
var cachedTs3 = null;
|
|
7912
|
+
function loadTypeScript3() {
|
|
7913
|
+
if (cachedTs3) return cachedTs3;
|
|
7914
|
+
const anchor = typeof import_meta5 !== "undefined" && import_meta5.url ? import_meta5.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/";
|
|
6890
7915
|
try {
|
|
6891
|
-
|
|
7916
|
+
cachedTs3 = (0, import_node_module5.createRequire)(anchor)("typescript");
|
|
6892
7917
|
} catch (err) {
|
|
6893
7918
|
throw new Error(
|
|
6894
7919
|
`@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.`
|
|
6895
7920
|
);
|
|
6896
7921
|
}
|
|
6897
|
-
return
|
|
7922
|
+
return cachedTs3;
|
|
6898
7923
|
}
|
|
6899
7924
|
var HOOK_BODY_WRITE_UNKNOWN_FIELD = "hook-body-write-unknown-field";
|
|
6900
7925
|
var HOOK_BODY_WRITE_PATTERNS = [
|
|
@@ -6969,24 +7994,24 @@ var IMPLICIT_FIELDS2 = /* @__PURE__ */ new Set([
|
|
|
6969
7994
|
"owner",
|
|
6970
7995
|
"record_type"
|
|
6971
7996
|
]);
|
|
6972
|
-
var
|
|
6973
|
-
function
|
|
6974
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
6975
|
-
if (
|
|
7997
|
+
var isRec19 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
7998
|
+
function asArray38(v) {
|
|
7999
|
+
if (Array.isArray(v)) return v.filter((x) => isRec19(x));
|
|
8000
|
+
if (isRec19(v)) {
|
|
6976
8001
|
return Object.entries(v).map(([name, def]) => ({
|
|
6977
8002
|
name,
|
|
6978
|
-
...
|
|
8003
|
+
...isRec19(def) ? def : {}
|
|
6979
8004
|
}));
|
|
6980
8005
|
}
|
|
6981
8006
|
return [];
|
|
6982
8007
|
}
|
|
6983
8008
|
function indexObjectFields2(stack) {
|
|
6984
8009
|
const out = /* @__PURE__ */ new Map();
|
|
6985
|
-
for (const obj of
|
|
8010
|
+
for (const obj of asArray38(stack.objects)) {
|
|
6986
8011
|
const name = typeof obj.name === "string" ? obj.name : void 0;
|
|
6987
8012
|
if (!name) continue;
|
|
6988
8013
|
const names = /* @__PURE__ */ new Set();
|
|
6989
|
-
for (const f of
|
|
8014
|
+
for (const f of asArray38(obj.fields)) {
|
|
6990
8015
|
if (typeof f.name === "string" && f.name) names.add(f.name);
|
|
6991
8016
|
}
|
|
6992
8017
|
out.set(name, names);
|
|
@@ -7005,7 +8030,7 @@ function extractHookBodyWriteSet(source) {
|
|
|
7005
8030
|
if (!/\bctx\b/.test(source) && !/\bObject\b/.test(source)) {
|
|
7006
8031
|
return { writes: [], ctxRecordEscapes: false };
|
|
7007
8032
|
}
|
|
7008
|
-
const tsc =
|
|
8033
|
+
const tsc = loadTypeScript3();
|
|
7009
8034
|
const sf = tsc.createSourceFile(
|
|
7010
8035
|
"hook-body.ts",
|
|
7011
8036
|
`async function __body(ctx) {
|
|
@@ -7116,12 +8141,12 @@ ${source}
|
|
|
7116
8141
|
}
|
|
7117
8142
|
function validateHookBodyWrites(stack) {
|
|
7118
8143
|
const findings = [];
|
|
7119
|
-
const hooks =
|
|
8144
|
+
const hooks = asArray38(stack.hooks);
|
|
7120
8145
|
if (hooks.length === 0) return findings;
|
|
7121
8146
|
let objectFields = null;
|
|
7122
8147
|
hooks.forEach((hook, hookIndex) => {
|
|
7123
8148
|
const body = hook.body;
|
|
7124
|
-
if (!
|
|
8149
|
+
if (!isRec19(body) || body.language !== "js") return;
|
|
7125
8150
|
const source = body.source;
|
|
7126
8151
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
7127
8152
|
const writes = extractHookBodyWrites(source).filter((w) => HOOK_APPLICABLE_IDS.has(w.patternId));
|
|
@@ -7202,13 +8227,13 @@ var ACTION_BODY_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_B
|
|
|
7202
8227
|
var ACTION_RECORD_WRITE_PATTERNS = HOOK_BODY_WRITE_PATTERNS.filter((p) => ACTION_RECORD_WRITE_PATTERN_IDS.includes(p.id));
|
|
7203
8228
|
var APPLICABLE_IDS = new Set(ACTION_BODY_WRITE_PATTERN_IDS);
|
|
7204
8229
|
var RECORD_WRITE_IDS = new Set(ACTION_RECORD_WRITE_PATTERN_IDS);
|
|
7205
|
-
var
|
|
7206
|
-
function
|
|
7207
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
7208
|
-
if (
|
|
8230
|
+
var isRec20 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8231
|
+
function asArray39(v) {
|
|
8232
|
+
if (Array.isArray(v)) return v.filter((x) => isRec20(x));
|
|
8233
|
+
if (isRec20(v)) {
|
|
7209
8234
|
return Object.entries(v).map(([name, def]) => ({
|
|
7210
8235
|
name,
|
|
7211
|
-
...
|
|
8236
|
+
...isRec20(def) ? def : {}
|
|
7212
8237
|
}));
|
|
7213
8238
|
}
|
|
7214
8239
|
return [];
|
|
@@ -7222,11 +8247,11 @@ function collectActionBodies(stack) {
|
|
|
7222
8247
|
const sites = [];
|
|
7223
8248
|
const seen = /* @__PURE__ */ new Set();
|
|
7224
8249
|
const collect = (actions, pathPrefix, parentObject) => {
|
|
7225
|
-
|
|
8250
|
+
asArray39(actions).forEach((action, index) => {
|
|
7226
8251
|
const type = typeof action.type === "string" ? action.type : "script";
|
|
7227
8252
|
if (type !== "script") return;
|
|
7228
8253
|
const body = action.body;
|
|
7229
|
-
if (!
|
|
8254
|
+
if (!isRec20(body) || body.language !== "js") return;
|
|
7230
8255
|
const source = body.source;
|
|
7231
8256
|
if (typeof source !== "string" || source.trim() === "") return;
|
|
7232
8257
|
const name = typeof action.name === "string" && action.name ? action.name : `#${index}`;
|
|
@@ -7237,7 +8262,7 @@ function collectActionBodies(stack) {
|
|
|
7237
8262
|
});
|
|
7238
8263
|
};
|
|
7239
8264
|
collect(stack.actions, "actions");
|
|
7240
|
-
|
|
8265
|
+
asArray39(stack.objects).forEach((obj, objIndex) => {
|
|
7241
8266
|
const parentObject = typeof obj.name === "string" && obj.name ? obj.name : void 0;
|
|
7242
8267
|
collect(obj.actions, `objects[${objIndex}].actions`, parentObject);
|
|
7243
8268
|
});
|
|
@@ -7245,7 +8270,7 @@ function collectActionBodies(stack) {
|
|
|
7245
8270
|
}
|
|
7246
8271
|
function validateActionBodyWrites(stack) {
|
|
7247
8272
|
const findings = [];
|
|
7248
|
-
if (!
|
|
8273
|
+
if (!isRec20(stack)) return findings;
|
|
7249
8274
|
const sites = collectActionBodies(stack);
|
|
7250
8275
|
if (sites.length === 0) return findings;
|
|
7251
8276
|
let objectFields = null;
|
|
@@ -7304,13 +8329,13 @@ var import_shared3 = require("@objectstack/spec/shared");
|
|
|
7304
8329
|
var FLOW_NODE_WRITE_UNKNOWN_FIELD = "flow-node-write-unknown-field";
|
|
7305
8330
|
var FLOW_WRITE_NODE_TYPES = ["update_record", "create_record"];
|
|
7306
8331
|
var FLOW_WRITE_NODE_TYPES_DEFERRED = [];
|
|
7307
|
-
var
|
|
7308
|
-
function
|
|
7309
|
-
if (Array.isArray(v)) return v.filter((x) =>
|
|
7310
|
-
if (
|
|
8332
|
+
var isRec21 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
8333
|
+
function asArray40(v) {
|
|
8334
|
+
if (Array.isArray(v)) return v.filter((x) => isRec21(x));
|
|
8335
|
+
if (isRec21(v)) {
|
|
7311
8336
|
return Object.entries(v).map(([name, def]) => ({
|
|
7312
8337
|
name,
|
|
7313
|
-
...
|
|
8338
|
+
...isRec21(def) ? def : {}
|
|
7314
8339
|
}));
|
|
7315
8340
|
}
|
|
7316
8341
|
return [];
|
|
@@ -7323,8 +8348,8 @@ function readLiteralObjectName2(config) {
|
|
|
7323
8348
|
var COVERED_TYPES = new Set(FLOW_WRITE_NODE_TYPES);
|
|
7324
8349
|
function validateFlowNodeWrites(stack) {
|
|
7325
8350
|
const findings = [];
|
|
7326
|
-
if (!
|
|
7327
|
-
const flows =
|
|
8351
|
+
if (!isRec21(stack)) return findings;
|
|
8352
|
+
const flows = asArray40(stack.flows);
|
|
7328
8353
|
if (flows.length === 0) return findings;
|
|
7329
8354
|
let objectFields = null;
|
|
7330
8355
|
flows.forEach((flow, flowIndex) => {
|
|
@@ -7332,10 +8357,10 @@ function validateFlowNodeWrites(stack) {
|
|
|
7332
8357
|
const walked = walkFlowNodes(flow, `flows[${flowIndex}]`);
|
|
7333
8358
|
walked.forEach(({ node, path: nodePath, regionTrail }, walkIndex) => {
|
|
7334
8359
|
if (typeof node.type !== "string" || !COVERED_TYPES.has(node.type)) return;
|
|
7335
|
-
const config =
|
|
8360
|
+
const config = isRec21(node.config) ? node.config : void 0;
|
|
7336
8361
|
if (!config) return;
|
|
7337
8362
|
const fields = config.fields;
|
|
7338
|
-
if (!
|
|
8363
|
+
if (!isRec21(fields)) return;
|
|
7339
8364
|
const written = Object.keys(fields);
|
|
7340
8365
|
if (written.length === 0) return;
|
|
7341
8366
|
const objectName = readLiteralObjectName2(config);
|
|
@@ -7476,7 +8501,8 @@ function validateReferenceIntegrity(stack) {
|
|
|
7476
8501
|
|
|
7477
8502
|
// src/lint-flow-patterns.ts
|
|
7478
8503
|
var import_automation5 = require("@objectstack/spec/automation");
|
|
7479
|
-
|
|
8504
|
+
var import_data8 = require("@objectstack/spec/data");
|
|
8505
|
+
function asArray41(v) {
|
|
7480
8506
|
if (Array.isArray(v)) return v;
|
|
7481
8507
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7482
8508
|
return [];
|
|
@@ -7750,7 +8776,21 @@ function scanBranchRouting(at, nodes, edges, findings) {
|
|
|
7750
8776
|
function filterCarriesNoCondition(filter) {
|
|
7751
8777
|
if (filter === void 0 || filter === null) return true;
|
|
7752
8778
|
if (typeof filter !== "object" || Array.isArray(filter)) return false;
|
|
7753
|
-
return
|
|
8779
|
+
return (0, import_data8.reduceFilterVerdict)(filter) === "true";
|
|
8780
|
+
}
|
|
8781
|
+
function describeUnboundedFilter(filter) {
|
|
8782
|
+
if (filter === void 0 || filter === null) return "no `filter` key";
|
|
8783
|
+
if (Object.keys(filter).length === 0) return "an EMPTY `filter`";
|
|
8784
|
+
return `a \`filter\` that REDUCES TO TRUE (\`${previewFilter(filter)}\`)`;
|
|
8785
|
+
}
|
|
8786
|
+
function previewFilter(filter) {
|
|
8787
|
+
try {
|
|
8788
|
+
const json = JSON.stringify(filter);
|
|
8789
|
+
if (typeof json !== "string") return typeof filter;
|
|
8790
|
+
return json.length > 80 ? `${json.slice(0, 77)}...` : json;
|
|
8791
|
+
} catch {
|
|
8792
|
+
return typeof filter;
|
|
8793
|
+
}
|
|
7754
8794
|
}
|
|
7755
8795
|
function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
7756
8796
|
for (const node of nodes) {
|
|
@@ -7761,10 +8801,10 @@ function scanUnboundedBulkWrites(at, nodes, findings) {
|
|
|
7761
8801
|
if (cfg.multi !== true) continue;
|
|
7762
8802
|
if (!filterCarriesNoCondition(cfg.filter)) continue;
|
|
7763
8803
|
const objectName = typeof cfg.objectName === "string" && cfg.objectName ? cfg.objectName : "(unnamed object)";
|
|
7764
|
-
const filterState = cfg.filter
|
|
8804
|
+
const filterState = describeUnboundedFilter(cfg.filter);
|
|
7765
8805
|
findings.push({
|
|
7766
8806
|
where: `${at} \xB7 node '${String(node.id)}' (${nodeType})`,
|
|
7767
|
-
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards \`where
|
|
8807
|
+
message: `declares \`multi: true\` with ${filterState} \u2014 this is a WHOLE-OBJECT write, by declaration: every row of '${objectName}' is ${consequence2.verb} on every run. The executor forwards the filter as \`where\` (an absent key becomes \`{}\`) plus the bulk intent, ${consequence2.dispatchNote}, and it lands on \`${consequence2.engineCall}\` bounded by nothing \u2014 a filter that reduces to TRUE constrains no row. Nothing refuses it at run time, so the only feedback is the step's \`acted\` row count \u2014 reported AFTER the rows are gone.`,
|
|
7768
8808
|
hint: `Write the constraint you mean into \`filter\` (e.g. \`{ status: 'closed' }\` \u2014 see examples/app-showcase \`showcase_inquiry_purge\`, bulk intent bounded by a predicate). If emptying '${objectName}' really is the intent, keep it: this is a warning, not a gate, and the run-time path stays open. Distinct from the #3810 erased-condition guard, which REFUSES this node at run time when a condition you WROTE interpolated to nothing \u2014 that guard is keyed on "a written condition is gone" and deliberately not on "the filter is empty", which is the fact this rule judges at authoring time. (#5482, #5393)`,
|
|
7769
8809
|
// Warning, not `error`: see the severity policy at the top of this file.
|
|
7770
8810
|
// The shape has a legitimate reading the engine grants on purpose, so it is
|
|
@@ -7846,7 +8886,7 @@ function scanApprovalReviseLoops(at, nodes, edges, findings) {
|
|
|
7846
8886
|
}
|
|
7847
8887
|
function lintFlowPatterns(stack) {
|
|
7848
8888
|
const findings = [];
|
|
7849
|
-
for (const flow of
|
|
8889
|
+
for (const flow of asArray41(stack.flows)) {
|
|
7850
8890
|
const flowName = typeof flow.name === "string" ? flow.name : "(unnamed flow)";
|
|
7851
8891
|
const nodes = Array.isArray(flow.nodes) ? flow.nodes : [];
|
|
7852
8892
|
const edges = Array.isArray(flow.edges) ? flow.edges : [];
|
|
@@ -7938,20 +8978,20 @@ function lintFlowPatterns(stack) {
|
|
|
7938
8978
|
}
|
|
7939
8979
|
|
|
7940
8980
|
// src/lint-liveness-properties.ts
|
|
7941
|
-
var
|
|
8981
|
+
var import_node_module6 = require("module");
|
|
7942
8982
|
var import_node_path = require("path");
|
|
7943
8983
|
var import_node_fs = require("fs");
|
|
7944
|
-
var
|
|
8984
|
+
var import_meta6 = {};
|
|
7945
8985
|
var LIVENESS_DEAD_PROPERTY = "liveness-dead-property";
|
|
7946
8986
|
var LIVENESS_EXPERIMENTAL_PROPERTY = "liveness-experimental-property";
|
|
7947
|
-
function
|
|
8987
|
+
function asArray42(v) {
|
|
7948
8988
|
if (Array.isArray(v)) return v;
|
|
7949
8989
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
7950
8990
|
return [];
|
|
7951
8991
|
}
|
|
7952
8992
|
function resolveLivenessDir() {
|
|
7953
8993
|
try {
|
|
7954
|
-
const require2 = (0,
|
|
8994
|
+
const require2 = (0, import_node_module6.createRequire)(import_meta6.url);
|
|
7955
8995
|
const pkgJson = require2.resolve("@objectstack/spec/package.json");
|
|
7956
8996
|
const dir = (0, import_node_path.join)((0, import_node_path.dirname)(pkgJson), "liveness");
|
|
7957
8997
|
return (0, import_node_fs.existsSync)(dir) ? dir : null;
|
|
@@ -8066,6 +9106,13 @@ var TYPE_COLLECTIONS = [
|
|
|
8066
9106
|
// checks every widget on the dashboard. Registering it here is not optional
|
|
8067
9107
|
// bookkeeping: without it the ledger would be newly correct and newly
|
|
8068
9108
|
// silent, which is the shape this lint exists to prevent.
|
|
9109
|
+
//
|
|
9110
|
+
// As of #6774 the dashboard ledger warns on NOTHING — four of those five were
|
|
9111
|
+
// retired in 17.0.0 (#5010) and `colorVariant` went `live` when objectui#3799
|
|
9112
|
+
// gave it a renderer. The type STAYS listed, the resolved state `webhook` and
|
|
9113
|
+
// `email_template` already sit in: a zero-warn entry costs one empty map
|
|
9114
|
+
// lookup, and it means a future regression that re-deadens a widget key warns
|
|
9115
|
+
// on its own instead of waiting for someone to notice this list again.
|
|
8069
9116
|
{ type: "dashboard", key: "dashboards" }
|
|
8070
9117
|
];
|
|
8071
9118
|
function lintLivenessProperties(stack) {
|
|
@@ -8074,11 +9121,11 @@ function lintLivenessProperties(stack) {
|
|
|
8074
9121
|
const findings = [];
|
|
8075
9122
|
const objectWarn = loadWarnMap(dir, "object");
|
|
8076
9123
|
const fieldWarn = loadWarnMap(dir, "field");
|
|
8077
|
-
for (const obj of
|
|
9124
|
+
for (const obj of asArray42(stack.objects)) {
|
|
8078
9125
|
const objName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8079
9126
|
if (objectWarn.size > 0) checkItem("object", obj, `object '${objName}'`, objectWarn, findings);
|
|
8080
9127
|
if (fieldWarn.size > 0) {
|
|
8081
|
-
for (const field of
|
|
9128
|
+
for (const field of asArray42(obj.fields)) {
|
|
8082
9129
|
const fieldName = typeof field.name === "string" ? field.name : "(unnamed field)";
|
|
8083
9130
|
checkItem("field", field, `object '${objName}' \xB7 field '${fieldName}'`, fieldWarn, findings);
|
|
8084
9131
|
}
|
|
@@ -8087,7 +9134,7 @@ function lintLivenessProperties(stack) {
|
|
|
8087
9134
|
for (const { type, key } of TYPE_COLLECTIONS) {
|
|
8088
9135
|
const warnMap = loadWarnMap(dir, type);
|
|
8089
9136
|
if (warnMap.size === 0) continue;
|
|
8090
|
-
for (const item of
|
|
9137
|
+
for (const item of asArray42(stack[key])) {
|
|
8091
9138
|
const name = typeof item.name === "string" ? item.name : typeof item.object === "string" ? item.object : `(unnamed ${type})`;
|
|
8092
9139
|
checkItem(type, item, `${type} '${name}'`, warnMap, findings);
|
|
8093
9140
|
}
|
|
@@ -8096,12 +9143,12 @@ function lintLivenessProperties(stack) {
|
|
|
8096
9143
|
}
|
|
8097
9144
|
|
|
8098
9145
|
// src/lint-autonumber-formats.ts
|
|
8099
|
-
var
|
|
9146
|
+
var import_data9 = require("@objectstack/spec/data");
|
|
8100
9147
|
var AUTONUMBER_UNKNOWN_FIELD = "autonumber-references-unknown-field";
|
|
8101
9148
|
var AUTONUMBER_OPTIONAL_FIELD = "autonumber-references-optional-field";
|
|
8102
9149
|
var AUTONUMBER_SELF_REFERENCE = "autonumber-references-self";
|
|
8103
9150
|
var AUTONUMBER_LITERAL_TOKEN = "autonumber-unrecognized-token";
|
|
8104
|
-
function
|
|
9151
|
+
function asArray43(v) {
|
|
8105
9152
|
if (Array.isArray(v)) return v;
|
|
8106
9153
|
if (v && typeof v === "object") {
|
|
8107
9154
|
return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
@@ -8110,9 +9157,9 @@ function asArray44(v) {
|
|
|
8110
9157
|
}
|
|
8111
9158
|
function lintAutonumberFormats(stack) {
|
|
8112
9159
|
const findings = [];
|
|
8113
|
-
for (const obj of
|
|
9160
|
+
for (const obj of asArray43(stack.objects)) {
|
|
8114
9161
|
const objectName = typeof obj.name === "string" ? obj.name : "(unnamed object)";
|
|
8115
|
-
const fields =
|
|
9162
|
+
const fields = asArray43(obj.fields);
|
|
8116
9163
|
const fieldMeta = /* @__PURE__ */ new Map();
|
|
8117
9164
|
for (const f of fields) {
|
|
8118
9165
|
if (typeof f.name === "string") fieldMeta.set(f.name, { required: f.required === true });
|
|
@@ -8122,8 +9169,8 @@ function lintAutonumberFormats(stack) {
|
|
|
8122
9169
|
const name = typeof f.name === "string" ? f.name : "(unnamed field)";
|
|
8123
9170
|
const fmt = typeof f.autonumberFormat === "string" ? f.autonumberFormat : typeof f.format === "string" ? f.format : "";
|
|
8124
9171
|
if (!fmt) continue;
|
|
8125
|
-
const tokens = (0,
|
|
8126
|
-
const refs = (0,
|
|
9172
|
+
const tokens = (0, import_data9.parseAutonumberFormat)(fmt);
|
|
9173
|
+
const refs = (0, import_data9.referencedFields)(tokens);
|
|
8127
9174
|
const where = `object '${objectName}' \xB7 field '${name}' (autonumber "${fmt}")`;
|
|
8128
9175
|
for (const t of tokens) {
|
|
8129
9176
|
if (t.kind !== "literal") continue;
|
|
@@ -8177,8 +9224,8 @@ function lintAutonumberFormats(stack) {
|
|
|
8177
9224
|
}
|
|
8178
9225
|
|
|
8179
9226
|
// src/lint-view-refs.ts
|
|
8180
|
-
var
|
|
8181
|
-
function
|
|
9227
|
+
var import_spec5 = require("@objectstack/spec");
|
|
9228
|
+
function asArray44(v) {
|
|
8182
9229
|
if (Array.isArray(v)) return v;
|
|
8183
9230
|
if (v && typeof v === "object") return Object.entries(v).map(([name, def]) => ({ name, ...def }));
|
|
8184
9231
|
return [];
|
|
@@ -8206,16 +9253,16 @@ function lintViewRefs(stack) {
|
|
|
8206
9253
|
s.add(kind);
|
|
8207
9254
|
};
|
|
8208
9255
|
const containers = [];
|
|
8209
|
-
for (const v of
|
|
9256
|
+
for (const v of asArray44(stack.views)) {
|
|
8210
9257
|
if (v.viewKind) {
|
|
8211
9258
|
if (typeof v.name === "string") indexKind(v.name, v.viewKind === "form" ? "form" : "list");
|
|
8212
9259
|
continue;
|
|
8213
9260
|
}
|
|
8214
|
-
if (!(0,
|
|
9261
|
+
if (!(0, import_spec5.isAggregatedViewContainer)(v)) continue;
|
|
8215
9262
|
const object = viewContainerObjectName(v);
|
|
8216
9263
|
if (object) containers.push({ object, container: v });
|
|
8217
9264
|
}
|
|
8218
|
-
for (const obj of
|
|
9265
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8219
9266
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8220
9267
|
if (!object) continue;
|
|
8221
9268
|
if (obj.list || obj.form || obj.listViews || obj.formViews) {
|
|
@@ -8223,7 +9270,7 @@ function lintViewRefs(stack) {
|
|
|
8223
9270
|
}
|
|
8224
9271
|
}
|
|
8225
9272
|
for (const { object, container } of containers) {
|
|
8226
|
-
const { items, collisions } = (0,
|
|
9273
|
+
const { items, collisions } = (0, import_spec5.expandViewContainerWithDiagnostics)(object, container);
|
|
8227
9274
|
for (const it of items) indexKind(it.name, it.viewKind);
|
|
8228
9275
|
for (const col of collisions) {
|
|
8229
9276
|
findings.push({
|
|
@@ -8269,11 +9316,11 @@ function lintViewRefs(stack) {
|
|
|
8269
9316
|
});
|
|
8270
9317
|
}
|
|
8271
9318
|
};
|
|
8272
|
-
for (const obj of
|
|
9319
|
+
for (const obj of asArray44(stack.objects)) {
|
|
8273
9320
|
const object = typeof obj.name === "string" ? obj.name : void 0;
|
|
8274
|
-
for (const action of
|
|
9321
|
+
for (const action of asArray44(obj.actions)) checkAction(action, object);
|
|
8275
9322
|
}
|
|
8276
|
-
for (const action of
|
|
9323
|
+
for (const action of asArray44(stack.actions)) checkAction(action);
|
|
8277
9324
|
return findings;
|
|
8278
9325
|
}
|
|
8279
9326
|
|
|
@@ -8461,13 +9508,14 @@ function lintDataModel(objects) {
|
|
|
8461
9508
|
if (!obj?.name) continue;
|
|
8462
9509
|
const objPath = `objects[${i}]`;
|
|
8463
9510
|
const fields = fieldEntries2(obj.fields);
|
|
8464
|
-
const hasNameField = !!obj.
|
|
9511
|
+
const hasNameField = !!obj.nameField || fields.some((f) => NAME_LIKE_FIELDS.includes(f.name));
|
|
8465
9512
|
if (fields.length > 0 && !hasNameField) {
|
|
8466
9513
|
issues.push({
|
|
8467
9514
|
severity: "suggestion",
|
|
8468
9515
|
rule: "object/missing-name-field",
|
|
8469
|
-
message: `Object "${obj.name}" has no name
|
|
8470
|
-
path: `${objPath}.fields
|
|
9516
|
+
message: `Object "${obj.name}" has no nameField and no name-like field \u2014 records will display as raw IDs`,
|
|
9517
|
+
path: `${objPath}.fields`,
|
|
9518
|
+
fix: `Set \`nameField: '<field>'\` \u2014 ADR-0079's canonical primary-title pointer \u2014 to a stored text/autonumber field, or to a formula field with \`returnType: 'text'\` for a composite title. A \`titleFormat\` template does NOT count: it is retired (ADR-0079) and render-only, so the server can neither return nor query the title it renders.`
|
|
8471
9519
|
});
|
|
8472
9520
|
}
|
|
8473
9521
|
for (const { name: fieldName, def } of fields) {
|
|
@@ -8576,6 +9624,7 @@ var CLI_ONLY = ["cli"];
|
|
|
8576
9624
|
var CLI_AND_RUNTIME = ["cli", "runtime-publish"];
|
|
8577
9625
|
var RUNTIME_NEEDS_FULL_SNAPSHOT = "P2 (#4463): reads a stack-wide collection the per-write snapshot does not carry, so running it now would report the rest of the tenant's metadata as missing rather than judging this write.";
|
|
8578
9626
|
var RUNTIME_HEAVY_SOURCE_PARSE = "Not runtime-safe: parses authored source through typescript/sucrase, the two dependencies the kernel boot path must never load (lazy-deps.test.ts). Studio compiles page source on its own path.";
|
|
9627
|
+
var RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY = "Deliberate, and not a snapshot limitation: this rule needs only the written item, but every other rule on the `views[]` visibility-predicate surface (validate-visibility-predicates.ts) is CLI-only. Gating one of three sibling verdicts about the same predicate at the Studio door is less predictable than gating none; move the family together, as one measured edit.";
|
|
8579
9628
|
var RUNTIME_OBJECT_WRITES_P2 = "P2 (#4463): judges an object/field declaration. Object writes are the hottest metadata path in the product, so P1 gates `flow` first and widens once the gate has real traffic behind it.";
|
|
8580
9629
|
var EXPRESSION_INVALID = "expression-invalid";
|
|
8581
9630
|
var AUTHORING_RULES = [
|
|
@@ -8605,8 +9654,12 @@ var AUTHORING_RULES = [
|
|
|
8605
9654
|
}))
|
|
8606
9655
|
},
|
|
8607
9656
|
// ADR-0053 — `userFilters`/`quickFilters` on an object list view ("views"
|
|
8608
|
-
// mode)
|
|
8609
|
-
//
|
|
9657
|
+
// mode). NOT "silently dropped" any more: since #4001 `ObjectListViewSchema`
|
|
9658
|
+
// is strict and refuses `quickFilters` by name, and `ObjectUserFiltersSchema`
|
|
9659
|
+
// refuses `element: 'tabs'` by enum — measured under #6073, `defineStack`
|
|
9660
|
+
// THROWS on both. `normalized` here therefore means "needs no parsed stack"
|
|
9661
|
+
// (so `os lint`, which never parses, can run it), not "sees evidence the
|
|
9662
|
+
// parse would have eaten".
|
|
8610
9663
|
{
|
|
8611
9664
|
name: "validateListViewMode",
|
|
8612
9665
|
tier: "gating",
|
|
@@ -8637,9 +9690,13 @@ var AUTHORING_RULES = [
|
|
|
8637
9690
|
surfaceReason: RUNTIME_OBJECT_WRITES_P2,
|
|
8638
9691
|
run: (stack) => validateFunctionalCompleteness(stack)
|
|
8639
9692
|
},
|
|
8640
|
-
// A
|
|
8641
|
-
//
|
|
8642
|
-
//
|
|
9693
|
+
// A view container in `views: []` that registers zero views: nothing appears
|
|
9694
|
+
// in the Console, and the schema step cannot tell it from an intentionally
|
|
9695
|
+
// empty one. The FLAT-list-view arm no longer needs this tier — `ViewSchema`
|
|
9696
|
+
// went strict at #4001, so `defineStack` now REFUSES `{ name, type, columns,
|
|
9697
|
+
// data }` by name with the wrap-it hint (measured under #6073); the arm that
|
|
9698
|
+
// still needs a rule is the all-slots-empty container, whose keys are all
|
|
9699
|
+
// declared and which survives the parse untouched.
|
|
8643
9700
|
{
|
|
8644
9701
|
name: "validateViewContainers",
|
|
8645
9702
|
tier: "gating",
|
|
@@ -8689,6 +9746,31 @@ var AUTHORING_RULES = [
|
|
|
8689
9746
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8690
9747
|
run: (stack) => validateFilterTokens(stack)
|
|
8691
9748
|
},
|
|
9749
|
+
// #5330 — the LITERAL empty combinators (`$and: []`, `$or: []`, `$not: {}`,
|
|
9750
|
+
// `{}`). #5322 ruled their RUNTIME meaning to be the boolean identity, and
|
|
9751
|
+
// this rule does not touch it: it refuses the literal SPELLINGS at authoring
|
|
9752
|
+
// time with a per-shape prescription, which is Prime Directive #12's standard
|
|
9753
|
+
// shape (reject at the producer, never tolerate at the consumer) and #5240's
|
|
9754
|
+
// same-direction precedent one shape over.
|
|
9755
|
+
{
|
|
9756
|
+
name: "validateEmptyCombinators",
|
|
9757
|
+
tier: "gating",
|
|
9758
|
+
input: "parsed",
|
|
9759
|
+
commands: ALL,
|
|
9760
|
+
source: "packages/lint/src/validate-empty-combinators.ts",
|
|
9761
|
+
// The one type #4463's P1 slice opened, and the one this rule most needs:
|
|
9762
|
+
// a flow CRUD node's `config.filter` is where an empty combinator has the
|
|
9763
|
+
// largest blast radius, and the write path is the only door an AI author
|
|
9764
|
+
// uses. This rule needs NO resolution context at all — it judges the filter
|
|
9765
|
+
// literal in isolation — so RUNTIME_NEEDS_FULL_SNAPSHOT does not apply to
|
|
9766
|
+
// it, and widening to the other filter-carrying types (`object`, `view`,
|
|
9767
|
+
// `page`, `dashboard`) is a one-line `runtimeTypes` edit once #4463 P2
|
|
9768
|
+
// opens them at the gate. Making that call here would widen the gate's
|
|
9769
|
+
// dispatch surface on this rule's authority, which is P2's decision.
|
|
9770
|
+
surfaces: CLI_AND_RUNTIME,
|
|
9771
|
+
runtimeTypes: ["flow"],
|
|
9772
|
+
run: (stack) => validateEmptyCombinators(stack)
|
|
9773
|
+
},
|
|
8692
9774
|
// The reference-integrity suite (#3583 §5 D5) — itself a registry, of the
|
|
8693
9775
|
// rules that answer "does this name resolve to anything?". It reached all
|
|
8694
9776
|
// three commands before this file existed; it is an entry here so the two
|
|
@@ -8737,6 +9819,16 @@ var AUTHORING_RULES = [
|
|
|
8737
9819
|
// `displayField` (#5775) — so gating today would fail the platform's own pages
|
|
8738
9820
|
// to enforce declarations the platform does not keep. The error upgrade is a
|
|
8739
9821
|
// separate step, once the warning-period inventory is empty.
|
|
9822
|
+
//
|
|
9823
|
+
// #5775 settled the record picker's half: `displayField` is retired in favour
|
|
9824
|
+
// of the `labelField` the renderer actually reads. Its claim that "the rest of
|
|
9825
|
+
// the keys the renderers honour are declared" did NOT hold — #6776 found five
|
|
9826
|
+
// more (`page:header` `recordChrome`/`showStar`/`showCopyId`,
|
|
9827
|
+
// `page:accordion.variant`, and the tab strip's visual style, whose declared
|
|
9828
|
+
// spelling `page:tabs.type` collided with the component node's own dispatch
|
|
9829
|
+
// key and so was unauthorable in the flat and JSX carriers). All five are
|
|
9830
|
+
// declared as of #6776, the last as the renamed `tabStyle`. What remains
|
|
9831
|
+
// before the error upgrade is #5728 and two page rewrites.
|
|
8740
9832
|
{
|
|
8741
9833
|
name: "validateComponentProps",
|
|
8742
9834
|
tier: "advisory",
|
|
@@ -8815,10 +9907,12 @@ var AUTHORING_RULES = [
|
|
|
8815
9907
|
//
|
|
8816
9908
|
// `gating` since #5762, which reviewed the file's rules as one family and
|
|
8817
9909
|
// split them on a single question: is THIS STACK enough to know the flow is
|
|
8818
|
-
// dead?
|
|
9910
|
+
// dead? Four rules answer yes and emit `error` — a `config.timeRelative`
|
|
8819
9911
|
// the spec's own `TimeRelativeTriggerSchema` refuses, one the engine's routing
|
|
8820
|
-
// predicate cannot route at all,
|
|
8821
|
-
// closed token grammar `triggerTypeToHookEvents` maps
|
|
9912
|
+
// predicate cannot route at all, a `record-*` triggerType outside the
|
|
9913
|
+
// closed token grammar `triggerTypeToHookEvents` maps, and (#6637) a
|
|
9914
|
+
// `type: 'record_change'` flow whose triggerType the engine's binding resolver
|
|
9915
|
+
// routes nowhere, silently demoting it to a manual flow. None of those verdicts
|
|
8822
9916
|
// can be changed by installing a package, so there is no reading under which
|
|
8823
9917
|
// the flow fires. `flow-trigger-unknown-object` deliberately stayed `warning`
|
|
8824
9918
|
// (the object may come from another installed package — a hedge this rule
|
|
@@ -8945,12 +10039,39 @@ var AUTHORING_RULES = [
|
|
|
8945
10039
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8946
10040
|
run: (stack) => validateSeedStateMachine(stack)
|
|
8947
10041
|
},
|
|
8948
|
-
// ADR-0089 D3b —
|
|
8949
|
-
//
|
|
8950
|
-
//
|
|
10042
|
+
// ADR-0089 D3b — a mis-layered binding root, plus (#6128) the bare-identifier
|
|
10043
|
+
// gate and (#6253) the syntax gate. This entry used to read "pre-parse: the
|
|
10044
|
+
// schema folds `visibleOn`/`visibility` into `visibleWhen` during parse, so
|
|
10045
|
+
// the alias the author wrote is gone from `result.data`". Measured false at
|
|
10046
|
+
// #6073: the ADR-0087 D2 conversions do that fold INSIDE
|
|
10047
|
+
// `normalizeStackInput`, one layer BEFORE this tier, so on every spec-valid
|
|
10048
|
+
// alias site the alias-KEY rule reported zero here too.
|
|
10049
|
+
//
|
|
10050
|
+
// #6318 closed that: `visibility-alias-deprecated` was RETIRED rather than
|
|
10051
|
+
// re-anchored. Re-anchoring would have had to move this entry's input to a
|
|
10052
|
+
// pre-`normalizeStackInput` value that `runAuthoringRules` does not accept —
|
|
10053
|
+
// a change to this package's external input contract, and the maintainer's
|
|
10054
|
+
// call, not a rule file's. Retirement is ADR-0049 (declared ≠ enforced) and
|
|
10055
|
+
// costs no author a signal: the same D2 conversion already shouts through
|
|
10056
|
+
// `warnConversionNotice` in `defineStack`, naming the site, the conversion and
|
|
10057
|
+
// the protocol-16 retirement window — better wording than the rule ever had.
|
|
10058
|
+
//
|
|
10059
|
+
// Every rule left in the family judges the predicate's VALUE, and the value
|
|
10060
|
+
// moves into `visibleWhen` intact, so all three report normally on this tier.
|
|
10061
|
+
// The tier therefore stays `normalized` on its SURVIVING justification (a
|
|
10062
|
+
// finding still reaches the author when an unrelated schema error would stop
|
|
10063
|
+
// the parse — see `AuthoringRuleInputTier`), never on the retired
|
|
10064
|
+
// "pre-parse evidence" one.
|
|
10065
|
+
//
|
|
10066
|
+
// `gating` since #6128: `visibility-bare-identifier` emits `error`. The two
|
|
10067
|
+
// ADR-0089 rules stay advisory findings within it — the tier is a property of
|
|
10068
|
+
// the RULE FUNCTION (can it emit `error`?), and the per-finding severity is
|
|
10069
|
+
// what decides whether any given diagnostic gates, exactly as `lintFlowPatterns`
|
|
10070
|
+
// has worked since #3760. The promotion follows the #5762 precedent: a family
|
|
10071
|
+
// that gains an `error` finding moves its registry tier in the same edit.
|
|
8951
10072
|
{
|
|
8952
10073
|
name: "validateVisibilityPredicates",
|
|
8953
|
-
tier: "
|
|
10074
|
+
tier: "gating",
|
|
8954
10075
|
input: "normalized",
|
|
8955
10076
|
commands: ALL,
|
|
8956
10077
|
source: "packages/lint/src/validate-visibility-predicates.ts",
|
|
@@ -8958,6 +10079,30 @@ var AUTHORING_RULES = [
|
|
|
8958
10079
|
surfaceReason: RUNTIME_NEEDS_FULL_SNAPSHOT,
|
|
8959
10080
|
run: (stack) => validateVisibilityPredicates(stack)
|
|
8960
10081
|
},
|
|
10082
|
+
// #7010 — the same predicate surface, one question further in. The three
|
|
10083
|
+
// ADR-0089 D3b rules above judge a predicate's SHAPE (does it parse, is it
|
|
10084
|
+
// rooted, is the root right for the layer) and never open the target schema,
|
|
10085
|
+
// so `data.tpye == 'formula'` passes all three and still resolves to nothing.
|
|
10086
|
+
// This rule resolves the PATH against the schema the form edits — the closed
|
|
10087
|
+
// `getMetadataTypeSchema` key set — and is therefore immune to the CEL
|
|
10088
|
+
// type-name blind spot that made #6248's gate structurally unable to catch
|
|
10089
|
+
// #6254's 16 bare `type ==` predicates.
|
|
10090
|
+
//
|
|
10091
|
+
// Scoped to schema-bound forms (`data: { provider: 'schema', schemaId }`);
|
|
10092
|
+
// the `record.*` layer is deliberately out of scope because an ObjectQL
|
|
10093
|
+
// object's addressable path set is NOT closed (lookup traversal, system
|
|
10094
|
+
// columns, formula outputs), and an `error` gate over an open set generates
|
|
10095
|
+
// false build errors. See the rule's module note.
|
|
10096
|
+
{
|
|
10097
|
+
name: "validatePredicatePathRefs",
|
|
10098
|
+
tier: "gating",
|
|
10099
|
+
input: "normalized",
|
|
10100
|
+
commands: ALL,
|
|
10101
|
+
source: "packages/lint/src/validate-predicate-path-refs.ts",
|
|
10102
|
+
surfaces: CLI_ONLY,
|
|
10103
|
+
surfaceReason: RUNTIME_VISIBILITY_FAMILY_IS_CLI_ONLY,
|
|
10104
|
+
run: (stack) => validatePredicatePathRefs(stack)
|
|
10105
|
+
},
|
|
8961
10106
|
// #1874 — flow authoring anti-patterns. Advisory by default; a finding marked
|
|
8962
10107
|
// `error` gates. Three do today: `flow-runas-unscoped` (#3760 — metadata the
|
|
8963
10108
|
// runtime REFUSES to execute), plus `flow-branch-label-unmatched` and
|
|
@@ -9354,10 +10499,13 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9354
10499
|
DASHBOARD_ACTION_ROUTE_UNRESOLVED,
|
|
9355
10500
|
DASHBOARD_ACTION_TARGET_UNDEFINED,
|
|
9356
10501
|
DASHBOARD_FILTER_FIELD_UNKNOWN,
|
|
10502
|
+
DEFAULT_AGENT_OUTSIDE_ROSTER,
|
|
9357
10503
|
EXPRESSION_INVALID,
|
|
9358
10504
|
FIELD_GROUP_EMPTY,
|
|
9359
10505
|
FIELD_GROUP_SHADOWED,
|
|
9360
10506
|
FIELD_GROUP_UNDECLARED,
|
|
10507
|
+
FILTER_EMPTY_COMBINATOR,
|
|
10508
|
+
FILTER_EMPTY_NODE,
|
|
9361
10509
|
FILTER_TOKEN_UNKNOWN,
|
|
9362
10510
|
FLOW_APPROVAL_REVISE_DEAD_END,
|
|
9363
10511
|
FLOW_APPROVAL_REVISE_DISABLED,
|
|
@@ -9384,6 +10532,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9384
10532
|
FLOW_TIME_RELATIVE_DESCRIPTOR_UNROUTABLE,
|
|
9385
10533
|
FLOW_TRIGGER_UNKNOWN_EVENT,
|
|
9386
10534
|
FLOW_TRIGGER_UNKNOWN_OBJECT,
|
|
10535
|
+
FLOW_TRIGGER_UNROUTABLE,
|
|
9387
10536
|
FLOW_UPDATE_READONLY_FIELD,
|
|
9388
10537
|
FLOW_UPDATE_READONLY_WHEN_FIELD,
|
|
9389
10538
|
FLOW_WRITE_NODE_TYPES,
|
|
@@ -9404,19 +10553,25 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9404
10553
|
NULL_GUARD_HINT,
|
|
9405
10554
|
OBJECT_REFERENCE_UNKNOWN,
|
|
9406
10555
|
OBJECT_REFERENCE_UNREGISTERED_PLATFORM,
|
|
10556
|
+
OPEN_VOCABULARY_PROBES,
|
|
9407
10557
|
ORG_AXIS_CROSS_ORG_BU_GRANT,
|
|
9408
10558
|
ORG_AXIS_PERMISSION_INHERITANCE,
|
|
9409
10559
|
PAGE_FIELD_UNKNOWN,
|
|
9410
10560
|
PAGE_SOURCE_CLASSNAME,
|
|
10561
|
+
PREDICATE_PATH_UNRESOLVED,
|
|
10562
|
+
PREDICATE_PATH_UNROOTED,
|
|
10563
|
+
PRE_SEAL_PHASES,
|
|
9411
10564
|
REACT_BLOCK_NEEDS_RECORD_CONTEXT,
|
|
9412
10565
|
REACT_CHART_AGGREGATE_INVALID,
|
|
9413
10566
|
REACT_CHART_AXIS_UNKNOWN,
|
|
9414
10567
|
REACT_CHART_DRILLDOWN_INVALID,
|
|
9415
10568
|
REACT_CHART_FIELD_UNKNOWN,
|
|
9416
10569
|
REFERENCE_INTEGRITY_RULES,
|
|
10570
|
+
RLS_PREDICATE_OVER_BUDGET,
|
|
9417
10571
|
RLS_PREDICATE_UNENFORCEABLE,
|
|
9418
10572
|
RLS_PREDICATE_UNPARSEABLE,
|
|
9419
10573
|
RUNTIME_AJV_OPTIONS,
|
|
10574
|
+
SEAL_MARKERS,
|
|
9420
10575
|
SEARCHABLE_FIELD_UNKNOWN,
|
|
9421
10576
|
SEARCHABLE_FIELD_UNSEARCHABLE,
|
|
9422
10577
|
SECURITY_ANCHOR_HIGH_PRIVILEGE,
|
|
@@ -9436,6 +10591,9 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9436
10591
|
SEMANTIC_ROLE_FIELD_UNKNOWN,
|
|
9437
10592
|
SHARING_RULE_RUNTIME_VARIABLE_CONDITION,
|
|
9438
10593
|
SHARING_RULE_UNLOWERABLE_CONDITION,
|
|
10594
|
+
STARTUP_OPEN_VOCABULARY_VERDICT,
|
|
10595
|
+
STARTUP_VERDICT_ASSERTIVE_WORDING,
|
|
10596
|
+
STARTUP_VERDICT_HINT,
|
|
9439
10597
|
STYLE_CLASSNAME_TAILWIND,
|
|
9440
10598
|
STYLE_NODE_MISSING_ID,
|
|
9441
10599
|
STYLE_RESPONSIVE_NO_BASE,
|
|
@@ -9457,7 +10615,9 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9457
10615
|
VIEW_KEY_COLLISION,
|
|
9458
10616
|
VIEW_REF_FORM_TARGET_KIND,
|
|
9459
10617
|
VIEW_REF_FORM_TARGET_MISSING,
|
|
9460
|
-
|
|
10618
|
+
VISIBILITY_BARE_IDENTIFIER,
|
|
10619
|
+
VISIBILITY_PREDICATE_OVER_BUDGET,
|
|
10620
|
+
VISIBILITY_PREDICATE_SYNTAX,
|
|
9461
10621
|
VISIBILITY_ROOT_MISLAYERED,
|
|
9462
10622
|
WIDGET_DATASET_UNKNOWN,
|
|
9463
10623
|
WIDGET_DIMENSION_UNKNOWN,
|
|
@@ -9469,6 +10629,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9469
10629
|
diffAccessMatrix,
|
|
9470
10630
|
extractHookBodyWriteSet,
|
|
9471
10631
|
extractHookBodyWrites,
|
|
10632
|
+
findStartupRegistryVerdicts,
|
|
9472
10633
|
findUnguardedNullableOperands,
|
|
9473
10634
|
isSourceAuthoredPage,
|
|
9474
10635
|
lintAutonumberFormats,
|
|
@@ -9498,6 +10659,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9498
10659
|
validateChartBindings,
|
|
9499
10660
|
validateComponentProps,
|
|
9500
10661
|
validateDashboardActionRefs,
|
|
10662
|
+
validateEmptyCombinators,
|
|
9501
10663
|
validateFilterTokens,
|
|
9502
10664
|
validateFlowNodeWrites,
|
|
9503
10665
|
validateFlowTemplatePaths,
|
|
@@ -9513,6 +10675,7 @@ function runRuntimeAuthoringRules(args) {
|
|
|
9513
10675
|
validateOrgAxisRedLines,
|
|
9514
10676
|
validatePageFieldBindings,
|
|
9515
10677
|
validatePageSourceStyling,
|
|
10678
|
+
validatePredicatePathRefs,
|
|
9516
10679
|
validateReactPageProps,
|
|
9517
10680
|
validateReactPages,
|
|
9518
10681
|
validateReadonlyFlowWrites,
|